From 90e6470eed4b583f43d9a5828cbff281ef8f24c8 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Fri, 1 Mar 2024 14:02:52 -0500 Subject: [PATCH] Messages: Users who aren't contacts or don't have Premium can't start chats (#4308) --- src/api/gramjs/apiBuilders/users.ts | 2 + src/api/gramjs/methods/messages.ts | 4 + src/api/types/users.ts | 1 + src/assets/tgs/Unlock.tgs | Bin 0 -> 824 bytes .../common/helpers/animatedAssets.ts | 2 + src/components/middle/MessageList.tsx | 5 ++ src/components/middle/MiddleColumn.tsx | 17 ++++- .../middle/PremiumRequiredMessage.module.scss | 66 ++++++++++++++++ .../middle/PremiumRequiredMessage.tsx | 72 ++++++++++++++++++ .../middle/PremiumRequiredPlaceholder.tsx | 42 ++++++++++ 10 files changed, 208 insertions(+), 3 deletions(-) create mode 100644 src/assets/tgs/Unlock.tgs create mode 100644 src/components/middle/PremiumRequiredMessage.module.scss create mode 100644 src/components/middle/PremiumRequiredMessage.tsx create mode 100644 src/components/middle/PremiumRequiredPlaceholder.tsx diff --git a/src/api/gramjs/apiBuilders/users.ts b/src/api/gramjs/apiBuilders/users.ts index e42696f67..54c3cf590 100644 --- a/src/api/gramjs/apiBuilders/users.ts +++ b/src/api/gramjs/apiBuilders/users.ts @@ -18,6 +18,7 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse about, commonChatsCount, pinnedMsgId, botInfo, blocked, profilePhoto, voiceMessagesForbidden, premiumGifts, fallbackPhoto, personalPhoto, translationsDisabled, storiesPinnedAvailable, + contactRequirePremium, }, users, } = mtpUserFull; @@ -37,6 +38,7 @@ export function buildApiUserFullInfo(mtpUserFull: GramJs.users.UserFull): ApiUse personalPhoto: personalPhoto instanceof GramJs.Photo ? buildApiPhoto(personalPhoto) : undefined, ...(premiumGifts && { premiumGifts: premiumGifts.map((gift) => buildApiPremiumGiftOption(gift)) }), ...(botInfo && { botInfo: buildApiBotInfo(botInfo, userId) }), + isContactRequirePremium: contactRequirePremium, }; } diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index 21dd438ba..ee4f6af94 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -394,6 +394,10 @@ export function sendMessage( }); if (update) handleLocalMessageUpdate(localMessage, update); } catch (error: any) { + if (error.message === 'PRIVACY_PREMIUM_REQUIRED') { + onUpdate({ '@type': 'updateRequestUserUpdate', id: chat.id }); + } + onUpdate({ '@type': 'updateMessageSendFailed', chatId: chat.id, diff --git a/src/api/types/users.ts b/src/api/types/users.ts index 2d96f80f3..e0f1a4939 100644 --- a/src/api/types/users.ts +++ b/src/api/types/users.ts @@ -53,6 +53,7 @@ export interface ApiUserFullInfo { premiumGifts?: ApiPremiumGiftOption[]; isTranslationDisabled?: true; hasPinnedStories?: boolean; + isContactRequirePremium?: boolean; } export type ApiFakeType = 'fake' | 'scam'; diff --git a/src/assets/tgs/Unlock.tgs b/src/assets/tgs/Unlock.tgs new file mode 100644 index 0000000000000000000000000000000000000000..b317f28f28983c350a8bda5e98cafa8091af7fd7 GIT binary patch literal 824 zcmV-81IPRyiwFP!000001Kn5OZ<{a>{#PPD1G2$5gtx8R`Z~4KULqbCAq|5-WN5dl z%KyHzec?xerK_@PnySX&JAdEZ*}gA%A`c`az7sf%SY#_nD5yoU0|{ZMU|=ph{2-wR z!_Exl9+upfMU~{RX0zF7=eVxZrY7M!vPcm>rcymWwdSmZky)g9Bq2u(h2R22Eww3! z6;Gs6AF*DO!!%WEf5HPhT129aX&aztY67fl>Iks}WJUVJjb@T!dOWoGxF3c8bXx#so9GOEU zTzZxcipW+L;}!+vrcxx9O3PcBnM}{X-ykFEtY`-{OTc;zp2Hxf9t-UgC`y-)SfEo$8^J zx>op)t^!>ZU}ZLU7avk{iPfbgcM`5@)|jJGnp>?6^^UZ3`H)B#YQ5GG;iv67E?wb- zc|lvG3)p$L#{LT0IX@_8n_D(hurPA7EPw&uxD_bRgoN9;sM90X?uZTObKL9}s`2eh zczgHZ{l~@EG-)cixK+t7?8UXL?hoLw+q4i^4!I$Sa#P)xo5fWnOK5r;P7aE|u{y_g z=SFm&vZ6^P@ZDTZlGP2-S@ic%9~_I}{&}b4H*TH$s5^MiAT*(eWAns zw%49IyW2)qc{*LCYZ)JQSpsnzugTaOb#4x$*rRSQ;yELmJ-NNChsDO6#e6xI3N)Wu zqzYBXQx^YpV!p_@thbfi6Lk>>Dpo>{aOo4=W2_u@Ja1NmE~KWy5p4PFo5AJf^js>lYARl$rmm6~ z_XUC~bmXVum9lDtB~jn1HP>?zZZWslH)osbxGnOo!x_zjE)J8puy0~yL$v`REY4|-@_7TL5fG{P=ahz_(zt(7X~4dZWo~}LMaN&2#O!J$4gdg& Cft8K` literal 0 HcmV?d00001 diff --git a/src/components/common/helpers/animatedAssets.ts b/src/components/common/helpers/animatedAssets.ts index 1079ead2d..316469fed 100644 --- a/src/components/common/helpers/animatedAssets.ts +++ b/src/components/common/helpers/animatedAssets.ts @@ -23,6 +23,7 @@ import FoldersAll from '../../../assets/tgs/settings/FoldersAll.tgs'; import FoldersNew from '../../../assets/tgs/settings/FoldersNew.tgs'; import FoldersShare from '../../../assets/tgs/settings/FoldersShare.tgs'; import Lock from '../../../assets/tgs/settings/Lock.tgs'; +import Unlock from '../../../assets/tgs/Unlock.tgs'; export const LOCAL_TGS_URLS = { MonkeyIdle, @@ -50,4 +51,5 @@ export const LOCAL_TGS_URLS = { PartyPopper, Flame, ReadTime, + Unlock, }; diff --git a/src/components/middle/MessageList.tsx b/src/components/middle/MessageList.tsx index 941c63ebd..4fce360f8 100644 --- a/src/components/middle/MessageList.tsx +++ b/src/components/middle/MessageList.tsx @@ -79,6 +79,7 @@ import ContactGreeting from './ContactGreeting'; import MessageListBotInfo from './MessageListBotInfo'; import MessageListContent from './MessageListContent'; import NoMessages from './NoMessages'; +import PremiumRequiredMessage from './PremiumRequiredMessage'; import './MessageList.scss'; @@ -96,6 +97,7 @@ type OwnProps = { withDefaultBg: boolean; onPinnedIntersectionChange: PinnedIntersectionChangedCallback; getForceNextPinnedInHeader: Signal; + isContactRequirePremium?: boolean; }; type StateProps = { @@ -181,6 +183,7 @@ const MessageList: FC = ({ currentUserId, getForceNextPinnedInHeader, onPinnedIntersectionChange, + isContactRequirePremium, }) => { const { loadViewportMessages, setScrollOffset, loadSponsoredMessages, loadMessageReactions, copyMessagesByIds, @@ -607,6 +610,8 @@ const MessageList: FC = ({ {restrictionReason ? restrictionReason.text : `This is a private ${isChannelChat ? 'channel' : 'chat'}`} + ) : isContactRequirePremium ? ( + ) : isBot && !hasMessages ? ( ) : shouldRenderGreeting ? ( diff --git a/src/components/middle/MiddleColumn.tsx b/src/components/middle/MiddleColumn.tsx index 12117b4f4..80d9370cd 100644 --- a/src/components/middle/MiddleColumn.tsx +++ b/src/components/middle/MiddleColumn.tsx @@ -55,6 +55,7 @@ import { selectTabState, selectTheme, selectThreadInfo, + selectUserFullInfo, } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; import buildStyle from '../../util/buildStyle'; @@ -92,6 +93,7 @@ import MessageList from './MessageList'; import MessageSelectToolbar from './MessageSelectToolbar.async'; import MiddleHeader from './MiddleHeader'; import MobileSearch from './MobileSearch.async'; +import PremiumRequiredPlaceholder from './PremiumRequiredPlaceholder'; import ReactorListModal from './ReactorListModal.async'; import './MiddleColumn.scss'; @@ -151,6 +153,7 @@ type StateProps = { canUnblock?: boolean; isSavedDialog?: boolean; canShowOpenChatButton?: boolean; + isContactRequirePremium?: boolean; }; function isImage(item: DataTransferItem) { @@ -210,6 +213,7 @@ function MiddleColumn({ canUnblock, isSavedDialog, canShowOpenChatButton, + isContactRequirePremium, }: OwnProps & StateProps) { const { openChat, @@ -267,7 +271,7 @@ function MiddleColumn({ const renderingCanUnblock = usePrevDuringAnimation(canUnblock, closeAnimationDuration); const renderingCanPost = usePrevDuringAnimation(canPost, closeAnimationDuration) && !renderingCanRestartBot && !renderingCanStartBot && !renderingCanSubscribe && !renderingCanUnblock - && chatId !== TMP_CHAT_ID; + && chatId !== TMP_CHAT_ID && !isContactRequirePremium; const renderingHasTools = usePrevDuringAnimation(hasTools, closeAnimationDuration); const renderingIsFabShown = usePrevDuringAnimation(isFabShown, closeAnimationDuration) && chatId !== TMP_CHAT_ID; const renderingIsChannel = usePrevDuringAnimation(isChannel, closeAnimationDuration); @@ -449,7 +453,9 @@ function MiddleColumn({ ); const forumComposerPlaceholder = getForumComposerPlaceholder(lang, chat, threadId, Boolean(draftReplyInfo)); - const composerRestrictionMessage = messageSendingRestrictionReason || forumComposerPlaceholder; + const composerRestrictionMessage = messageSendingRestrictionReason + ?? forumComposerPlaceholder + ?? (isContactRequirePremium ? : undefined); // CSS Variables calculation doesn't work properly with transforms, so we calculate transform values in JS const { @@ -549,6 +555,7 @@ function MiddleColumn({ onFabToggle={setIsFabShown} onNotchToggle={setIsNotchShown} isReady={isReady} + isContactRequirePremium={isContactRequirePremium} withBottomShift={withMessageListBottomShift} withDefaultBg={Boolean(!customBackground && !backgroundColor)} onPinnedIntersectionChange={renderingOnPinnedIntersectionChange!} @@ -805,6 +812,8 @@ export default memo(withGlobal( ) ); + const isContactRequirePremium = selectUserFullInfo(global, chatId)?.isContactRequirePremium; + return { ...state, chatId, @@ -815,7 +824,8 @@ export default memo(withGlobal( isPrivate, areChatSettingsLoaded: Boolean(chat?.settings), isComments: isMessageThread, - canPost: !isPinnedMessageList + canPost: + !isPinnedMessageList && (!chat || canPost) && !isBotNotStarted && !(shouldJoinToSend && chat?.isNotJoined) @@ -842,6 +852,7 @@ export default memo(withGlobal( canUnblock, isSavedDialog, canShowOpenChatButton, + isContactRequirePremium, }; }, )(MiddleColumn)); diff --git a/src/components/middle/PremiumRequiredMessage.module.scss b/src/components/middle/PremiumRequiredMessage.module.scss new file mode 100644 index 000000000..8d2855d54 --- /dev/null +++ b/src/components/middle/PremiumRequiredMessage.module.scss @@ -0,0 +1,66 @@ +.root { + width: 100%; + height: 100%; + display: flex; + align-items: center; + justify-content: center; + color: var(--color-white); + + .button { + background: var(--pattern-color); + width: 10rem; + margin-top: 0.5rem; + text-transform: none; + color: var(--color-white); + height: 2.25rem; + line-height: 2.25rem; + transition: filter 150ms ease-in-out; + + &:not(.disabled):not(:disabled):hover { + background-color: var(--pattern-color); + filter: brightness(1.05); + } + } +} + +.inner { + display: flex; + flex-direction: column; + align-items: center; + background: var(--pattern-color); + max-width: 15rem; + padding: 0.75rem 0; + border-radius: 1.5rem; + + &[dir="rtl"] { + text-align: right; + } +} + +.icons-container { + border-radius: 50%; + width: 8rem; + height: 8rem; + display: flex; + align-items: center; + justify-content: center; + position: relative; + background: var(--pattern-color); +} + +.animated-unlock { + z-index: 10; +} + +.comments-icon { + font-size: 5rem; + position: absolute; + top: 0; + transform: translateY(1.75rem); +} + +.description { + text-align: center; + padding: 0 1rem; + margin-top: 0.5rem; +} diff --git a/src/components/middle/PremiumRequiredMessage.tsx b/src/components/middle/PremiumRequiredMessage.tsx new file mode 100644 index 000000000..4af7b52c8 --- /dev/null +++ b/src/components/middle/PremiumRequiredMessage.tsx @@ -0,0 +1,72 @@ +import React, { memo } from '../../lib/teact/teact'; +import { getActions, withGlobal } from '../../global'; + +import { getUserFirstOrLastName } from '../../global/helpers'; +import { selectTheme, selectUser } from '../../global/selectors'; +import { LOCAL_TGS_URLS } from '../common/helpers/animatedAssets'; +import renderText from '../common/helpers/renderText'; + +import useLang from '../../hooks/useLang'; +import useLastCallback from '../../hooks/useLastCallback'; + +import AnimatedIconWithPreview from '../common/AnimatedIconWithPreview'; +import Icon from '../common/Icon'; +import Button from '../ui/Button'; + +import styles from './PremiumRequiredMessage.module.scss'; + +type OwnProps = { + userId: string; +}; + +type StateProps = { + patternColor?: string; + userName?: string; +}; + +function PremiumRequiredMessage({ patternColor, userName }: StateProps) { + const lang = useLang(); + const { openPremiumModal } = getActions(); + + const handleOpenPremiumModal = useLastCallback(() => openPremiumModal()); + + return ( +
+
+
+ + +
+ + {renderText(lang('MessageLockedPremium', userName), ['simple_markdown'])} + + +
+
+ ); +} + +export default memo( + withGlobal((global, { userId }): StateProps => { + const theme = selectTheme(global); + const { patternColor } = global.settings.themes[theme] || {}; + const user = selectUser(global, userId); + + return { + patternColor, + userName: getUserFirstOrLastName(user), + }; + })(PremiumRequiredMessage), +); diff --git a/src/components/middle/PremiumRequiredPlaceholder.tsx b/src/components/middle/PremiumRequiredPlaceholder.tsx new file mode 100644 index 000000000..5dacebe1a --- /dev/null +++ b/src/components/middle/PremiumRequiredPlaceholder.tsx @@ -0,0 +1,42 @@ +import React, { memo } from '../../lib/teact/teact'; +import { getActions, withGlobal } from '../../global'; + +import { getUserFirstOrLastName } from '../../global/helpers'; +import { selectUser } from '../../global/selectors'; + +import useLang from '../../hooks/useLang'; +import useLastCallback from '../../hooks/useLastCallback'; + +import Link from '../ui/Link'; + +type OwnProps = { + userId: string; +}; + +type StateProps = { + userName?: string; +}; + +function PremiumRequiredPlaceholder({ userName }: StateProps) { + const lang = useLang(); + const { openPremiumModal } = getActions(); + + const handleOpenPremiumModal = useLastCallback(() => openPremiumModal()); + + return ( +
+
{lang('Chat.MessagingRestrictedPlaceholder', userName)}
+ {lang('Chat.MessagingRestrictedPlaceholderAction')} +
+ ); +} + +export default memo(withGlobal( + (global, { userId }): StateProps => { + const user = selectUser(global, userId); + + return { + userName: getUserFirstOrLastName(user), + }; + }, +)(PremiumRequiredPlaceholder));