From cf8eaf270eed24db827eeb1adf0fbb090629b986 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Tue, 14 May 2024 04:23:26 +0200 Subject: [PATCH] Privacy Settings: Implement premium category (#4536) --- src/components/common/Avatar.scss | 5 +- src/components/common/Avatar.tsx | 48 +++--- src/components/common/FullNameTitle.tsx | 46 +++--- src/components/common/Picker.scss | 12 ++ src/components/common/Picker.tsx | 145 +++++++++++++----- src/components/common/PickerSelectedItem.scss | 2 +- src/components/common/PickerSelectedItem.tsx | 23 +-- src/components/common/PrivateChatInfo.tsx | 32 ++-- src/components/common/helpers/peerColor.ts | 8 +- src/components/left/settings/Settings.tsx | 1 + .../left/settings/SettingsPrivacy.tsx | 6 +- .../settings/SettingsPrivacyVisibility.tsx | 8 +- ...SettingsPrivacyVisibilityExceptionList.tsx | 53 ++++++- .../right/statistics/BoostStatistics.tsx | 3 +- src/components/ui/Checkbox.tsx | 2 +- src/global/actions/api/settings.ts | 6 +- src/global/types.ts | 1 + src/types/index.ts | 14 ++ src/util/objects/customPeer.ts | 19 +++ 19 files changed, 304 insertions(+), 130 deletions(-) create mode 100644 src/util/objects/customPeer.ts diff --git a/src/components/common/Avatar.scss b/src/components/common/Avatar.scss index 1ca79c71f..6c833d13c 100644 --- a/src/components/common/Avatar.scss +++ b/src/components/common/Avatar.scss @@ -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); } } diff --git a/src/components/common/Avatar.tsx b/src/components/common/Avatar.tsx index 53a77d162..35c9c5680 100644 --- a/src/components/common/Avatar.tsx +++ b/src/components/common/Avatar.tsx @@ -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 = ({ text, isSavedMessages, isSavedDialog, - isUnknownUser, withVideo, withStory, forPremiumPromo, @@ -97,12 +95,14 @@ const Avatar: FC = ({ // eslint-disable-next-line no-null/no-null const ref = useRef(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 = ({ 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 = ({ } const specialIcon = useMemo(() => { - if (isUnknownUser) { - return 'user'; + if (isCustomPeer) { + return peer.avatarIcon; } if (isSavedMessages) { @@ -143,7 +143,7 @@ const Avatar: FC = ({ } 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 = ({ 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 = ({ const hasMedia = Boolean(isSavedMessages || imgBlobUrl); const { handleClick, handleMouseDown } = useFastClick((e: ReactMouseEvent) => { - 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 = ({
= ({
{typeof content === 'string' ? renderText(content, [size === 'jumbo' ? 'hq_emoji' : 'emoji']) : content}
- {withStory && peer?.hasStories && ( - + {withStory && realPeer?.hasStories && ( + )}
); diff --git a/src/components/common/FullNameTitle.tsx b/src/components/common/FullNameTitle.tsx index 7f335590f..721ae701f 100644 --- a/src/components/common/FullNameTitle.tsx +++ b/src/components/common/FullNameTitle.tsx @@ -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 = ({ 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 = ({ }); 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 = ({ {!iconElement && peer && ( <> - {!noVerified && peer?.isVerified && } - {!noFake && peer?.fakeType && } - {withEmojiStatus && peer.emojiStatus && ( + {!noVerified && realPeer?.isVerified && } + {!noFake && realPeer?.fakeType && } + {withEmojiStatus && realPeer?.emojiStatus && ( )} - {withEmojiStatus && !peer.emojiStatus && isPremium && } + {withEmojiStatus && !realPeer?.emojiStatus && isPremium && } )} {iconElement} diff --git a/src/components/common/Picker.scss b/src/components/common/Picker.scss index f57e82b42..3f460805b 100644 --- a/src/components/common/Picker.scss +++ b/src/components/common/Picker.scss @@ -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; diff --git a/src/components/common/Picker.tsx b/src/components/common/Picker.tsx index aadeeb863..83cd6355d 100644 --- a/src/components/common/Picker.tsx +++ b/src/components/common/Picker.tsx @@ -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 = ({ className, + categories, itemIds, + selectedCategories, selectedIds, filterValue, filterPlaceholder, @@ -73,6 +79,7 @@ const Picker: FC = ({ lockedUnselectedSubtitle, forceShowSelf, isViewOnly, + onSelectedCategoriesChange, onSelectedIdsChange, onFilterChange, onDisabledClick, @@ -100,7 +107,16 @@ const Picker: FC = ({ 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 = ({ } }); - 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 = ({ 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 = ({ return buildCollectionByKey(countryList, 'iso2'); }, [countryList]); - const renderChatInfo = (id: string) => { + const renderCategory = useLastCallback((category: CustomPeer) => { + return ( + + ); + }); + + const renderChatInfo = useLastCallback((id: string) => { const isUnselectable = lockedUnselectedIdsSet.has(id); if (isCountryList && countriesByIso) { const country = countriesByIso[id]; @@ -169,12 +204,69 @@ const Picker: FC = ({ } else { return ; } - }; + }); + + 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 : ( + + ); + }; + return ( + handleItemClick(id)} + ripple + > + {!isRoundCheckbox ? renderCheckbox() : undefined} + {category ? renderCategory(category) : renderChatInfo(id)} + {isRoundCheckbox ? renderCheckbox() : undefined} + + ); + }, [ + categoriesByType, isRoundCheckbox, isViewOnly, lockedSelectedIdsSet, lockedUnselectedIdsSet, + onDisabledClick, renderChatInfo, selectedCategories, selectedIds, + ]); + + const beforeChildren = useMemo(() => { + return ( +
+ {Boolean(categories?.length) && ( +
{lang('PrivacyUserTypes')}
+ )} + {categories?.map((category) => renderItem(category.type, true))} +
{lang('FilterChats')}
+
+ ); + }, [categories, lang, renderItem]); return (
{isSearchable && (
+ {selectedCategories?.map((category) => ( + + ))} {lockedSelectedIds?.map((id, i) => ( = ({ - {viewportIds.map((id) => { - const shouldRenderLockIcon = lockedUnselectedIdsSet.has(id); - const isLocked = lockedSelectedIdsSet.has(id) || shouldRenderLockIcon; - const renderCheckbox = () => { - return (isViewOnly || shouldRenderLockIcon) ? undefined : ( - - ); - }; - return ( - handleItemClick(id)} - ripple - > - {!isRoundCheckbox ? renderCheckbox() : undefined} - {renderChatInfo(id)} - {isRoundCheckbox ? renderCheckbox() : undefined} - - ); - })} + {viewportIds.map((id) => renderItem(id))} ) : !isLoading && viewportIds && !viewportIds.length ? (

{notFoundText || 'Sorry, nothing found.'}

diff --git a/src/components/common/PickerSelectedItem.scss b/src/components/common/PickerSelectedItem.scss index f0af3e528..fe8d830e9 100644 --- a/src/components/common/PickerSelectedItem.scss +++ b/src/components/common/PickerSelectedItem.scss @@ -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; diff --git a/src/components/common/PickerSelectedItem.tsx b/src/components/common/PickerSelectedItem.tsx index 1048d352c..d42594826 100644 --- a/src/components/common/PickerSelectedItem.tsx +++ b/src/components/common/PickerSelectedItem.tsx @@ -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 = ({ clickArg, chat, user, + customPeer, className, fluid, isSavedMessages, @@ -59,23 +63,24 @@ const PickerSelectedItem: FC = ({ if (icon && title) { iconElement = (
- +
); titleText = title; - } else if (user || chat) { + } else if (customPeer || user || chat) { iconElement = ( ); - 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 = ({ 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 = ({ )} {canClose && (
- +
)}
diff --git a/src/components/common/PrivateChatInfo.tsx b/src/components/common/PrivateChatInfo.tsx index 3515582b0..5e06ec2e9 100644 --- a/src/components/common/PrivateChatInfo.tsx +++ b/src/components/common/PrivateChatInfo.tsx @@ -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 = ({ + customPeer, typingStatus, avatarSize = 'medium', status, @@ -82,7 +83,6 @@ const PrivateChatInfo: FC = ({ noEmojiStatus, noFake, noVerified, - isUnknownUser, noRtl, user, userStatus, @@ -131,7 +131,7 @@ const PrivateChatInfo: FC = ({ const mainUsername = useMemo(() => user && withUsername && getMainUsername(user), [user, withUsername]); - if (!user && !isUnknownUser) { + if (!user && !customPeer) { return undefined; } @@ -153,6 +153,14 @@ const PrivateChatInfo: FC = ({ ); } + if (customPeer?.subtitleKey) { + return ( + + {lang(customPeer.subtitleKey)} + + ); + } + if (!user) { return undefined; } @@ -194,7 +202,7 @@ const PrivateChatInfo: FC = ({ return ( = ({ isSavedMessages={isSavedMessages} isSavedDialog={isSavedDialog} onEmojiStatusClick={onEmojiStatusClick} - isUnknownUser={isUnknownUser} iconElement={iconElement} /> ); @@ -220,12 +227,11 @@ const PrivateChatInfo: FC = ({ /> )} = ({ export default memo(withGlobal( (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, diff --git a/src/components/common/helpers/peerColor.ts b/src/components/common/helpers/peerColor.ts index e0219e990..831b8b6b6 100644 --- a/src/components/common/helpers/peerColor.ts +++ b/src/components/common/helpers/peerColor.ts @@ -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)}`; } diff --git a/src/components/left/settings/Settings.tsx b/src/components/left/settings/Settings.tsx index 02afcb7ee..f2ad957dd 100644 --- a/src/components/left/settings/Settings.tsx +++ b/src/components/left/settings/Settings.tsx @@ -356,6 +356,7 @@ const Settings: FC = ({ return ( = ({ }, [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 = ({ const exceptionString = total.length ? `(${total.join(',')})` : ''; + if (shouldAllowPremium) { + return lang(exceptionString ? 'ContactsAndPremium' : 'PrivacyPremium'); + } + switch (visibility) { case 'everybody': return `${lang('P2PEverybody')} ${exceptionString}`; diff --git a/src/components/left/settings/SettingsPrivacyVisibility.tsx b/src/components/left/settings/SettingsPrivacyVisibility.tsx index cc0569662..09cf2ddb1 100644 --- a/src/components/left/settings/SettingsPrivacyVisibility.tsx +++ b/src/components/left/settings/SettingsPrivacyVisibility.tsx @@ -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(() => { diff --git a/src/components/left/settings/SettingsPrivacyVisibilityExceptionList.tsx b/src/components/left/settings/SettingsPrivacyVisibilityExceptionList.tsx index b63d4679f..a2e04047e 100644 --- a/src/components/left/settings/SettingsPrivacyVisibilityExceptionList.tsx +++ b/src/components/left/settings/SettingsPrivacyVisibilityExceptionList.tsx @@ -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 = ({ isAllowList, + withPremiumCategory, screen, isActive, currentUserId, @@ -57,23 +64,46 @@ const SettingsPrivacyVisibilityExceptionList: FC = ({ 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(''); const [isSubmitShown, setIsSubmitShown] = useState(false); const [newSelectedContactIds, setNewSelectedContactIds] = useState(selectedContactIds); + const [newSelectedCategoryTypes, setNewSelectedCategoryTypes] = useState(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 = ({ ]); }, [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 = ({ 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 = ({ return (
diff --git a/src/components/right/statistics/BoostStatistics.tsx b/src/components/right/statistics/BoostStatistics.tsx index b9a6dac83..0146a833a 100644 --- a/src/components/right/statistics/BoostStatistics.tsx +++ b/src/components/right/statistics/BoostStatistics.tsx @@ -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 = ({ diff --git a/src/components/ui/Checkbox.tsx b/src/components/ui/Checkbox.tsx index 966c9bfff..d3626b72d 100644 --- a/src/components/ui/Checkbox.tsx +++ b/src/components/ui/Checkbox.tsx @@ -19,7 +19,7 @@ type OwnProps = { value?: string; label: TeactNode; subLabel?: string; - checked: boolean; + checked?: boolean; rightIcon?: IconName; disabled?: boolean; tabIndex?: number; diff --git a/src/global/actions/api/settings.ts b/src/global/actions/api/settings.ts index 06b472bae..c10199a2e 100644 --- a/src/global/actions/api/settings.ts +++ b/src/global/actions/api/settings.ts @@ -533,7 +533,9 @@ addActionHandler('setPrivacyVisibility', async (global, actions, payload): Promi }); addActionHandler('setPrivacySettings', async (global, actions, payload): Promise => { - 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], }); diff --git a/src/global/types.ts b/src/global/types.ts index 251c41cfb..e1aa00f8a 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -1238,6 +1238,7 @@ export interface ActionPayloads { privacyKey: ApiPrivacyKey; isAllowList: boolean; updatedIds: string[]; + isPremiumAllowed?: true; }; loadNotificationExceptions: undefined; setThemeSettings: { theme: ThemeKey } & Partial; diff --git a/src/types/index.ts b/src/types/index.ts index bb2967148..5011f6d75 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -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; +} diff --git a/src/util/objects/customPeer.ts b/src/util/objects/customPeer.ts new file mode 100644 index 000000000..a0c47d569 --- /dev/null +++ b/src/util/objects/customPeer.ts @@ -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, +};