Premium Modal: Display user emoji status (#3776)

This commit is contained in:
Alexander Zinchuk 2023-09-04 04:05:23 +02:00
parent a7700015ac
commit 44c31ff6a0
8 changed files with 139 additions and 25 deletions

View File

@ -44,6 +44,7 @@ type OwnProps = {
noStatusOrTyping?: boolean; noStatusOrTyping?: boolean;
noRtl?: boolean; noRtl?: boolean;
adminMember?: ApiChatMember; adminMember?: ApiChatMember;
onEmojiStatusClick?: NoneToVoidFunction;
}; };
type StateProps = type StateProps =
@ -75,6 +76,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
areMessagesLoaded, areMessagesLoaded,
adminMember, adminMember,
ripple, ripple,
onEmojiStatusClick,
}) => { }) => {
const { const {
loadFullUser, loadFullUser,
@ -159,6 +161,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
withEmojiStatus={!noEmojiStatus} withEmojiStatus={!noEmojiStatus}
emojiStatusSize={emojiStatusSize} emojiStatusSize={emojiStatusSize}
isSavedMessages={isSavedMessages} isSavedMessages={isSavedMessages}
onEmojiStatusClick={onEmojiStatusClick}
/> />
{customTitle && <span className="custom-title">{customTitle}</span>} {customTitle && <span className="custom-title">{customTitle}</span>}
</div> </div>
@ -171,6 +174,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
withEmojiStatus={!noEmojiStatus} withEmojiStatus={!noEmojiStatus}
emojiStatusSize={emojiStatusSize} emojiStatusSize={emojiStatusSize}
isSavedMessages={isSavedMessages} isSavedMessages={isSavedMessages}
onEmojiStatusClick={onEmojiStatusClick}
/> />
); );
} }

View File

@ -44,7 +44,7 @@
.custom-emoji { .custom-emoji {
--custom-emoji-size: 1.5rem; --custom-emoji-size: 1.5rem;
color: var(--color-white); color: var(--color-white) !important;
pointer-events: auto; pointer-events: auto;
cursor: var(--custom-cursor, pointer); cursor: var(--custom-cursor, pointer);
} }

View File

@ -138,10 +138,10 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
}); });
}); });
const handleClickPremium = useLastCallback(() => { const handleStatusClick = useLastCallback(() => {
if (!user) return; if (!userId) return;
openPremiumModal({ fromUserId: user.id }); openPremiumModal({ fromUserId: userId });
}); });
const selectPreviousMedia = useLastCallback(() => { const selectPreviousMedia = useLastCallback(() => {
@ -334,7 +334,7 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
withEmojiStatus withEmojiStatus
emojiStatusSize={EMOJI_STATUS_SIZE} emojiStatusSize={EMOJI_STATUS_SIZE}
isSavedMessages={isSavedMessages} isSavedMessages={isSavedMessages}
onEmojiStatusClick={handleClickPremium} onEmojiStatusClick={handleStatusClick}
noLoopLimit noLoopLimit
canCopyTitle canCopyTitle
/> />

View File

@ -11,7 +11,7 @@
} }
.modal-header { .modal-header {
padding: 0.5rem 1rem; padding: 0.5rem;
&.with-top-border { &.with-top-border {
/* stylelint-disable-next-line plugin/whole-pixel */ /* stylelint-disable-next-line plugin/whole-pixel */
@ -24,13 +24,13 @@
text-align: center; text-align: center;
padding: 0 !important; padding: 0 !important;
}
&.custom-emoji { &.custom-emoji .modal-content {
--emoji-size: 2.25rem; --emoji-size: 2.25rem;
.stickers { .stickers {
padding: 0 0.5rem; padding: 0 0.5rem;
}
} }
} }

View File

@ -27,4 +27,5 @@
align-self: center; align-self: center;
border-radius: 0.625rem; border-radius: 0.625rem;
background: var(--item-color, #000); background: var(--item-color, #000);
margin-left: 1rem;
} }

View File

@ -1,3 +1,5 @@
@import '../../../styles/mixins';
.root { .root {
--premium-gradient: linear-gradient(88.39deg, #6C93FF -2.56%, #976FFF 51.27%, #DF69D1 107.39%); --premium-gradient: linear-gradient(88.39deg, #6C93FF -2.56%, #976FFF 51.27%, #DF69D1 107.39%);
--premium-feature-background: linear-gradient(65.85deg, #6C93FF -0.24%, #976FFF 53.99%, #DF69D1 110.53%); --premium-feature-background: linear-gradient(65.85deg, #6C93FF -0.24%, #976FFF 53.99%, #DF69D1 110.53%);
@ -30,12 +32,14 @@
} }
.main { .main {
padding: 1rem; padding: 1rem 0.5rem;
height: 100%; height: 100%;
overflow: auto; overflow: scroll;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@include adapt-padding-to-scrollbar(0.5rem);
} }
.logo { .logo {
@ -45,14 +49,23 @@
min-height: 6.25rem; min-height: 6.25rem;
} }
.status-emoji {
--custom-emoji-size: 8rem;
margin: 1rem;
cursor: var(--custom-cursor, pointer);
}
.header-text { .header-text {
font-size: 1.5rem; font-size: 1.5rem;
font-weight: 500; font-weight: 500;
text-align: center; text-align: center;
margin-inline: 0.5rem;
} }
.description { .description {
text-align: center; text-align: center;
margin-inline: 0.5rem;
margin-bottom: 2rem; margin-bottom: 2rem;
} }
@ -119,6 +132,25 @@
padding: 1rem; padding: 1rem;
} }
.stickerSetText {
font-size: 1.25rem;
}
.stickerSetLink {
--custom-emoji-size: 1.5rem;
color: var(--color-links);
cursor: var(--custom-cursor, pointer);
&:hover {
text-decoration: underline;
}
}
.stickerSetLinkIcon {
vertical-align: middle;
margin-inline-end: 0.25rem;
}
@media (max-width: 600px) { @media (max-width: 600px) {
.root :global(.modal-dialog) { .root :global(.modal-dialog) {

View File

@ -4,7 +4,9 @@ import React, {
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import type { FC } from '../../../lib/teact/teact'; import type { FC } from '../../../lib/teact/teact';
import type { ApiPremiumPromo, ApiUser } from '../../../api/types'; import type {
ApiPremiumPromo, ApiSticker, ApiStickerSet, ApiUser,
} from '../../../api/types';
import type { GlobalState } from '../../../global/types'; import type { GlobalState } from '../../../global/types';
import PremiumFeatureModal, { import PremiumFeatureModal, {
@ -15,19 +17,24 @@ import PremiumFeatureModal, {
import { TME_LINK_PREFIX } from '../../../config'; import { TME_LINK_PREFIX } from '../../../config';
import { formatCurrency } from '../../../util/formatCurrency'; import { formatCurrency } from '../../../util/formatCurrency';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { selectTabState, selectIsCurrentUserPremium, selectUser } from '../../../global/selectors'; import {
selectTabState, selectIsCurrentUserPremium, selectUser, selectStickerSet,
} from '../../../global/selectors';
import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities'; import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities';
import { selectPremiumLimit } from '../../../global/selectors/limits'; import { selectPremiumLimit } from '../../../global/selectors/limits';
import renderText from '../../common/helpers/renderText'; import renderText from '../../common/helpers/renderText';
import { getUserFullName } from '../../../global/helpers'; import { getUserFullName } from '../../../global/helpers';
import { REM } from '../../common/helpers/mediaDimensions';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import useSyncEffect from '../../../hooks/useSyncEffect'; import useSyncEffect from '../../../hooks/useSyncEffect';
import useLastCallback from '../../../hooks/useLastCallback';
import Modal from '../../ui/Modal'; import Modal from '../../ui/Modal';
import Button from '../../ui/Button'; import Button from '../../ui/Button';
import PremiumFeatureItem from './PremiumFeatureItem'; import PremiumFeatureItem from './PremiumFeatureItem';
import Transition from '../../ui/Transition'; import Transition from '../../ui/Transition';
import CustomEmoji from '../../common/CustomEmoji';
import PremiumLogo from '../../../assets/premium/PremiumLogo.svg'; import PremiumLogo from '../../../assets/premium/PremiumLogo.svg';
import PremiumLimits from '../../../assets/premium/PremiumLimits.svg'; import PremiumLimits from '../../../assets/premium/PremiumLimits.svg';
@ -47,6 +54,7 @@ import PremiumTranslate from '../../../assets/premium/PremiumTranslate.svg';
import styles from './PremiumMainModal.module.scss'; import styles from './PremiumMainModal.module.scss';
const LIMIT_ACCOUNTS = 4; const LIMIT_ACCOUNTS = 4;
const STATUS_EMOJI_SIZE = 8 * REM;
const PREMIUM_FEATURE_COLOR_ICONS: Record<string, string> = { const PREMIUM_FEATURE_COLOR_ICONS: Record<string, string> = {
double_limits: PremiumLimits, double_limits: PremiumLimits,
@ -73,6 +81,8 @@ type StateProps = {
promo?: ApiPremiumPromo; promo?: ApiPremiumPromo;
isClosing?: boolean; isClosing?: boolean;
fromUser?: ApiUser; fromUser?: ApiUser;
fromUserStatusEmoji?: ApiSticker;
fromUserStatusSet?: ApiStickerSet;
toUser?: ApiUser; toUser?: ApiUser;
initialSection?: string; initialSection?: string;
isPremium?: boolean; isPremium?: boolean;
@ -93,6 +103,8 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
isOpen, isOpen,
currentUserId, currentUserId,
fromUser, fromUser,
fromUserStatusEmoji,
fromUserStatusSet,
promo, promo,
initialSection, initialSection,
isPremium, isPremium,
@ -113,7 +125,7 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const dialogRef = useRef<HTMLDivElement>(null); const dialogRef = useRef<HTMLDivElement>(null);
const { const {
closePremiumModal, openInvoice, requestConfetti, openTelegramLink, closePremiumModal, openInvoice, requestConfetti, openTelegramLink, loadStickers, openStickerSet,
} = getActions(); } = getActions();
const lang = useLang(); const lang = useLang();
@ -148,9 +160,9 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
} }
} }
function handleClick() { const handleClick = useLastCallback(() => {
handleClickWithStartParam(); handleClickWithStartParam();
} });
const showConfetti = useCallback(() => { const showConfetti = useCallback(() => {
const dialog = dialogRef.current; const dialog = dialogRef.current;
@ -185,7 +197,39 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
return premiumPromoOrder.filter((section) => PREMIUM_FEATURE_SECTIONS.includes(section)); return premiumPromoOrder.filter((section) => PREMIUM_FEATURE_SECTIONS.includes(section));
}, [premiumPromoOrder]); }, [premiumPromoOrder]);
if (!promo) return undefined; useEffect(() => {
if (!fromUserStatusEmoji || fromUserStatusSet) return;
loadStickers({
stickerSetInfo: fromUserStatusEmoji.stickerSetInfo,
});
}, [loadStickers, fromUserStatusEmoji, fromUserStatusSet]);
const handleOpenStatusSet = useLastCallback(() => {
if (!fromUserStatusSet) return;
openStickerSet({
stickerSetInfo: fromUserStatusSet,
});
});
const stickerSetTitle = useMemo(() => {
if (!fromUserStatusSet || !fromUser) return undefined;
const template = lang('lng_premium_emoji_status_title').replace('{user}', getUserFullName(fromUser)!);
const [first, second] = template.split('{link}');
const emoji = fromUserStatusSet.thumbCustomEmojiId ? (
<CustomEmoji className={styles.stickerSetLinkIcon} documentId={fromUserStatusSet.thumbCustomEmojiId} />
) : undefined;
const link = (
<span className={styles.stickerSetLink} onClick={handleOpenStatusSet}>
{emoji}{renderText(fromUserStatusSet.title)}
</span>
);
return [renderText(first), link, renderText(second)];
}, [fromUser, fromUserStatusSet, lang]);
if (!promo || (fromUserStatusEmoji && !fromUserStatusSet)) return undefined;
// TODO Support all subscription options // TODO Support all subscription options
const month = promo.options.find((option) => option.months === 1)!; const month = promo.options.find((option) => option.months === 1)!;
@ -209,6 +253,10 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
: lang('TelegramPremiumUserGiftedPremiumDialogSubtitle'); : lang('TelegramPremiumUserGiftedPremiumDialogSubtitle');
} }
if (fromUserStatusSet) {
return lang('TelegramPremiumUserStatusDialogSubtitle');
}
return fromUser return fromUser
? lang('TelegramPremiumUserDialogSubtitle') ? lang('TelegramPremiumUserDialogSubtitle')
: lang(isPremium ? 'TelegramPremiumSubscribedSubtitle' : 'TelegramPremiumSubtitle'); : lang(isPremium ? 'TelegramPremiumSubscribedSubtitle' : 'TelegramPremiumSubtitle');
@ -252,9 +300,19 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
> >
<i className="icon icon-close" /> <i className="icon icon-close" />
</Button> </Button>
<img className={styles.logo} src={PremiumLogo} alt="" /> {fromUserStatusEmoji ? (
<h2 className={styles.headerText}> <CustomEmoji
{renderText(getHeaderText(), ['simple_markdown', 'emoji'])} className={styles.statusEmoji}
onClick={handleOpenStatusSet}
documentId={fromUserStatusEmoji.id}
isBig
size={STATUS_EMOJI_SIZE}
/>
) : (
<img className={styles.logo} src={PremiumLogo} alt="" />
)}
<h2 className={buildClassName(styles.headerText, fromUserStatusSet && styles.stickerSetText)}>
{fromUserStatusSet ? stickerSetTitle : renderText(getHeaderText(), ['simple_markdown', 'emoji'])}
</h2> </h2>
<div className={styles.description}> <div className={styles.description}>
{renderText(getHeaderDescription(), ['simple_markdown', 'emoji'])} {renderText(getHeaderDescription(), ['simple_markdown', 'emoji'])}
@ -297,7 +355,6 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
</div> </div>
{!isPremium && ( {!isPremium && (
<div className={styles.footer}> <div className={styles.footer}>
{/* eslint-disable-next-line react/jsx-no-bind */}
<Button className={styles.button} isShiny withPremiumGradient onClick={handleClick}> <Button className={styles.button} isShiny withPremiumGradient onClick={handleClick}>
{lang('SubscribeToPremium', formatCurrency(Number(month.amount), month.currency, lang.code))} {lang('SubscribeToPremium', formatCurrency(Number(month.amount), month.currency, lang.code))}
</Button> </Button>
@ -324,6 +381,13 @@ export default memo(withGlobal<OwnProps>((global): StateProps => {
const { const {
premiumModal, premiumModal,
} = selectTabState(global); } = selectTabState(global);
const fromUser = premiumModal?.fromUserId ? selectUser(global, premiumModal.fromUserId) : undefined;
const fromUserStatusEmoji = fromUser?.emojiStatus ? global.customEmojis.byId[fromUser.emojiStatus.documentId]
: undefined;
const fromUserStatusSet = fromUserStatusEmoji ? selectStickerSet(global, fromUserStatusEmoji.stickerSetInfo)
: undefined;
return { return {
currentUserId: global.currentUserId, currentUserId: global.currentUserId,
promo: premiumModal?.promo, promo: premiumModal?.promo,
@ -331,7 +395,9 @@ export default memo(withGlobal<OwnProps>((global): StateProps => {
isSuccess: premiumModal?.isSuccess, isSuccess: premiumModal?.isSuccess,
isGift: premiumModal?.isGift, isGift: premiumModal?.isGift,
monthsAmount: premiumModal?.monthsAmount, monthsAmount: premiumModal?.monthsAmount,
fromUser: premiumModal?.fromUserId ? selectUser(global, premiumModal.fromUserId) : undefined, fromUser,
fromUserStatusEmoji,
fromUserStatusSet,
toUser: premiumModal?.toUserId ? selectUser(global, premiumModal.toUserId) : undefined, toUser: premiumModal?.toUserId ? selectUser(global, premiumModal.toUserId) : undefined,
initialSection: premiumModal?.initialSection, initialSection: premiumModal?.initialSection,
isPremium: selectIsCurrentUserPremium(global), isPremium: selectIsCurrentUserPremium(global),

View File

@ -149,6 +149,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
loadPinnedMessages, loadPinnedMessages,
toggleLeftColumn, toggleLeftColumn,
exitMessageSelectMode, exitMessageSelectMode,
openPremiumModal,
} = getActions(); } = getActions();
const lang = useLang(); const lang = useLang();
@ -184,7 +185,12 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
const componentRef = useRef<HTMLDivElement>(null); const componentRef = useRef<HTMLDivElement>(null);
const shouldAnimateTools = useRef<boolean>(true); const shouldAnimateTools = useRef<boolean>(true);
const { handleClick: handleHeaderClick, handleMouseDown: handleHeaderMouseDown } = useFastClick(() => { const {
handleClick: handleHeaderClick,
handleMouseDown: handleHeaderMouseDown,
} = useFastClick((e: React.MouseEvent<HTMLDivElement | HTMLButtonElement>) => {
if (e.type === 'mousedown' && (e.target as Element).closest('.title > .custom-emoji')) return;
openChatWithInfo({ id: chatId, threadId }); openChatWithInfo({ id: chatId, threadId });
}); });
@ -214,6 +220,10 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
}, BACK_BUTTON_INACTIVE_TIME); }, BACK_BUTTON_INACTIVE_TIME);
}); });
const handleStatusClick = useLastCallback(() => {
openPremiumModal({ fromUserId: chatId });
});
const handleBackClick = useLastCallback((e: React.MouseEvent<HTMLElement, MouseEvent>) => { const handleBackClick = useLastCallback((e: React.MouseEvent<HTMLElement, MouseEvent>) => {
if (!isBackButtonActive.current) return; if (!isBackButtonActive.current) return;
@ -371,6 +381,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
withUpdatingStatus withUpdatingStatus
emojiStatusSize={EMOJI_STATUS_SIZE} emojiStatusSize={EMOJI_STATUS_SIZE}
noRtl noRtl
onEmojiStatusClick={handleStatusClick}
/> />
) : ( ) : (
<GroupChatInfo <GroupChatInfo