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 000000000..b317f28f2 Binary files /dev/null and b/src/assets/tgs/Unlock.tgs differ 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));