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

View File

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

View File

@ -2,8 +2,11 @@ import type { FC } from '../../lib/teact/teact';
import React, { memo, useMemo } from '../../lib/teact/teact';
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 { CustomPeer } from '../../types';
import { EMOJI_STATUS_LOOP_LIMIT } from '../../config';
import {
@ -25,7 +28,7 @@ import VerifiedIcon from './VerifiedIcon';
import styles from './FullNameTitle.module.scss';
type OwnProps = {
peer?: ApiPeer;
peer: ApiPeer | CustomPeer;
className?: string;
noVerified?: boolean;
noFake?: boolean;
@ -34,7 +37,6 @@ type OwnProps = {
isSavedMessages?: boolean;
isSavedDialog?: boolean;
noLoopLimit?: boolean;
isUnknownUser?: boolean;
canCopyTitle?: boolean;
onEmojiStatusClick?: NoneToVoidFunction;
observeIntersection?: ObserveFn;
@ -55,25 +57,15 @@ const FullNameTitle: FC<OwnProps> = ({
onEmojiStatusClick,
observeIntersection,
iconElement,
isUnknownUser,
}) => {
const lang = useLang();
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 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) => {
if (!title || !canCopyTitle) {
return;
@ -85,20 +77,24 @@ const FullNameTitle: FC<OwnProps> = ({
});
const specialTitle = useMemo(() => {
if (customPeer) {
return lang(customPeer.titleKey);
}
if (isSavedMessages) {
return lang(isSavedDialog ? 'MyNotes' : 'SavedMessages');
}
if (peer && isAnonymousForwardsChat(peer.id)) {
if (isAnonymousForwardsChat(realPeer!.id)) {
return lang('AnonymousForward');
}
if (peer && isChatWithRepliesBot(peer.id)) {
if (isChatWithRepliesBot(realPeer!.id)) {
return lang('RepliesTitle');
}
return undefined;
}, [isSavedDialog, isSavedMessages, lang, peer]);
}, [customPeer, isSavedDialog, isSavedMessages, lang, realPeer]);
if (specialTitle) {
return (
@ -120,18 +116,18 @@ const FullNameTitle: FC<OwnProps> = ({
</h3>
{!iconElement && peer && (
<>
{!noVerified && peer?.isVerified && <VerifiedIcon />}
{!noFake && peer?.fakeType && <FakeIcon fakeType={peer.fakeType} />}
{withEmojiStatus && peer.emojiStatus && (
{!noVerified && realPeer?.isVerified && <VerifiedIcon />}
{!noFake && realPeer?.fakeType && <FakeIcon fakeType={realPeer.fakeType} />}
{withEmojiStatus && realPeer?.emojiStatus && (
<CustomEmoji
documentId={peer.emojiStatus.documentId}
documentId={realPeer.emojiStatus.documentId}
size={emojiStatusSize}
loopLimit={!noLoopLimit ? EMOJI_STATUS_LOOP_LIMIT : undefined}
observeIntersectionForLoading={observeIntersection}
onClick={onEmojiStatusClick}
/>
)}
{withEmojiStatus && !peer.emojiStatus && isPremium && <PremiumIcon />}
{withEmojiStatus && !realPeer?.emojiStatus && isPremium && <PremiumIcon />}
</>
)}
{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 {
flex-grow: 1;
overflow-y: auto;

View File

@ -1,9 +1,10 @@
import type { FC } from '../../lib/teact/teact';
import React, {
memo, useEffect, useMemo, useRef,
memo, useCallback, useEffect, useMemo, useRef,
} from '../../lib/teact/teact';
import type { ApiCountry } from '../../api/types';
import type { CustomPeer, CustomPeerType } from '../../types';
import { requestMeasure } from '../../lib/fasterdom/fasterdom';
import { isUserId } from '../../global/helpers';
@ -27,7 +28,9 @@ import './Picker.scss';
type OwnProps = {
className?: string;
categories?: CustomPeer[];
itemIds: string[];
selectedCategories?: CustomPeerType[];
selectedIds: string[];
lockedSelectedIds?: string[];
lockedUnselectedIds?: string[];
@ -42,6 +45,7 @@ type OwnProps = {
isRoundCheckbox?: boolean;
forceShowSelf?: boolean;
isViewOnly?: boolean;
onSelectedCategoriesChange?: (categories: CustomPeerType[]) => void;
onSelectedIdsChange?: (ids: string[]) => void;
onFilterChange?: (value: string) => void;
onDisabledClick?: (id: string, isSelected: boolean) => void;
@ -58,7 +62,9 @@ const ALWAYS_FULL_ITEMS_COUNT = 5;
const Picker: FC<OwnProps> = ({
className,
categories,
itemIds,
selectedCategories,
selectedIds,
filterValue,
filterPlaceholder,
@ -73,6 +79,7 @@ const Picker: FC<OwnProps> = ({
lockedUnselectedSubtitle,
forceShowSelf,
isViewOnly,
onSelectedCategoriesChange,
onSelectedIdsChange,
onFilterChange,
onDisabledClick,
@ -100,7 +107,16 @@ const Picker: FC<OwnProps> = ({
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[] = [];
@ -115,8 +131,8 @@ const Picker: FC<OwnProps> = ({
}
});
return lockedSelectedBucket.concat(unlockedBucket).concat(lockedUnselectableBucket);
}, [itemIds, lockedSelectedIdsSet, lockedUnselectedIdsSet]);
return lockedSelectedBucket.concat(unlockedBucket, lockedUnselectableBucket);
}, [filterValue, itemIds, lockedSelectedIdsSet, lockedUnselectedIdsSet]);
const handleItemClick = useLastCallback((id: string) => {
if (lockedSelectedIdsSet.has(id)) {
@ -129,13 +145,24 @@ const Picker: FC<OwnProps> = ({
return;
}
const newSelectedIds = selectedIds.slice();
if (newSelectedIds.includes(id)) {
newSelectedIds.splice(newSelectedIds.indexOf(id), 1);
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 {
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?.('');
});
@ -153,7 +180,15 @@ const Picker: FC<OwnProps> = ({
return buildCollectionByKey(countryList, 'iso2');
}, [countryList]);
const renderChatInfo = (id: string) => {
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];
@ -169,12 +204,69 @@ const Picker: FC<OwnProps> = ({
} 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) && (
<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 (
<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}
@ -209,40 +301,11 @@ const Picker: FC<OwnProps> = ({
<InfiniteScroll
className={buildClassName('picker-list', 'custom-scroll', isRoundCheckbox && 'withRoundedCheckbox')}
items={viewportIds}
beforeChildren={beforeChildren}
onLoadMore={getMore}
noScrollRestore={noScrollRestore}
>
{viewportIds.map((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>
);
})}
{viewportIds.map((id) => renderItem(id))}
</InfiniteScroll>
) : !isLoading && viewportIds && !viewportIds.length ? (
<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-end-start-radius: 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 type { ApiChat, ApiUser } from '../../api/types';
import type { CustomPeer } from '../../types';
import type { IconName } from '../../types/icons';
import { getChatTitle, getUserFirstOrLastName } from '../../global/helpers';
@ -14,11 +15,13 @@ import renderText from './helpers/renderText';
import useLang from '../../hooks/useLang';
import Avatar from './Avatar';
import Icon from './Icon';
import './PickerSelectedItem.scss';
type OwnProps = {
peerId?: string;
customPeer?: CustomPeer;
icon?: IconName;
title?: string;
isMinimized?: boolean;
@ -45,6 +48,7 @@ const PickerSelectedItem: FC<OwnProps & StateProps> = ({
clickArg,
chat,
user,
customPeer,
className,
fluid,
isSavedMessages,
@ -59,23 +63,24 @@ const PickerSelectedItem: FC<OwnProps & StateProps> = ({
if (icon && title) {
iconElement = (
<div className="item-icon">
<i className={buildClassName('icon', `icon-${icon}`)} />
<Icon name={icon} />
</div>
);
titleText = title;
} else if (user || chat) {
} else if (customPeer || user || chat) {
iconElement = (
<Avatar
peer={user || chat}
peer={customPeer || user || chat}
size="small"
isSavedMessages={isSavedMessages}
/>
);
const name = !chat || (user && !isSavedMessages)
? getUserFirstOrLastName(user)
: getChatTitle(lang, chat, isSavedMessages);
const name = (customPeer && lang(customPeer.titleKey))
|| (!chat || (user && !isSavedMessages)
? getUserFirstOrLastName(user)
: getChatTitle(lang, chat, isSavedMessages));
titleText = name ? renderText(name) : undefined;
}
@ -83,11 +88,11 @@ const PickerSelectedItem: FC<OwnProps & StateProps> = ({
const fullClassName = buildClassName(
'PickerSelectedItem',
className,
chat?.isForum && 'forum-avatar',
(chat?.isForum || customPeer?.isAvatarSquare) && 'square-avatar',
isMinimized && 'minimized',
canClose && 'closeable',
fluid && 'fluid',
withPeerColors && getPeerColorClass(chat || user),
withPeerColors && getPeerColorClass(customPeer || chat || user),
);
return (
@ -105,7 +110,7 @@ const PickerSelectedItem: FC<OwnProps & StateProps> = ({
)}
{canClose && (
<div className="item-remove">
<i className="icon icon-close" />
<Icon name="close" />
</div>
)}
</div>

View File

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

View File

@ -1,12 +1,18 @@
import type { ApiPeer, ApiPeerColor } from '../../../api/types';
import type { CustomPeer } from '../../../types';
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 (!shouldReset) return undefined;
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)}`;
}

View File

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

View File

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

View File

@ -196,12 +196,14 @@ function PrivacySubsection({
}
}, [lang, screen]);
const prepareSubtitle = useLastCallback((userIds?: string[], chatIds?: string[]) => {
const prepareSubtitle = useLastCallback((userIds?: string[], chatIds?: string[], shouldAllowPremium?: boolean) => {
const userIdsCount = userIds?.length || 0;
const chatIdsCount = chatIds?.length || 0;
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;
@ -211,7 +213,7 @@ function PrivacySubsection({
});
const allowedString = useMemo(() => {
return prepareSubtitle(privacy?.allowUserIds, privacy?.allowChatIds);
return prepareSubtitle(privacy?.allowUserIds, privacy?.allowChatIds, privacy?.shouldAllowPremium);
}, [privacy]);
const blockString = useMemo(() => {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,6 +8,7 @@ import type {
ApiExportedInvite,
ApiLanguage, ApiMessage, ApiReaction, ApiStickerSet, ApiUser,
} from '../api/types';
import type { IconName } from './icons';
export type TextPart = TeactNode;
@ -457,3 +458,16 @@ export type InlineBotSettings = {
switchWebview?: ApiBotInlineSwitchWebview;
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,
};