Messages: Users who aren't contacts or don't have Premium can't start chats (#4308)

This commit is contained in:
Alexander Zinchuk 2024-03-01 14:02:52 -05:00
parent 45d019b5f8
commit 90e6470eed
10 changed files with 208 additions and 3 deletions

View File

@ -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,
};
}

View File

@ -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,

View File

@ -53,6 +53,7 @@ export interface ApiUserFullInfo {
premiumGifts?: ApiPremiumGiftOption[];
isTranslationDisabled?: true;
hasPinnedStories?: boolean;
isContactRequirePremium?: boolean;
}
export type ApiFakeType = 'fake' | 'scam';

BIN
src/assets/tgs/Unlock.tgs Normal file

Binary file not shown.

View File

@ -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,
};

View File

@ -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<boolean | undefined>;
isContactRequirePremium?: boolean;
};
type StateProps = {
@ -181,6 +183,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
currentUserId,
getForceNextPinnedInHeader,
onPinnedIntersectionChange,
isContactRequirePremium,
}) => {
const {
loadViewportMessages, setScrollOffset, loadSponsoredMessages, loadMessageReactions, copyMessagesByIds,
@ -607,6 +610,8 @@ const MessageList: FC<OwnProps & StateProps> = ({
{restrictionReason ? restrictionReason.text : `This is a private ${isChannelChat ? 'channel' : 'chat'}`}
</span>
</div>
) : isContactRequirePremium ? (
<PremiumRequiredMessage userId={chatId} />
) : isBot && !hasMessages ? (
<MessageListBotInfo chatId={chatId} />
) : shouldRenderGreeting ? (

View File

@ -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 ? <PremiumRequiredPlaceholder userId={chatId!} /> : 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<OwnProps>(
)
);
const isContactRequirePremium = selectUserFullInfo(global, chatId)?.isContactRequirePremium;
return {
...state,
chatId,
@ -815,7 +824,8 @@ export default memo(withGlobal<OwnProps>(
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<OwnProps>(
canUnblock,
isSavedDialog,
canShowOpenChatButton,
isContactRequirePremium,
};
},
)(MiddleColumn));

View File

@ -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;
}

View File

@ -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 (
<div className={styles.root}>
<div className={styles.inner}>
<div className={styles.iconsContainer}>
<AnimatedIconWithPreview
tgsUrl={LOCAL_TGS_URLS.Unlock}
size={54}
color={patternColor}
className={styles.animatedUnlock}
/>
<Icon name="comments-sticker" className={styles.commentsIcon} />
</div>
<span className={styles.description}>
{renderText(lang('MessageLockedPremium', userName), ['simple_markdown'])}
</span>
<Button
color="translucent-black"
size="tiny"
onClick={handleOpenPremiumModal}
className={styles.button}
>
{lang('MessagePremiumUnlock')}
</Button>
</div>
</div>
);
}
export default memo(
withGlobal<OwnProps>((global, { userId }): StateProps => {
const theme = selectTheme(global);
const { patternColor } = global.settings.themes[theme] || {};
const user = selectUser(global, userId);
return {
patternColor,
userName: getUserFirstOrLastName(user),
};
})(PremiumRequiredMessage),
);

View File

@ -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 (
<div>
<div>{lang('Chat.MessagingRestrictedPlaceholder', userName)}</div>
<Link isPrimary onClick={handleOpenPremiumModal}>{lang('Chat.MessagingRestrictedPlaceholderAction')}</Link>
</div>
);
}
export default memo(withGlobal<OwnProps>(
(global, { userId }): StateProps => {
const user = selectUser(global, userId);
return {
userName: getUserFirstOrLastName(user),
};
},
)(PremiumRequiredPlaceholder));