Privacy Settings: Implement premium category (#4536)

This commit is contained in:
Alexander Zinchuk 2024-05-14 04:23:26 +02:00
parent d64ed45af8
commit cf8eaf270e
19 changed files with 304 additions and 130 deletions

View File

@ -1,4 +1,5 @@
.Avatar { .Avatar {
--premium-gradient: linear-gradient(88.39deg, #6C93FF -2.56%, #976FFF 51.27%, #DF69D1 107.39%);
--color-user: var(--color-primary); --color-user: var(--color-primary);
--radius: 50%; --radius: 50%;
@ -258,7 +259,7 @@
--color-user: var(--color-deleted-account); --color-user: var(--color-deleted-account);
} }
&.unknown-user { &.premium-gradient-bg > .inner {
background: var(--premium-gradient); background-image: var(--premium-gradient);
} }
} }

View File

@ -7,7 +7,7 @@ import type {
ApiChat, ApiPeer, ApiPhoto, ApiUser, ApiChat, ApiPeer, ApiPhoto, ApiUser,
} from '../../api/types'; } from '../../api/types';
import type { ObserveFn } from '../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../hooks/useIntersectionObserver';
import type { StoryViewerOrigin } from '../../types'; import type { CustomPeer, StoryViewerOrigin } from '../../types';
import { ApiMediaFormat } from '../../api/types'; import { ApiMediaFormat } from '../../api/types';
import { IS_TEST } from '../../config'; import { IS_TEST } from '../../config';
@ -49,11 +49,10 @@ cn.icon = cn('icon');
type OwnProps = { type OwnProps = {
className?: string; className?: string;
size?: AvatarSize; size?: AvatarSize;
peer?: ApiPeer; peer?: ApiPeer | CustomPeer;
photo?: ApiPhoto; photo?: ApiPhoto;
text?: string; text?: string;
isSavedMessages?: boolean; isSavedMessages?: boolean;
isUnknownUser?: boolean;
isSavedDialog?: boolean; isSavedDialog?: boolean;
withVideo?: boolean; withVideo?: boolean;
withStory?: boolean; withStory?: boolean;
@ -78,7 +77,6 @@ const Avatar: FC<OwnProps> = ({
text, text,
isSavedMessages, isSavedMessages,
isSavedDialog, isSavedDialog,
isUnknownUser,
withVideo, withVideo,
withStory, withStory,
forPremiumPromo, forPremiumPromo,
@ -97,12 +95,14 @@ const Avatar: FC<OwnProps> = ({
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const videoLoopCountRef = useRef(0); const videoLoopCountRef = useRef(0);
const isPeerChat = peer && 'title' in peer; const isCustomPeer = peer && 'isCustomPeer' in peer;
const realPeer = peer && !isCustomPeer ? peer : undefined;
const isPeerChat = realPeer && 'title' in realPeer;
const user = peer && !isPeerChat ? peer as ApiUser : undefined; const user = peer && !isPeerChat ? peer as ApiUser : undefined;
const chat = peer && isPeerChat ? peer as ApiChat : undefined; const chat = peer && isPeerChat ? peer as ApiChat : undefined;
const isDeleted = user && isDeletedUser(user); const isDeleted = user && isDeletedUser(user);
const isReplies = peer && isChatWithRepliesBot(peer.id); const isReplies = realPeer && isChatWithRepliesBot(realPeer.id);
const isAnonymousForwards = peer && isAnonymousForwardsChat(peer.id); const isAnonymousForwards = realPeer && isAnonymousForwardsChat(realPeer.id);
const isForum = chat?.isForum; const isForum = chat?.isForum;
let imageHash: string | undefined; let imageHash: string | undefined;
let videoHash: string | undefined; let videoHash: string | undefined;
@ -112,7 +112,7 @@ const Avatar: FC<OwnProps> = ({
const shouldFetchBig = size === 'jumbo'; const shouldFetchBig = size === 'jumbo';
if (!isSavedMessages && !isDeleted) { if (!isSavedMessages && !isDeleted) {
if ((user && !noPersonalPhoto) || chat) { if ((user && !noPersonalPhoto) || chat) {
imageHash = getChatAvatarHash(peer!, shouldFetchBig ? 'big' : undefined); imageHash = getChatAvatarHash(peer as ApiPeer, shouldFetchBig ? 'big' : undefined);
} else if (photo) { } else if (photo) {
imageHash = `photo${photo.id}?size=m`; imageHash = `photo${photo.id}?size=m`;
if (photo.isVideo && withVideo) { if (photo.isVideo && withVideo) {
@ -122,8 +122,8 @@ const Avatar: FC<OwnProps> = ({
} }
const specialIcon = useMemo(() => { const specialIcon = useMemo(() => {
if (isUnknownUser) { if (isCustomPeer) {
return 'user'; return peer.avatarIcon;
} }
if (isSavedMessages) { if (isSavedMessages) {
@ -143,7 +143,7 @@ const Avatar: FC<OwnProps> = ({
} }
return undefined; return undefined;
}, [isUnknownUser, isSavedMessages, isDeleted, isReplies, isAnonymousForwards, isSavedDialog]); }, [isCustomPeer, isSavedMessages, isDeleted, isReplies, isAnonymousForwards, peer, isSavedDialog]);
const imgBlobUrl = useMedia(imageHash, false, ApiMediaFormat.BlobUrl); const imgBlobUrl = useMedia(imageHash, false, ApiMediaFormat.BlobUrl);
const videoBlobUrl = useMedia(videoHash, !shouldLoadVideo, ApiMediaFormat.BlobUrl); const videoBlobUrl = useMedia(videoHash, !shouldLoadVideo, ApiMediaFormat.BlobUrl);
@ -215,23 +215,25 @@ const Avatar: FC<OwnProps> = ({
content = getFirstLetters(text, 2); content = getFirstLetters(text, 2);
} }
const isRoundedRect = isForum && !((withStory || withStorySolid) && peer?.hasStories); const isRoundedRect = (isCustomPeer && peer.isAvatarSquare)
|| (isForum && !((withStory || withStorySolid) && realPeer?.hasStories));
const isPremiumGradient = isCustomPeer && peer.withPremiumGradient;
const fullClassName = buildClassName( const fullClassName = buildClassName(
`Avatar size-${size}`, `Avatar size-${size}`,
className, className,
getPeerColorClass(peer), getPeerColorClass(peer),
isUnknownUser && 'unknown-user',
!peer && text && 'hidden-user', !peer && text && 'hidden-user',
isSavedMessages && 'saved-messages', isSavedMessages && 'saved-messages',
isAnonymousForwards && 'anonymous-forwards', isAnonymousForwards && 'anonymous-forwards',
isDeleted && 'deleted-account', isDeleted && 'deleted-account',
isReplies && 'replies-bot-account', isReplies && 'replies-bot-account',
isPremiumGradient && 'premium-gradient-bg',
isRoundedRect && 'forum', isRoundedRect && 'forum',
((withStory && peer?.hasStories) || forPremiumPromo) && 'with-story-circle', ((withStory && realPeer?.hasStories) || forPremiumPromo) && 'with-story-circle',
withStorySolid && peer?.hasStories && 'with-story-solid', withStorySolid && realPeer?.hasStories && 'with-story-solid',
withStorySolid && forceFriendStorySolid && 'close-friend', withStorySolid && forceFriendStorySolid && 'close-friend',
withStorySolid && (peer?.hasUnreadStories || forceUnreadStorySolid) && 'has-unread-story', withStorySolid && (realPeer?.hasUnreadStories || forceUnreadStorySolid) && 'has-unread-story',
onClick && 'interactive', onClick && 'interactive',
(!isSavedMessages && !imgBlobUrl) && 'no-photo', (!isSavedMessages && !imgBlobUrl) && 'no-photo',
); );
@ -239,11 +241,11 @@ const Avatar: FC<OwnProps> = ({
const hasMedia = Boolean(isSavedMessages || imgBlobUrl); const hasMedia = Boolean(isSavedMessages || imgBlobUrl);
const { handleClick, handleMouseDown } = useFastClick((e: ReactMouseEvent<HTMLDivElement, MouseEvent>) => { const { handleClick, handleMouseDown } = useFastClick((e: ReactMouseEvent<HTMLDivElement, MouseEvent>) => {
if (withStory && storyViewerMode !== 'disabled' && peer?.hasStories) { if (withStory && storyViewerMode !== 'disabled' && realPeer?.hasStories) {
e.stopPropagation(); e.stopPropagation();
openStoryViewer({ openStoryViewer({
peerId: peer.id, peerId: realPeer.id,
isSinglePeer: storyViewerMode === 'single-peer', isSinglePeer: storyViewerMode === 'single-peer',
origin: storyViewerOrigin, origin: storyViewerOrigin,
}); });
@ -259,9 +261,9 @@ const Avatar: FC<OwnProps> = ({
<div <div
ref={ref} ref={ref}
className={fullClassName} className={fullClassName}
id={peer?.id && withStory ? getPeerStoryHtmlId(peer.id) : undefined} id={realPeer?.id && withStory ? getPeerStoryHtmlId(realPeer.id) : undefined}
data-peer-id={peer?.id} data-peer-id={realPeer?.id}
data-test-sender-id={IS_TEST ? peer?.id : undefined} data-test-sender-id={IS_TEST ? realPeer?.id : undefined}
aria-label={typeof content === 'string' ? author : undefined} aria-label={typeof content === 'string' ? author : undefined}
onClick={handleClick} onClick={handleClick}
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
@ -269,8 +271,8 @@ const Avatar: FC<OwnProps> = ({
<div className="inner"> <div className="inner">
{typeof content === 'string' ? renderText(content, [size === 'jumbo' ? 'hq_emoji' : 'emoji']) : content} {typeof content === 'string' ? renderText(content, [size === 'jumbo' ? 'hq_emoji' : 'emoji']) : content}
</div> </div>
{withStory && peer?.hasStories && ( {withStory && realPeer?.hasStories && (
<AvatarStoryCircle peerId={peer.id} size={size} withExtraGap={withStoryGap} /> <AvatarStoryCircle peerId={realPeer.id} size={size} withExtraGap={withStoryGap} />
)} )}
</div> </div>
); );

View File

@ -2,8 +2,11 @@ import type { FC } from '../../lib/teact/teact';
import React, { memo, useMemo } from '../../lib/teact/teact'; import React, { memo, useMemo } from '../../lib/teact/teact';
import { getActions } from '../../global'; import { getActions } from '../../global';
import type { ApiChat, ApiPeer, ApiUser } from '../../api/types'; import type {
ApiChat, ApiPeer, ApiUser,
} from '../../api/types';
import type { ObserveFn } from '../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../hooks/useIntersectionObserver';
import type { CustomPeer } from '../../types';
import { EMOJI_STATUS_LOOP_LIMIT } from '../../config'; import { EMOJI_STATUS_LOOP_LIMIT } from '../../config';
import { import {
@ -25,7 +28,7 @@ import VerifiedIcon from './VerifiedIcon';
import styles from './FullNameTitle.module.scss'; import styles from './FullNameTitle.module.scss';
type OwnProps = { type OwnProps = {
peer?: ApiPeer; peer: ApiPeer | CustomPeer;
className?: string; className?: string;
noVerified?: boolean; noVerified?: boolean;
noFake?: boolean; noFake?: boolean;
@ -34,7 +37,6 @@ type OwnProps = {
isSavedMessages?: boolean; isSavedMessages?: boolean;
isSavedDialog?: boolean; isSavedDialog?: boolean;
noLoopLimit?: boolean; noLoopLimit?: boolean;
isUnknownUser?: boolean;
canCopyTitle?: boolean; canCopyTitle?: boolean;
onEmojiStatusClick?: NoneToVoidFunction; onEmojiStatusClick?: NoneToVoidFunction;
observeIntersection?: ObserveFn; observeIntersection?: ObserveFn;
@ -55,25 +57,15 @@ const FullNameTitle: FC<OwnProps> = ({
onEmojiStatusClick, onEmojiStatusClick,
observeIntersection, observeIntersection,
iconElement, iconElement,
isUnknownUser,
}) => { }) => {
const lang = useLang(); const lang = useLang();
const { showNotification } = getActions(); const { showNotification } = getActions();
const isUser = peer && isUserId(peer.id); const realPeer = 'id' in peer ? peer : undefined;
const customPeer = 'isCustomPeer' in peer ? peer : undefined;
const isUser = realPeer && isUserId(realPeer.id);
const title = realPeer && (isUser ? getUserFullName(realPeer as ApiUser) : getChatTitle(lang, realPeer as ApiChat));
const isPremium = isUser && (peer as ApiUser).isPremium; const isPremium = isUser && (peer as ApiUser).isPremium;
const title = useMemo(() => {
if (isUnknownUser) {
return lang('BoostingToBeDistributed');
}
if (peer && isUserId(peer.id)) {
return getUserFullName(peer as ApiUser);
}
return peer && getChatTitle(lang, peer as ApiChat);
}, [isUnknownUser, lang, peer]);
const handleTitleClick = useLastCallback((e) => { const handleTitleClick = useLastCallback((e) => {
if (!title || !canCopyTitle) { if (!title || !canCopyTitle) {
return; return;
@ -85,20 +77,24 @@ const FullNameTitle: FC<OwnProps> = ({
}); });
const specialTitle = useMemo(() => { const specialTitle = useMemo(() => {
if (customPeer) {
return lang(customPeer.titleKey);
}
if (isSavedMessages) { if (isSavedMessages) {
return lang(isSavedDialog ? 'MyNotes' : 'SavedMessages'); return lang(isSavedDialog ? 'MyNotes' : 'SavedMessages');
} }
if (peer && isAnonymousForwardsChat(peer.id)) { if (isAnonymousForwardsChat(realPeer!.id)) {
return lang('AnonymousForward'); return lang('AnonymousForward');
} }
if (peer && isChatWithRepliesBot(peer.id)) { if (isChatWithRepliesBot(realPeer!.id)) {
return lang('RepliesTitle'); return lang('RepliesTitle');
} }
return undefined; return undefined;
}, [isSavedDialog, isSavedMessages, lang, peer]); }, [customPeer, isSavedDialog, isSavedMessages, lang, realPeer]);
if (specialTitle) { if (specialTitle) {
return ( return (
@ -120,18 +116,18 @@ const FullNameTitle: FC<OwnProps> = ({
</h3> </h3>
{!iconElement && peer && ( {!iconElement && peer && (
<> <>
{!noVerified && peer?.isVerified && <VerifiedIcon />} {!noVerified && realPeer?.isVerified && <VerifiedIcon />}
{!noFake && peer?.fakeType && <FakeIcon fakeType={peer.fakeType} />} {!noFake && realPeer?.fakeType && <FakeIcon fakeType={realPeer.fakeType} />}
{withEmojiStatus && peer.emojiStatus && ( {withEmojiStatus && realPeer?.emojiStatus && (
<CustomEmoji <CustomEmoji
documentId={peer.emojiStatus.documentId} documentId={realPeer.emojiStatus.documentId}
size={emojiStatusSize} size={emojiStatusSize}
loopLimit={!noLoopLimit ? EMOJI_STATUS_LOOP_LIMIT : undefined} loopLimit={!noLoopLimit ? EMOJI_STATUS_LOOP_LIMIT : undefined}
observeIntersectionForLoading={observeIntersection} observeIntersectionForLoading={observeIntersection}
onClick={onEmojiStatusClick} onClick={onEmojiStatusClick}
/> />
)} )}
{withEmojiStatus && !peer.emojiStatus && isPremium && <PremiumIcon />} {withEmojiStatus && !realPeer?.emojiStatus && isPremium && <PremiumIcon />}
</> </>
)} )}
{iconElement} {iconElement}

View File

@ -29,6 +29,18 @@
} }
} }
.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 { .picker-list {
flex-grow: 1; flex-grow: 1;
overflow-y: auto; overflow-y: auto;

View File

@ -1,9 +1,10 @@
import type { FC } from '../../lib/teact/teact'; import type { FC } from '../../lib/teact/teact';
import React, { import React, {
memo, useEffect, useMemo, useRef, memo, useCallback, useEffect, useMemo, useRef,
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import type { ApiCountry } from '../../api/types'; import type { ApiCountry } from '../../api/types';
import type { CustomPeer, CustomPeerType } from '../../types';
import { requestMeasure } from '../../lib/fasterdom/fasterdom'; import { requestMeasure } from '../../lib/fasterdom/fasterdom';
import { isUserId } from '../../global/helpers'; import { isUserId } from '../../global/helpers';
@ -27,7 +28,9 @@ import './Picker.scss';
type OwnProps = { type OwnProps = {
className?: string; className?: string;
categories?: CustomPeer[];
itemIds: string[]; itemIds: string[];
selectedCategories?: CustomPeerType[];
selectedIds: string[]; selectedIds: string[];
lockedSelectedIds?: string[]; lockedSelectedIds?: string[];
lockedUnselectedIds?: string[]; lockedUnselectedIds?: string[];
@ -42,6 +45,7 @@ type OwnProps = {
isRoundCheckbox?: boolean; isRoundCheckbox?: boolean;
forceShowSelf?: boolean; forceShowSelf?: boolean;
isViewOnly?: boolean; isViewOnly?: boolean;
onSelectedCategoriesChange?: (categories: CustomPeerType[]) => void;
onSelectedIdsChange?: (ids: string[]) => void; onSelectedIdsChange?: (ids: string[]) => void;
onFilterChange?: (value: string) => void; onFilterChange?: (value: string) => void;
onDisabledClick?: (id: string, isSelected: boolean) => void; onDisabledClick?: (id: string, isSelected: boolean) => void;
@ -58,7 +62,9 @@ const ALWAYS_FULL_ITEMS_COUNT = 5;
const Picker: FC<OwnProps> = ({ const Picker: FC<OwnProps> = ({
className, className,
categories,
itemIds, itemIds,
selectedCategories,
selectedIds, selectedIds,
filterValue, filterValue,
filterPlaceholder, filterPlaceholder,
@ -73,6 +79,7 @@ const Picker: FC<OwnProps> = ({
lockedUnselectedSubtitle, lockedUnselectedSubtitle,
forceShowSelf, forceShowSelf,
isViewOnly, isViewOnly,
onSelectedCategoriesChange,
onSelectedIdsChange, onSelectedIdsChange,
onFilterChange, onFilterChange,
onDisabledClick, onDisabledClick,
@ -100,7 +107,16 @@ const Picker: FC<OwnProps> = ({
return selectedIds.filter((id) => !lockedSelectedIdsSet.has(id)); return selectedIds.filter((id) => !lockedSelectedIdsSet.has(id));
}, [lockedSelectedIdsSet, selectedIds]); }, [lockedSelectedIdsSet, selectedIds]);
const categoriesByType = useMemo(() => {
if (!categories) return {};
return buildCollectionByKey(categories, 'type');
}, [categories]);
const sortedItemIds = useMemo(() => { const sortedItemIds = useMemo(() => {
if (filterValue) {
return itemIds;
}
const lockedSelectedBucket: string[] = []; const lockedSelectedBucket: string[] = [];
const unlockedBucket: string[] = []; const unlockedBucket: string[] = [];
const lockedUnselectableBucket: string[] = []; const lockedUnselectableBucket: string[] = [];
@ -115,8 +131,8 @@ const Picker: FC<OwnProps> = ({
} }
}); });
return lockedSelectedBucket.concat(unlockedBucket).concat(lockedUnselectableBucket); return lockedSelectedBucket.concat(unlockedBucket, lockedUnselectableBucket);
}, [itemIds, lockedSelectedIdsSet, lockedUnselectedIdsSet]); }, [filterValue, itemIds, lockedSelectedIdsSet, lockedUnselectedIdsSet]);
const handleItemClick = useLastCallback((id: string) => { const handleItemClick = useLastCallback((id: string) => {
if (lockedSelectedIdsSet.has(id)) { if (lockedSelectedIdsSet.has(id)) {
@ -129,13 +145,24 @@ const Picker: FC<OwnProps> = ({
return; return;
} }
const newSelectedIds = selectedIds.slice(); if (categoriesByType[id]) {
if (newSelectedIds.includes(id)) { const categoryType = categoriesByType[id].type;
newSelectedIds.splice(newSelectedIds.indexOf(id), 1); const newSelectedCategories = selectedCategories?.slice() || [];
if (newSelectedCategories.includes(categoryType)) {
newSelectedCategories.splice(newSelectedCategories.indexOf(categoryType), 1);
} else {
newSelectedCategories.push(categoryType);
}
onSelectedCategoriesChange?.(newSelectedCategories);
} else { } else {
newSelectedIds.push(id); const newSelectedIds = selectedIds.slice();
if (newSelectedIds.includes(id)) {
newSelectedIds.splice(newSelectedIds.indexOf(id), 1);
} else {
newSelectedIds.push(id);
}
onSelectedIdsChange?.(newSelectedIds);
} }
onSelectedIdsChange?.(newSelectedIds);
onFilterChange?.(''); onFilterChange?.('');
}); });
@ -153,7 +180,15 @@ const Picker: FC<OwnProps> = ({
return buildCollectionByKey(countryList, 'iso2'); return buildCollectionByKey(countryList, 'iso2');
}, [countryList]); }, [countryList]);
const renderChatInfo = (id: string) => { const renderCategory = useLastCallback((category: CustomPeer) => {
return (
<PrivateChatInfo
customPeer={category}
/>
);
});
const renderChatInfo = useLastCallback((id: string) => {
const isUnselectable = lockedUnselectedIdsSet.has(id); const isUnselectable = lockedUnselectedIdsSet.has(id);
if (isCountryList && countriesByIso) { if (isCountryList && countriesByIso) {
const country = countriesByIso[id]; const country = countriesByIso[id];
@ -169,12 +204,69 @@ const Picker: FC<OwnProps> = ({
} else { } else {
return <GroupChatInfo chatId={id} status={isUnselectable ? lockedUnselectedSubtitle : undefined} />; 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) && (
<div className="picker-category-title">{lang('PrivacyUserTypes')}</div>
)}
{categories?.map((category) => renderItem(category.type, true))}
<div className="picker-category-title">{lang('FilterChats')}</div>
</div>
);
}, [categories, lang, renderItem]);
return ( return (
<div className={buildClassName('Picker', className)}> <div className={buildClassName('Picker', className)}>
{isSearchable && ( {isSearchable && (
<div className="picker-header custom-scroll" dir={lang.isRtl ? 'rtl' : undefined}> <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) => ( {lockedSelectedIds?.map((id, i) => (
<PickerSelectedItem <PickerSelectedItem
peerId={id} peerId={id}
@ -209,40 +301,11 @@ const Picker: FC<OwnProps> = ({
<InfiniteScroll <InfiniteScroll
className={buildClassName('picker-list', 'custom-scroll', isRoundCheckbox && 'withRoundedCheckbox')} className={buildClassName('picker-list', 'custom-scroll', isRoundCheckbox && 'withRoundedCheckbox')}
items={viewportIds} items={viewportIds}
beforeChildren={beforeChildren}
onLoadMore={getMore} onLoadMore={getMore}
noScrollRestore={noScrollRestore} noScrollRestore={noScrollRestore}
> >
{viewportIds.map((id) => { {viewportIds.map((id) => renderItem(id))}
const shouldRenderLockIcon = lockedUnselectedIdsSet.has(id);
const isLocked = lockedSelectedIdsSet.has(id) || shouldRenderLockIcon;
const renderCheckbox = () => {
return (isViewOnly || shouldRenderLockIcon) ? undefined : (
<Checkbox
label=""
disabled={isLocked}
checked={selectedIds.includes(id)}
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}
{renderChatInfo(id)}
{isRoundCheckbox ? renderCheckbox() : undefined}
</ListItem>
);
})}
</InfiniteScroll> </InfiniteScroll>
) : !isLoading && viewportIds && !viewportIds.length ? ( ) : !isLoading && viewportIds && !viewportIds.length ? (
<p className="no-results">{notFoundText || 'Sorry, nothing found.'}</p> <p className="no-results">{notFoundText || 'Sorry, nothing found.'}</p>

View File

@ -125,7 +125,7 @@
} }
&.forum-avatar { &.square-avatar {
border-start-start-radius: 0.625rem; border-start-start-radius: 0.625rem;
border-end-start-radius: 0.625rem; border-end-start-radius: 0.625rem;
--border-radius-forum-avatar: 0.625rem; --border-radius-forum-avatar: 0.625rem;

View File

@ -3,6 +3,7 @@ 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 { IconName } from '../../types/icons'; import type { IconName } from '../../types/icons';
import { getChatTitle, getUserFirstOrLastName } from '../../global/helpers'; import { getChatTitle, getUserFirstOrLastName } from '../../global/helpers';
@ -14,11 +15,13 @@ import renderText from './helpers/renderText';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import Avatar from './Avatar'; import Avatar from './Avatar';
import Icon from './Icon';
import './PickerSelectedItem.scss'; import './PickerSelectedItem.scss';
type OwnProps = { type OwnProps = {
peerId?: string; peerId?: string;
customPeer?: CustomPeer;
icon?: IconName; icon?: IconName;
title?: string; title?: string;
isMinimized?: boolean; isMinimized?: boolean;
@ -45,6 +48,7 @@ const PickerSelectedItem: FC<OwnProps & StateProps> = ({
clickArg, clickArg,
chat, chat,
user, user,
customPeer,
className, className,
fluid, fluid,
isSavedMessages, isSavedMessages,
@ -59,23 +63,24 @@ const PickerSelectedItem: FC<OwnProps & StateProps> = ({
if (icon && title) { if (icon && title) {
iconElement = ( iconElement = (
<div className="item-icon"> <div className="item-icon">
<i className={buildClassName('icon', `icon-${icon}`)} /> <Icon name={icon} />
</div> </div>
); );
titleText = title; titleText = title;
} else if (user || chat) { } else if (customPeer || user || chat) {
iconElement = ( iconElement = (
<Avatar <Avatar
peer={user || chat} peer={customPeer || user || chat}
size="small" size="small"
isSavedMessages={isSavedMessages} isSavedMessages={isSavedMessages}
/> />
); );
const name = !chat || (user && !isSavedMessages) const name = (customPeer && lang(customPeer.titleKey))
? getUserFirstOrLastName(user) || (!chat || (user && !isSavedMessages)
: getChatTitle(lang, chat, isSavedMessages); ? getUserFirstOrLastName(user)
: getChatTitle(lang, chat, isSavedMessages));
titleText = name ? renderText(name) : undefined; titleText = name ? renderText(name) : undefined;
} }
@ -83,11 +88,11 @@ const PickerSelectedItem: FC<OwnProps & StateProps> = ({
const fullClassName = buildClassName( const fullClassName = buildClassName(
'PickerSelectedItem', 'PickerSelectedItem',
className, className,
chat?.isForum && 'forum-avatar', (chat?.isForum || customPeer?.isAvatarSquare) && 'square-avatar',
isMinimized && 'minimized', isMinimized && 'minimized',
canClose && 'closeable', canClose && 'closeable',
fluid && 'fluid', fluid && 'fluid',
withPeerColors && getPeerColorClass(chat || user), withPeerColors && getPeerColorClass(customPeer || chat || user),
); );
return ( return (
@ -105,7 +110,7 @@ const PickerSelectedItem: FC<OwnProps & StateProps> = ({
)} )}
{canClose && ( {canClose && (
<div className="item-remove"> <div className="item-remove">
<i className="icon icon-close" /> <Icon name="close" />
</div> </div>
)} )}
</div> </div>

View File

@ -5,7 +5,7 @@ import { getActions, withGlobal } from '../../global';
import type { import type {
ApiChatMember, ApiTypingStatus, ApiUser, ApiUserStatus, ApiChatMember, ApiTypingStatus, ApiUser, ApiUserStatus,
} from '../../api/types'; } from '../../api/types';
import type { StoryViewerOrigin } from '../../types'; import type { CustomPeer, StoryViewerOrigin } from '../../types';
import type { IconName } from '../../types/icons'; import type { IconName } from '../../types/icons';
import { MediaViewerOrigin } from '../../types'; import { MediaViewerOrigin } from '../../types';
@ -27,7 +27,8 @@ import Icon from './Icon';
import TypingStatus from './TypingStatus'; import TypingStatus from './TypingStatus';
type OwnProps = { type OwnProps = {
userId: string; userId?: string;
customPeer?: CustomPeer;
typingStatus?: ApiTypingStatus; typingStatus?: ApiTypingStatus;
avatarSize?: 'tiny' | 'small' | 'medium' | 'large' | 'jumbo'; avatarSize?: 'tiny' | 'small' | 'medium' | 'large' | 'jumbo';
forceShowSelf?: boolean; forceShowSelf?: boolean;
@ -38,7 +39,6 @@ type OwnProps = {
withMediaViewer?: boolean; withMediaViewer?: boolean;
withUsername?: boolean; withUsername?: boolean;
withStory?: boolean; withStory?: boolean;
isUnknownUser?: boolean;
withFullInfo?: boolean; withFullInfo?: boolean;
withUpdatingStatus?: boolean; withUpdatingStatus?: boolean;
storyViewerOrigin?: StoryViewerOrigin; storyViewerOrigin?: StoryViewerOrigin;
@ -67,6 +67,7 @@ type StateProps =
}; };
const PrivateChatInfo: FC<OwnProps & StateProps> = ({ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
customPeer,
typingStatus, typingStatus,
avatarSize = 'medium', avatarSize = 'medium',
status, status,
@ -82,7 +83,6 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
noEmojiStatus, noEmojiStatus,
noFake, noFake,
noVerified, noVerified,
isUnknownUser,
noRtl, noRtl,
user, user,
userStatus, userStatus,
@ -131,7 +131,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
const mainUsername = useMemo(() => user && withUsername && getMainUsername(user), [user, withUsername]); const mainUsername = useMemo(() => user && withUsername && getMainUsername(user), [user, withUsername]);
if (!user && !isUnknownUser) { if (!user && !customPeer) {
return undefined; return undefined;
} }
@ -153,6 +153,14 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
); );
} }
if (customPeer?.subtitleKey) {
return (
<span className="status" dir="auto">
<span className="user-status" dir="auto">{lang(customPeer.subtitleKey)}</span>
</span>
);
}
if (!user) { if (!user) {
return undefined; return undefined;
} }
@ -194,7 +202,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
return ( return (
<FullNameTitle <FullNameTitle
peer={user!} peer={customPeer || user!}
noFake={noFake} noFake={noFake}
noVerified={noVerified} noVerified={noVerified}
withEmojiStatus={!noEmojiStatus} withEmojiStatus={!noEmojiStatus}
@ -202,7 +210,6 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
isSavedMessages={isSavedMessages} isSavedMessages={isSavedMessages}
isSavedDialog={isSavedDialog} isSavedDialog={isSavedDialog}
onEmojiStatusClick={onEmojiStatusClick} onEmojiStatusClick={onEmojiStatusClick}
isUnknownUser={isUnknownUser}
iconElement={iconElement} iconElement={iconElement}
/> />
); );
@ -220,12 +227,11 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
/> />
)} )}
<Avatar <Avatar
key={user?.id} key={customPeer?.type || user?.id}
size={avatarSize} size={avatarSize}
peer={user} peer={customPeer || user}
className={buildClassName(isSavedDialog && 'overlay-avatar')} className={buildClassName(isSavedDialog && 'overlay-avatar')}
isSavedMessages={isSavedMessages} isSavedMessages={isSavedMessages}
isUnknownUser={isUnknownUser}
isSavedDialog={isSavedDialog} isSavedDialog={isSavedDialog}
withStory={withStory} withStory={withStory}
storyViewerOrigin={storyViewerOrigin} storyViewerOrigin={storyViewerOrigin}
@ -245,11 +251,11 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global, { userId, forceShowSelf }): StateProps => { (global, { userId, forceShowSelf }): StateProps => {
const { isSynced } = global; const { isSynced } = global;
const user = selectUser(global, userId); const user = userId ? selectUser(global, userId) : undefined;
const userStatus = selectUserStatus(global, userId); const userStatus = userId ? selectUserStatus(global, userId) : undefined;
const isSavedMessages = !forceShowSelf && user && user.isSelf; const isSavedMessages = !forceShowSelf && user && user.isSelf;
const self = isSavedMessages ? user : selectUser(global, global.currentUserId!); const self = isSavedMessages ? user : selectUser(global, global.currentUserId!);
const areMessagesLoaded = Boolean(selectChatMessages(global, userId)); const areMessagesLoaded = Boolean(userId && selectChatMessages(global, userId));
return { return {
user, user,

View File

@ -1,12 +1,18 @@
import type { ApiPeer, ApiPeerColor } from '../../../api/types'; import type { ApiPeer, ApiPeerColor } from '../../../api/types';
import type { CustomPeer } from '../../../types';
import { getPeerColorCount, getPeerColorKey } from '../../../global/helpers'; import { getPeerColorCount, getPeerColorKey } from '../../../global/helpers';
export function getPeerColorClass(peer?: ApiPeer, noUserColors?: boolean, shouldReset?: boolean) { export function getPeerColorClass(peer?: ApiPeer | CustomPeer, noUserColors?: boolean, shouldReset?: boolean) {
if (!peer) { if (!peer) {
if (!shouldReset) return undefined; if (!shouldReset) return undefined;
return noUserColors ? 'peer-color-count-1' : 'peer-color-0'; return noUserColors ? 'peer-color-count-1' : 'peer-color-0';
} }
if ('isCustomPeer' in peer) {
if (!peer.peerColorId) return undefined;
return `peer-color-${peer.peerColorId}`;
}
return noUserColors ? `peer-color-count-${getPeerColorCount(peer)}` : `peer-color-${getPeerColorKey(peer)}`; return noUserColors ? `peer-color-count-${getPeerColorCount(peer)}` : `peer-color-${getPeerColorKey(peer)}`;
} }

View File

@ -356,6 +356,7 @@ const Settings: FC<OwnProps> = ({
return ( return (
<SettingsPrivacyVisibilityExceptionList <SettingsPrivacyVisibilityExceptionList
isAllowList isAllowList
withPremiumCategory={currentScreen === SettingsScreens.PrivacyGroupChatsAllowedContacts}
screen={currentScreen} screen={currentScreen}
onScreenSelect={onScreenSelect} onScreenSelect={onScreenSelect}
isActive={isScreenActive || privacyAllowScreens[currentScreen]} isActive={isScreenActive || privacyAllowScreens[currentScreen]}

View File

@ -103,7 +103,7 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
}, [updateContentSettings]); }, [updateContentSettings]);
function getVisibilityValue(setting?: ApiPrivacySettings) { function getVisibilityValue(setting?: ApiPrivacySettings) {
const { visibility } = setting || {}; const { visibility, shouldAllowPremium } = setting || {};
const blockCount = setting ? setting.blockChatIds.length + setting.blockUserIds.length : 0; const blockCount = setting ? setting.blockChatIds.length + setting.blockUserIds.length : 0;
const allowCount = setting ? setting.allowChatIds.length + setting.allowUserIds.length : 0; const allowCount = setting ? setting.allowChatIds.length + setting.allowUserIds.length : 0;
const total = []; const total = [];
@ -112,6 +112,10 @@ const SettingsPrivacy: FC<OwnProps & StateProps> = ({
const exceptionString = total.length ? `(${total.join(',')})` : ''; const exceptionString = total.length ? `(${total.join(',')})` : '';
if (shouldAllowPremium) {
return lang(exceptionString ? 'ContactsAndPremium' : 'PrivacyPremium');
}
switch (visibility) { switch (visibility) {
case 'everybody': case 'everybody':
return `${lang('P2PEverybody')} ${exceptionString}`; return `${lang('P2PEverybody')} ${exceptionString}`;

View File

@ -196,12 +196,14 @@ function PrivacySubsection({
} }
}, [lang, screen]); }, [lang, screen]);
const prepareSubtitle = useLastCallback((userIds?: string[], chatIds?: string[]) => { const prepareSubtitle = useLastCallback((userIds?: string[], chatIds?: string[], shouldAllowPremium?: boolean) => {
const userIdsCount = userIds?.length || 0; const userIdsCount = userIds?.length || 0;
const chatIdsCount = chatIds?.length || 0; const chatIdsCount = chatIds?.length || 0;
if (!userIdsCount && !chatIdsCount) { if (!userIdsCount && !chatIdsCount) {
return lang('EditAdminAddUsers'); return shouldAllowPremium ? lang('PrivacyPremium') : lang('EditAdminAddUsers');
} else if (shouldAllowPremium) {
return lang('ContactsAndPremium');
} }
const userCountString = userIdsCount > 0 ? lang('Users', userIdsCount) : undefined; const userCountString = userIdsCount > 0 ? lang('Users', userIdsCount) : undefined;
@ -211,7 +213,7 @@ function PrivacySubsection({
}); });
const allowedString = useMemo(() => { const allowedString = useMemo(() => {
return prepareSubtitle(privacy?.allowUserIds, privacy?.allowChatIds); return prepareSubtitle(privacy?.allowUserIds, privacy?.allowChatIds, privacy?.shouldAllowPremium);
}, [privacy]); }, [privacy]);
const blockString = useMemo(() => { const blockString = useMemo(() => {

View File

@ -5,12 +5,15 @@ import React, {
import { getActions, getGlobal, withGlobal } from '../../../global'; import { getActions, getGlobal, withGlobal } from '../../../global';
import type { GlobalState } from '../../../global/types'; import type { GlobalState } from '../../../global/types';
import type { ApiPrivacySettings } from '../../../types'; import type { ApiPrivacySettings, CustomPeerType } from '../../../types';
import { SettingsScreens } from '../../../types'; import { SettingsScreens } from '../../../types';
import { ALL_FOLDER_ID, ARCHIVED_FOLDER_ID } from '../../../config'; import { ALL_FOLDER_ID, ARCHIVED_FOLDER_ID, SERVICE_NOTIFICATIONS_USER_ID } from '../../../config';
import { filterChatsByName } from '../../../global/helpers'; import {
filterChatsByName, isChatChannel, isDeletedUser,
} from '../../../global/helpers';
import { unique } from '../../../util/iteratees'; import { unique } from '../../../util/iteratees';
import { CUSTOM_PEER_PREMIUM } from '../../../util/objects/customPeer';
import { getPrivacyKey } from './helpers/privacy'; import { getPrivacyKey } from './helpers/privacy';
import { useFolderManagerForOrderedIds } from '../../../hooks/useFolderManager'; import { useFolderManagerForOrderedIds } from '../../../hooks/useFolderManager';
@ -22,6 +25,7 @@ import FloatingActionButton from '../../ui/FloatingActionButton';
export type OwnProps = { export type OwnProps = {
isAllowList?: boolean; isAllowList?: boolean;
withPremiumCategory?: boolean;
screen: SettingsScreens; screen: SettingsScreens;
isActive?: boolean; isActive?: boolean;
onScreenSelect: (screen: SettingsScreens) => void; onScreenSelect: (screen: SettingsScreens) => void;
@ -33,8 +37,11 @@ type StateProps = {
settings?: ApiPrivacySettings; settings?: ApiPrivacySettings;
}; };
const PREMIUM_CATEGORY = [CUSTOM_PEER_PREMIUM];
const SettingsPrivacyVisibilityExceptionList: FC<OwnProps & StateProps> = ({ const SettingsPrivacyVisibilityExceptionList: FC<OwnProps & StateProps> = ({
isAllowList, isAllowList,
withPremiumCategory,
screen, screen,
isActive, isActive,
currentUserId, currentUserId,
@ -57,23 +64,46 @@ const SettingsPrivacyVisibilityExceptionList: FC<OwnProps & StateProps> = ({
return [...settings.blockUserIds, ...settings.blockChatIds]; return [...settings.blockUserIds, ...settings.blockChatIds];
} }
}, [isAllowList, settings]); }, [isAllowList, settings]);
const selectedCategoryTypes = useMemo(() => {
if (!settings) {
return [];
}
return [settings.shouldAllowPremium ? CUSTOM_PEER_PREMIUM.type : undefined].filter(Boolean);
}, [settings]);
const [searchQuery, setSearchQuery] = useState<string>(''); const [searchQuery, setSearchQuery] = useState<string>('');
const [isSubmitShown, setIsSubmitShown] = useState<boolean>(false); const [isSubmitShown, setIsSubmitShown] = useState<boolean>(false);
const [newSelectedContactIds, setNewSelectedContactIds] = useState<string[]>(selectedContactIds); const [newSelectedContactIds, setNewSelectedContactIds] = useState<string[]>(selectedContactIds);
const [newSelectedCategoryTypes, setNewSelectedCategoryTypes] = useState<CustomPeerType[]>(selectedCategoryTypes);
// Reset selected contact ids on change from other client when screen is not active // Reset selected contact ids on change from other client when screen is not active
useEffect(() => { useEffect(() => {
if (!isActive) setNewSelectedContactIds(selectedContactIds); if (!isActive) {
}, [isActive, selectedContactIds]); setNewSelectedContactIds(selectedContactIds);
setNewSelectedCategoryTypes(selectedCategoryTypes);
}
}, [isActive, selectedCategoryTypes, selectedContactIds]);
const folderAllOrderedIds = useFolderManagerForOrderedIds(ALL_FOLDER_ID); const folderAllOrderedIds = useFolderManagerForOrderedIds(ALL_FOLDER_ID);
const folderArchivedOrderedIds = useFolderManagerForOrderedIds(ARCHIVED_FOLDER_ID); const folderArchivedOrderedIds = useFolderManagerForOrderedIds(ARCHIVED_FOLDER_ID);
const displayedIds = useMemo(() => { const displayedIds = useMemo(() => {
// No need for expensive global updates on chats, so we avoid them // No need for expensive global updates on chats, so we avoid them
const chatsById = getGlobal().chats.byId; const chatsById = getGlobal().chats.byId;
const usersById = getGlobal().users.byId;
const chatIds = unique([...folderAllOrderedIds || [], ...folderArchivedOrderedIds || []]) const chatIds = unique([...folderAllOrderedIds || [], ...folderArchivedOrderedIds || []])
.filter((chatId) => chatId !== currentUserId); .filter((chatId) => {
const chat = chatsById[chatId];
const user = usersById[chatId];
const isDeleted = user && isDeletedUser(user);
const isChannel = chat && isChatChannel(chat);
return chatId !== currentUserId && chatId !== SERVICE_NOTIFICATIONS_USER_ID && !isChannel && !isDeleted;
});
const filteredChats = filterChatsByName(lang, chatIds, chatsById, searchQuery);
// Show only relevant items
if (searchQuery) return filteredChats;
return unique([ return unique([
...selectedContactIds, ...selectedContactIds,
@ -81,6 +111,11 @@ const SettingsPrivacyVisibilityExceptionList: FC<OwnProps & StateProps> = ({
]); ]);
}, [folderAllOrderedIds, folderArchivedOrderedIds, selectedContactIds, lang, searchQuery, currentUserId]); }, [folderAllOrderedIds, folderArchivedOrderedIds, selectedContactIds, lang, searchQuery, currentUserId]);
const handleSelectedCategoriesChange = useCallback((value: CustomPeerType[]) => {
setNewSelectedCategoryTypes(value);
setIsSubmitShown(true);
}, []);
const handleSelectedContactIdsChange = useCallback((value: string[]) => { const handleSelectedContactIdsChange = useCallback((value: string[]) => {
setNewSelectedContactIds(value); setNewSelectedContactIds(value);
setIsSubmitShown(true); setIsSubmitShown(true);
@ -91,10 +126,11 @@ const SettingsPrivacyVisibilityExceptionList: FC<OwnProps & StateProps> = ({
privacyKey: getPrivacyKey(screen)!, privacyKey: getPrivacyKey(screen)!,
isAllowList: Boolean(isAllowList), isAllowList: Boolean(isAllowList),
updatedIds: newSelectedContactIds, updatedIds: newSelectedContactIds,
isPremiumAllowed: newSelectedCategoryTypes.includes(CUSTOM_PEER_PREMIUM.type) || undefined,
}); });
onScreenSelect(SettingsScreens.Privacy); onScreenSelect(SettingsScreens.Privacy);
}, [isAllowList, newSelectedContactIds, onScreenSelect, screen, setPrivacySettings]); }, [isAllowList, newSelectedCategoryTypes, newSelectedContactIds, onScreenSelect, screen]);
useHistoryBack({ useHistoryBack({
isActive, isActive,
@ -104,13 +140,16 @@ const SettingsPrivacyVisibilityExceptionList: FC<OwnProps & StateProps> = ({
return ( return (
<div className="NewChat-inner step-1"> <div className="NewChat-inner step-1">
<Picker <Picker
categories={withPremiumCategory ? PREMIUM_CATEGORY : undefined}
itemIds={displayedIds || []} itemIds={displayedIds || []}
selectedIds={newSelectedContactIds} selectedIds={newSelectedContactIds}
selectedCategories={newSelectedCategoryTypes}
filterValue={searchQuery} filterValue={searchQuery}
filterPlaceholder={isAllowList ? lang('AlwaysAllowPlaceholder') : lang('NeverAllowPlaceholder')} filterPlaceholder={isAllowList ? lang('AlwaysAllowPlaceholder') : lang('NeverAllowPlaceholder')}
searchInputId="new-group-picker-search" searchInputId="new-group-picker-search"
isSearchable isSearchable
onSelectedIdsChange={handleSelectedContactIdsChange} onSelectedIdsChange={handleSelectedContactIdsChange}
onSelectedCategoriesChange={handleSelectedCategoriesChange}
onFilterChange={setSearchQuery} onFilterChange={setSearchQuery}
/> />

View File

@ -17,6 +17,7 @@ import {
} from '../../../global/selectors'; } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { formatDateAtTime } from '../../../util/date/dateFormat'; import { formatDateAtTime } from '../../../util/date/dateFormat';
import { CUSTOM_PEER_TO_BE_DISTRIBUTED } from '../../../util/objects/customPeer';
import { getBoostProgressInfo } from '../../common/helpers/boostInfo'; import { getBoostProgressInfo } from '../../common/helpers/boostInfo';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
@ -197,12 +198,12 @@ const BoostStatistics = ({
<PrivateChatInfo <PrivateChatInfo
className={styles.user} className={styles.user}
userId={boost.userId} userId={boost.userId}
customPeer={!boost.userId ? CUSTOM_PEER_TO_BE_DISTRIBUTED : undefined}
status={lang('BoostExpireOn', formatDateAtTime(lang, boost.expires * 1000))} status={lang('BoostExpireOn', formatDateAtTime(lang, boost.expires * 1000))}
noEmojiStatus noEmojiStatus
forceShowSelf forceShowSelf
noFake noFake
noVerified noVerified
isUnknownUser={!boost.userId}
iconElement={boost.multiplier ? renderBoostIcon(boost.multiplier) : undefined} iconElement={boost.multiplier ? renderBoostIcon(boost.multiplier) : undefined}
rightElement={renderBoostTypeIcon(boost)} rightElement={renderBoostTypeIcon(boost)}
/> />

View File

@ -19,7 +19,7 @@ type OwnProps = {
value?: string; value?: string;
label: TeactNode; label: TeactNode;
subLabel?: string; subLabel?: string;
checked: boolean; checked?: boolean;
rightIcon?: IconName; rightIcon?: IconName;
disabled?: boolean; disabled?: boolean;
tabIndex?: number; tabIndex?: number;

View File

@ -533,7 +533,9 @@ addActionHandler('setPrivacyVisibility', async (global, actions, payload): Promi
}); });
addActionHandler('setPrivacySettings', async (global, actions, payload): Promise<void> => { addActionHandler('setPrivacySettings', async (global, actions, payload): Promise<void> => {
const { privacyKey, isAllowList, updatedIds } = payload!; const {
privacyKey, isAllowList, updatedIds, isPremiumAllowed,
} = payload!;
const { const {
privacy: { [privacyKey]: settings }, privacy: { [privacyKey]: settings },
} = global.settings; } = global.settings;
@ -545,7 +547,7 @@ addActionHandler('setPrivacySettings', async (global, actions, payload): Promise
const rules = buildApiInputPrivacyRules(global, { const rules = buildApiInputPrivacyRules(global, {
visibility: settings.visibility, visibility: settings.visibility,
isUnspecified: settings.isUnspecified, isUnspecified: settings.isUnspecified,
shouldAllowPremium: settings.shouldAllowPremium, shouldAllowPremium: isPremiumAllowed,
allowedIds: isAllowList ? updatedIds : [...settings.allowUserIds, ...settings.allowChatIds], allowedIds: isAllowList ? updatedIds : [...settings.allowUserIds, ...settings.allowChatIds],
blockedIds: !isAllowList ? updatedIds : [...settings.blockUserIds, ...settings.blockChatIds], blockedIds: !isAllowList ? updatedIds : [...settings.blockUserIds, ...settings.blockChatIds],
}); });

View File

@ -1238,6 +1238,7 @@ export interface ActionPayloads {
privacyKey: ApiPrivacyKey; privacyKey: ApiPrivacyKey;
isAllowList: boolean; isAllowList: boolean;
updatedIds: string[]; updatedIds: string[];
isPremiumAllowed?: true;
}; };
loadNotificationExceptions: undefined; loadNotificationExceptions: undefined;
setThemeSettings: { theme: ThemeKey } & Partial<IThemeSettings>; setThemeSettings: { theme: ThemeKey } & Partial<IThemeSettings>;

View File

@ -8,6 +8,7 @@ import type {
ApiExportedInvite, ApiExportedInvite,
ApiLanguage, ApiMessage, ApiReaction, ApiStickerSet, ApiUser, ApiLanguage, ApiMessage, ApiReaction, ApiStickerSet, ApiUser,
} from '../api/types'; } from '../api/types';
import type { IconName } from './icons';
export type TextPart = TeactNode; export type TextPart = TeactNode;
@ -457,3 +458,16 @@ export type InlineBotSettings = {
switchWebview?: ApiBotInlineSwitchWebview; switchWebview?: ApiBotInlineSwitchWebview;
cacheTime: number; cacheTime: number;
}; };
export type CustomPeerType = 'premium' | 'toBeDistributed';
export interface CustomPeer {
type: CustomPeerType;
isCustomPeer: true;
titleKey: string;
subtitleKey?: string;
avatarIcon: IconName;
isAvatarSquare?: boolean;
peerColorId?: number;
withPremiumGradient?: boolean;
}

View File

@ -0,0 +1,19 @@
import type { CustomPeer } from '../../types';
export const CUSTOM_PEER_PREMIUM: CustomPeer = {
isCustomPeer: true,
type: 'premium',
titleKey: 'PrivacyPremium',
subtitleKey: 'PrivacyPremiumText',
avatarIcon: 'premium',
isAvatarSquare: true,
withPremiumGradient: true,
};
export const CUSTOM_PEER_TO_BE_DISTRIBUTED: CustomPeer = {
isCustomPeer: true,
type: 'toBeDistributed',
titleKey: 'BoostingToBeDistributed',
avatarIcon: 'user',
withPremiumGradient: true,
};