Introduce Gift Premium (#1967)
This commit is contained in:
parent
47c0bc206f
commit
1eb030b2d8
@ -813,6 +813,7 @@ function buildAction(
|
||||
let type: ApiAction['type'] = 'other';
|
||||
let photo: ApiPhoto | undefined;
|
||||
let score: number | undefined;
|
||||
let months: number | undefined;
|
||||
|
||||
const targetUserIds = 'users' in action
|
||||
? action.users && action.users.map((id) => buildApiPeerId(id, 'user'))
|
||||
@ -944,6 +945,19 @@ function buildAction(
|
||||
} else if (action instanceof GramJs.MessageActionWebViewDataSent) {
|
||||
text = 'Notification.WebAppSentData';
|
||||
translationValues.push(action.text);
|
||||
} else if (action instanceof GramJs.MessageActionGiftPremium) {
|
||||
text = isOutgoing ? 'ActionGiftOutbound' : 'ActionGiftInbound';
|
||||
if (isOutgoing) {
|
||||
translationValues.push('%gift_payment_amount%');
|
||||
} else {
|
||||
translationValues.push('%action_origin%', '%gift_payment_amount%');
|
||||
}
|
||||
if (targetPeerId) {
|
||||
targetUserIds.push(targetPeerId);
|
||||
}
|
||||
currency = action.currency;
|
||||
amount = action.amount.toJSNumber();
|
||||
months = action.months;
|
||||
} else {
|
||||
text = 'ChatList.UnsupportedMessage';
|
||||
}
|
||||
@ -965,6 +979,7 @@ function buildAction(
|
||||
call,
|
||||
phoneCall,
|
||||
score,
|
||||
months,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -41,7 +41,7 @@ export {
|
||||
fetchStickerSets, fetchRecentStickers, fetchFavoriteStickers, fetchFeaturedStickers,
|
||||
faveSticker, fetchStickers, fetchSavedGifs, saveGif, searchStickers, installStickerSet, uninstallStickerSet,
|
||||
searchGifs, fetchAnimatedEmojis, fetchStickersForEmoji, fetchEmojiKeywords, fetchAnimatedEmojiEffects,
|
||||
removeRecentSticker, clearRecentStickers,
|
||||
removeRecentSticker, clearRecentStickers, fetchPremiumGifts,
|
||||
} from './symbols';
|
||||
|
||||
export {
|
||||
|
||||
@ -163,6 +163,21 @@ export async function fetchAnimatedEmojiEffects() {
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchPremiumGifts() {
|
||||
const result = await invokeRequest(new GramJs.messages.GetStickerSet({
|
||||
stickerset: new GramJs.InputStickerSetPremiumGifts(),
|
||||
}));
|
||||
|
||||
if (!(result instanceof GramJs.messages.StickerSet)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
set: buildStickerSet(result.set),
|
||||
stickers: processStickerResult(result.documents),
|
||||
};
|
||||
}
|
||||
|
||||
export async function searchStickers({ query, hash = '0' }: { query: string; hash?: string }) {
|
||||
const result = await invokeRequest(new GramJs.messages.SearchStickerSets({
|
||||
q: query,
|
||||
|
||||
@ -235,6 +235,7 @@ export interface ApiAction {
|
||||
call?: Partial<ApiGroupCall>;
|
||||
phoneCall?: PhoneCallAction;
|
||||
score?: number;
|
||||
months?: number;
|
||||
}
|
||||
|
||||
export interface ApiWebPage {
|
||||
|
||||
Binary file not shown.
Binary file not shown.
1
src/assets/mir.svg
Normal file
1
src/assets/mir.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg width="100" height="80" fill="none" xmlns="http://www.w3.org/2000/svg"><path fill="#fff" d="M0 0h100v80H0z"/><g clip-path="url(#a)"><path d="M31.558 29.005v.01c-.01 0-3.126-.01-3.957 2.967-.761 2.73-2.908 10.265-2.967 10.473h-.594s-2.196-7.703-2.967-10.482c-.832-2.978-3.957-2.968-3.957-2.968H10V51.65h7.119V38.2h.593l4.155 13.45h4.943l4.155-13.44h.593v13.44h7.119V29.005h-7.119Zm26.006 0s-2.087.188-3.066 2.374l-5.042 11.076h-.594v-13.45h-7.119V51.65h6.724s2.186-.198 3.165-2.374L56.575 38.2h.593v13.45h7.12V29.005h-6.724Zm9.889 10.284v12.362h7.119v-7.218h7.712a7.703 7.703 0 0 0 7.268-5.14h-22.1v-.004Z" fill="#4DB45E"/><path d="M82.287 29.005H66.454c.791 4.313 4.026 7.772 8.207 8.9.97.262 1.972.395 2.977.395h12.204A7.604 7.604 0 0 0 90 36.718a7.711 7.711 0 0 0-7.713-7.713Z" fill="url(#b)"/></g><defs><linearGradient id="b" x1="66.454" y1="33.653" x2="90" y2="33.653" gradientUnits="userSpaceOnUse"><stop offset=".3" stop-color="#00B4E6"/><stop offset="1" stop-color="#088CCB"/></linearGradient><clipPath id="a"><path fill="#fff" transform="translate(10 29)" d="M0 0h80v22.656H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.1 KiB |
@ -13,6 +13,7 @@ export { default as BotTrustModal } from '../components/main/BotTrustModal';
|
||||
export { default as BotAttachModal } from '../components/main/BotAttachModal';
|
||||
export { default as DeleteFolderDialog } from '../components/main/DeleteFolderDialog';
|
||||
export { default as PremiumMainModal } from '../components/main/premium/PremiumMainModal';
|
||||
export { default as GiftPremiumModal } from '../components/main/premium/GiftPremiumModal';
|
||||
export { default as PremiumLimitReachedModal } from '../components/main/premium/common/PremiumLimitReachedModal';
|
||||
|
||||
export { default as AboutAdsModal } from '../components/common/AboutAdsModal';
|
||||
|
||||
@ -1,17 +1,20 @@
|
||||
const VISA = /^4[0-9]{12}(?:[0-9]{1,3})?$/;
|
||||
const MASTERCARD1 = /^5[1-5][0-9]{11,14}$/;
|
||||
const MASTERCARD2 = /^2[2-7][0-9]{11,14}$/;
|
||||
const MIR = /^220[0-4]/;
|
||||
|
||||
export enum CardType {
|
||||
Default,
|
||||
Visa,
|
||||
Mastercard,
|
||||
Mir,
|
||||
}
|
||||
|
||||
const cards: Record<number, string> = {
|
||||
[CardType.Default]: '',
|
||||
[CardType.Visa]: 'visa',
|
||||
[CardType.Mastercard]: 'mastercard',
|
||||
[CardType.Mir]: 'mir',
|
||||
};
|
||||
|
||||
export function detectCardType(cardNumber: string): number {
|
||||
@ -19,6 +22,9 @@ export function detectCardType(cardNumber: string): number {
|
||||
if (VISA.test(cardNumber)) {
|
||||
return CardType.Visa;
|
||||
}
|
||||
if (MIR.test(cardNumber)) {
|
||||
return CardType.Mir;
|
||||
}
|
||||
if (MASTERCARD1.test(cardNumber) || MASTERCARD2.test(cardNumber)) {
|
||||
return CardType.Mastercard;
|
||||
}
|
||||
|
||||
@ -55,6 +55,15 @@ export function renderActionMessageText(
|
||||
if (translationKey.includes('ScoredInGame')) { // Translation hack for games
|
||||
unprocessed = unprocessed.replace('un1', '%action_origin%').replace('un2', '%message%');
|
||||
}
|
||||
if (translationKey === 'ActionGiftOutbound') { // Translation hack for Premium Gift
|
||||
unprocessed = unprocessed.replace('un2', '%gift_payment_amount%').replace(/\*\*/g, '');
|
||||
}
|
||||
if (translationKey === 'ActionGiftInbound') { // Translation hack for Premium Gift
|
||||
unprocessed = unprocessed
|
||||
.replace('un1', '%action_origin%')
|
||||
.replace('un2', '%gift_payment_amount%')
|
||||
.replace(/\*\*/g, '');
|
||||
}
|
||||
let processed: TextPart[];
|
||||
|
||||
if (unprocessed.includes('%payment_amount%')) {
|
||||
@ -80,6 +89,16 @@ export function renderActionMessageText(
|
||||
unprocessed = processed.pop() as string;
|
||||
content.push(...processed);
|
||||
|
||||
if (unprocessed.includes('%gift_payment_amount%')) {
|
||||
processed = processPlaceholder(
|
||||
unprocessed,
|
||||
'%gift_payment_amount%',
|
||||
formatCurrency(amount!, currency!, lang.code),
|
||||
);
|
||||
unprocessed = processed.pop() as string;
|
||||
content.push(...processed);
|
||||
}
|
||||
|
||||
if (unprocessed.includes('%score%')) {
|
||||
processed = processPlaceholder(
|
||||
unprocessed,
|
||||
|
||||
@ -169,6 +169,7 @@ const Main: FC<StateProps> = ({
|
||||
loadCountryList,
|
||||
loadAvailableReactions,
|
||||
loadStickerSets,
|
||||
loadPremiumGifts,
|
||||
loadAddedStickers,
|
||||
loadFavoriteStickers,
|
||||
ensureTimeFormat,
|
||||
@ -206,10 +207,12 @@ const Main: FC<StateProps> = ({
|
||||
loadEmojiKeywords({ language: BASE_EMOJI_KEYWORD_LANG });
|
||||
loadAttachMenuBots();
|
||||
loadContactList();
|
||||
loadPremiumGifts();
|
||||
}
|
||||
}, [
|
||||
lastSyncTime, loadAnimatedEmojis, loadEmojiKeywords, loadNotificationExceptions, loadNotificationSettings,
|
||||
loadTopInlineBots, updateIsOnline, loadAvailableReactions, loadAppConfig, loadAttachMenuBots, loadContactList,
|
||||
loadPremiumGifts,
|
||||
]);
|
||||
|
||||
// Language-based API calls
|
||||
|
||||
110
src/components/main/premium/GiftOption.module.scss
Normal file
110
src/components/main/premium/GiftOption.module.scss
Normal file
@ -0,0 +1,110 @@
|
||||
.wrapper {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding-inline: 4.5rem 1rem;
|
||||
margin-bottom: 1.5rem;
|
||||
border-radius: var(--border-radius-default);
|
||||
background: var(--color-background);
|
||||
box-shadow: 0 0 0 0.0625rem var(--color-borders-input);
|
||||
|
||||
cursor: pointer;
|
||||
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
.active {
|
||||
box-shadow: 0 0 0 0.125rem var(--color-primary);
|
||||
}
|
||||
|
||||
.input {
|
||||
position: absolute;
|
||||
z-index: var(--z-below);
|
||||
opacity: 0;
|
||||
|
||||
&:checked ~ .content {
|
||||
&::before {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
&::after {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
display: grid;
|
||||
grid-template-areas: "left_top right" "left_bottom right";
|
||||
grid-template-columns: 1fr auto;
|
||||
justify-content: start;
|
||||
padding: 0.5rem 0;
|
||||
gap: 0.25rem;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
inset-inline-start: 1.0625rem;
|
||||
top: 50%;
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
|
||||
&::before {
|
||||
border: 2px solid var(--color-borders-input);
|
||||
border-radius: 50%;
|
||||
background-color: var(--color-background);
|
||||
opacity: 1;
|
||||
transition: border-color 0.1s ease, opacity 0.1s ease;
|
||||
}
|
||||
|
||||
&::after {
|
||||
inset-inline-start: 1.375rem;
|
||||
width: 0.625rem;
|
||||
height: 0.625rem;
|
||||
border-radius: 50%;
|
||||
background: var(--color-primary);
|
||||
opacity: 0;
|
||||
transition: opacity 0.1s ease;
|
||||
}
|
||||
}
|
||||
|
||||
.month {
|
||||
grid-area: left_top;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.perMonth {
|
||||
grid-area: left_bottom;
|
||||
align-self: end;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
margin-inline-end: auto;
|
||||
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
|
||||
@media (max-width: 450px) {
|
||||
flex-direction: column-reverse;
|
||||
}
|
||||
}
|
||||
|
||||
.amount {
|
||||
grid-area: right;
|
||||
align-self: center;
|
||||
justify-self: end;
|
||||
padding-inline-start: 1.5rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.discount {
|
||||
color: var(--color-white);
|
||||
background: var(--color-primary);
|
||||
border-radius: var(--border-radius-default-small);
|
||||
padding: 0 0.5rem;
|
||||
unicode-bidi: plaintext;
|
||||
margin-inline-end: 0.5rem;
|
||||
align-self: baseline;
|
||||
}
|
||||
64
src/components/main/premium/GiftOption.tsx
Normal file
64
src/components/main/premium/GiftOption.tsx
Normal file
@ -0,0 +1,64 @@
|
||||
import type { ChangeEvent } from 'react';
|
||||
import React, { memo, useCallback, useMemo } from '../../../lib/teact/teact';
|
||||
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import type { ApiPremiumGiftOption } from '../../../api/types';
|
||||
|
||||
import { formatCurrency } from '../../../util/formatCurrency';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
|
||||
import styles from './GiftOption.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
option: ApiPremiumGiftOption;
|
||||
checked?: boolean;
|
||||
fullMonthlyAmount?: number;
|
||||
onChange: (month: number) => void;
|
||||
};
|
||||
|
||||
const GiftOption: FC<OwnProps> = ({
|
||||
option, checked, fullMonthlyAmount, onChange,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
|
||||
const { months, amount, currency } = option;
|
||||
const perMonth = Math.floor(amount / months);
|
||||
|
||||
const discount = useMemo(() => {
|
||||
return fullMonthlyAmount && fullMonthlyAmount > perMonth
|
||||
? Math.ceil(100 - perMonth / (fullMonthlyAmount / 100))
|
||||
: undefined;
|
||||
}, [fullMonthlyAmount, perMonth]);
|
||||
|
||||
const handleChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.checked) {
|
||||
onChange(months);
|
||||
}
|
||||
}, [months, onChange]);
|
||||
|
||||
return (
|
||||
<label className={buildClassName(styles.wrapper, checked && styles.active)} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
<input
|
||||
className={styles.input}
|
||||
type="radio"
|
||||
name="gift_option"
|
||||
value={months}
|
||||
checked={checked}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
<div className={styles.content}>
|
||||
<div className={styles.month}>{lang('Months', months)}</div>
|
||||
<div className={styles.perMonth}>
|
||||
{lang('PricePerMonth', formatCurrency(perMonth, currency, lang.code))}
|
||||
{discount && (
|
||||
<span className={styles.discount} title={lang('GiftDiscount')}> −{discount}% </span>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.amount}>{formatCurrency(amount, currency, lang.code)}</div>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(GiftOption);
|
||||
17
src/components/main/premium/GiftPremiumModal.async.tsx
Normal file
17
src/components/main/premium/GiftPremiumModal.async.tsx
Normal file
@ -0,0 +1,17 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { memo } from '../../../lib/teact/teact';
|
||||
import { Bundles } from '../../../util/moduleLoader';
|
||||
|
||||
import type { OwnProps } from './GiftPremiumModal';
|
||||
|
||||
import useModuleLoader from '../../../hooks/useModuleLoader';
|
||||
|
||||
const GiftPremiumModalAsync: FC<OwnProps> = (props) => {
|
||||
const { isOpen } = props;
|
||||
const GiftPremiumModal = useModuleLoader(Bundles.Extra, 'GiftPremiumModal', !isOpen);
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return GiftPremiumModal ? <GiftPremiumModal {...props} /> : undefined;
|
||||
};
|
||||
|
||||
export default memo(GiftPremiumModalAsync);
|
||||
44
src/components/main/premium/GiftPremiumModal.module.scss
Normal file
44
src/components/main/premium/GiftPremiumModal.module.scss
Normal file
@ -0,0 +1,44 @@
|
||||
@media (min-width: 451px) {
|
||||
.modalDialog :global(.modal-dialog) {
|
||||
max-width: 32rem !important;
|
||||
}
|
||||
}
|
||||
|
||||
.closeButton {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
left: 0.5rem;
|
||||
}
|
||||
|
||||
.avatar {
|
||||
margin: 0 auto 1.5rem;
|
||||
}
|
||||
|
||||
.headerText {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.description,
|
||||
.premiumFeatures {
|
||||
text-align: center;
|
||||
margin: 0 auto 2rem;
|
||||
max-width: 25rem;
|
||||
}
|
||||
|
||||
.premiumFeatures {
|
||||
font-size: 0.9375rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.options {
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.button {
|
||||
height: 3rem;
|
||||
background: linear-gradient(88.39deg, #6C93FF -2.56%, #976FFF 51.27%, #DF69D1 107.39%);
|
||||
font-size: 1rem;
|
||||
font-weight: 600;
|
||||
}
|
||||
166
src/components/main/premium/GiftPremiumModal.tsx
Normal file
166
src/components/main/premium/GiftPremiumModal.tsx
Normal file
@ -0,0 +1,166 @@
|
||||
import React, {
|
||||
memo, useCallback, useEffect, useMemo, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import type { ApiPremiumGiftOption, ApiUser } from '../../../api/types';
|
||||
|
||||
import { formatCurrency } from '../../../util/formatCurrency';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
import { getUserFirstOrLastName } from '../../../global/helpers';
|
||||
import { selectUser } from '../../../global/selectors';
|
||||
|
||||
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
|
||||
import Modal from '../../ui/Modal';
|
||||
import Button from '../../ui/Button';
|
||||
import Link from '../../ui/Link';
|
||||
import Avatar from '../../common/Avatar';
|
||||
import GiftOption from './GiftOption';
|
||||
|
||||
import styles from './GiftPremiumModal.module.scss';
|
||||
|
||||
export type OwnProps = {
|
||||
isOpen?: boolean;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
user?: ApiUser;
|
||||
gifts?: ApiPremiumGiftOption[];
|
||||
monthlyCurrency?: string;
|
||||
monthlyAmount?: number;
|
||||
};
|
||||
|
||||
const GiftPremiumModal: FC<OwnProps & StateProps> = ({
|
||||
isOpen, user, gifts, monthlyCurrency, monthlyAmount,
|
||||
}) => {
|
||||
const { openPremiumModal, closeGiftPremiumModal, openUrl } = getActions();
|
||||
|
||||
const lang = useLang();
|
||||
const renderedUser = useCurrentOrPrev(user, true);
|
||||
const renderedGifts = useCurrentOrPrev(gifts, true);
|
||||
const [selectedOption, setSelectedOption] = useState<number | undefined>();
|
||||
const firstGift = renderedGifts?.[0];
|
||||
const fullMonthlyAmount = useMemo(() => {
|
||||
if (!renderedGifts || renderedGifts.length === 0 || !firstGift) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const cheaperGift = renderedGifts.reduce((acc, gift) => {
|
||||
return gift.amount < firstGift?.amount ? gift : firstGift;
|
||||
}, firstGift);
|
||||
|
||||
return cheaperGift.currency === monthlyCurrency && monthlyAmount
|
||||
? monthlyAmount
|
||||
: Math.floor(cheaperGift.amount / cheaperGift.months);
|
||||
}, [firstGift, renderedGifts, monthlyAmount, monthlyCurrency]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setSelectedOption(firstGift?.months);
|
||||
}
|
||||
}, [firstGift?.months, isOpen]);
|
||||
|
||||
const selectedGift = useMemo(() => {
|
||||
return renderedGifts?.find((gift) => gift.months === selectedOption);
|
||||
}, [renderedGifts, selectedOption]);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (!selectedGift) {
|
||||
return;
|
||||
}
|
||||
|
||||
closeGiftPremiumModal();
|
||||
openUrl({ url: selectedGift.botUrl });
|
||||
}, [closeGiftPremiumModal, openUrl, selectedGift]);
|
||||
|
||||
const handlePremiumClick = useCallback(() => {
|
||||
openPremiumModal();
|
||||
}, [openPremiumModal]);
|
||||
|
||||
function renderPremiumFeaturesLink() {
|
||||
const info = lang('GiftPremiumListFeaturesAndTerms');
|
||||
// Translation hack for rendering component inside string
|
||||
const parts = info.match(/([^*]*)\*([^*]+)\*(.*)/);
|
||||
|
||||
if (!parts || parts.length < 4) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<p className={styles.premiumFeatures}>
|
||||
{parts[1]}
|
||||
<Link isPrimary onClick={handlePremiumClick}>{parts[2]}</Link>
|
||||
{parts[3]}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onClose={closeGiftPremiumModal}
|
||||
isOpen={isOpen}
|
||||
className={styles.modalDialog}
|
||||
>
|
||||
<div className="custom-scroll">
|
||||
<Button
|
||||
round
|
||||
size="smaller"
|
||||
className={styles.closeButton}
|
||||
color="translucent"
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => closeGiftPremiumModal()}
|
||||
ariaLabel={lang('Close')}
|
||||
>
|
||||
<i className="icon-close" />
|
||||
</Button>
|
||||
<Avatar user={renderedUser} size="jumbo" className={styles.avatar} />
|
||||
<h2 className={styles.headerText}>
|
||||
{lang('GiftTelegramPremiumTitle')}
|
||||
</h2>
|
||||
<p className={styles.description}>
|
||||
{renderText(
|
||||
lang('GiftTelegramPremiumDescription', getUserFirstOrLastName(renderedUser)),
|
||||
['emoji', 'simple_markdown'],
|
||||
)}
|
||||
</p>
|
||||
|
||||
<div className={styles.options}>
|
||||
{renderedGifts?.map((gift) => (
|
||||
<GiftOption
|
||||
key={gift.amount}
|
||||
option={gift}
|
||||
fullMonthlyAmount={fullMonthlyAmount}
|
||||
checked={gift.months === selectedOption}
|
||||
onChange={setSelectedOption}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{renderPremiumFeaturesLink()}
|
||||
</div>
|
||||
|
||||
<Button className={styles.button} isShiny disabled={!selectedOption} onClick={handleSubmit}>
|
||||
{lang(
|
||||
'GiftSubscriptionFor',
|
||||
selectedGift && formatCurrency(Number(selectedGift.amount), selectedGift.currency, lang.code),
|
||||
)}
|
||||
</Button>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>((global): StateProps => {
|
||||
const { forUserId, monthlyCurrency, monthlyAmount } = global.giftPremiumModal || {};
|
||||
const user = forUserId ? selectUser(global, forUserId) : undefined;
|
||||
const gifts = user ? user.fullInfo?.premiumGifts : undefined;
|
||||
|
||||
return {
|
||||
user,
|
||||
gifts,
|
||||
monthlyCurrency,
|
||||
monthlyAmount: monthlyAmount ? Number(monthlyAmount) : undefined,
|
||||
};
|
||||
})(GiftPremiumModal));
|
||||
@ -14,11 +14,11 @@
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 14px;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
white-space: pre-wrap;
|
||||
line-height: 20px;
|
||||
min-height: 40px;
|
||||
line-height: 1.25rem;
|
||||
min-height: 2.5rem;
|
||||
}
|
||||
|
||||
.icon {
|
||||
|
||||
@ -4,8 +4,8 @@
|
||||
|
||||
.button {
|
||||
font-weight: 600;
|
||||
font-size: 16px;
|
||||
height: 48px;
|
||||
font-size: 1rem;
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
.button-premium {
|
||||
@ -63,17 +63,17 @@
|
||||
.limits-content {
|
||||
overflow: auto;
|
||||
padding: 1rem;
|
||||
margin-top: 59px;
|
||||
margin-top: 3.6875rem;
|
||||
height: calc(var(--vh) * 55 + 41px);
|
||||
}
|
||||
|
||||
.header {
|
||||
padding-left: 4rem;
|
||||
font-size: 20px;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 500;
|
||||
padding-top: 0.875rem;
|
||||
padding-bottom: 0.875rem;
|
||||
border-bottom: 1px solid var(--color-borders);
|
||||
border-bottom: 0.0625rem solid var(--color-borders);
|
||||
position: absolute;
|
||||
background: var(--color-background);
|
||||
width: 100%;
|
||||
@ -101,7 +101,7 @@
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 20px;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
color: var(--color-text);
|
||||
@ -109,16 +109,16 @@
|
||||
}
|
||||
|
||||
.description {
|
||||
font-size: 16px;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
line-height: 22px;
|
||||
line-height: 1.375rem;
|
||||
text-align: center;
|
||||
color: var(--color-text-secondary);
|
||||
padding: 0 5%;
|
||||
}
|
||||
|
||||
.footer {
|
||||
border-top: 1px solid var(--color-borders);
|
||||
border-top: 0.0625rem solid var(--color-borders);
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
|
||||
@ -26,8 +26,8 @@
|
||||
.button {
|
||||
font-weight: 600;
|
||||
background: var(--premium-gradient);
|
||||
font-size: 16px;
|
||||
height: 48px;
|
||||
font-size: 1rem;
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
.main {
|
||||
@ -46,7 +46,7 @@
|
||||
}
|
||||
|
||||
.header-text {
|
||||
font-size: 24px;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
}
|
||||
@ -57,7 +57,7 @@
|
||||
}
|
||||
|
||||
.list {
|
||||
margin-bottom: 80px;
|
||||
margin-bottom: 5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@ -69,12 +69,12 @@
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--color-borders);
|
||||
border-bottom: 0.0625rem solid var(--color-borders);
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 56px;
|
||||
height: 3.5rem;
|
||||
padding: 0.5rem;
|
||||
background: var(--color-background);
|
||||
transition: 0.25s ease-out transform;
|
||||
@ -92,7 +92,7 @@
|
||||
}
|
||||
|
||||
.premium-header-text {
|
||||
font-size: 20px;
|
||||
font-size: 1.25rem;
|
||||
font-weight: 500;
|
||||
margin: 0 0 0 3rem;
|
||||
}
|
||||
@ -111,7 +111,7 @@
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
background: var(--color-background);
|
||||
border-top: 1px solid var(--color-borders);
|
||||
border-top: 0.0625rem solid var(--color-borders);
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
padding: 1rem;
|
||||
|
||||
@ -65,12 +65,16 @@ export type OwnProps = {
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
currentUserId?: string;
|
||||
promo?: ApiPremiumPromo;
|
||||
isClosing?: boolean;
|
||||
fromUser?: ApiUser;
|
||||
toUser?: ApiUser;
|
||||
initialSection?: string;
|
||||
isPremium?: boolean;
|
||||
isSuccess?: boolean;
|
||||
isGift?: boolean;
|
||||
monthsAmount?: number;
|
||||
limitChannels: number;
|
||||
limitPins: number;
|
||||
limitLinks: number;
|
||||
@ -83,6 +87,7 @@ type StateProps = {
|
||||
|
||||
const PremiumMainModal: FC<OwnProps & StateProps> = ({
|
||||
isOpen,
|
||||
currentUserId,
|
||||
fromUser,
|
||||
promo,
|
||||
initialSection,
|
||||
@ -96,6 +101,9 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
|
||||
premiumBotUsername,
|
||||
isClosing,
|
||||
isSuccess,
|
||||
isGift,
|
||||
toUser,
|
||||
monthsAmount,
|
||||
premiumPromoOrder,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
@ -170,6 +178,42 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
|
||||
|
||||
if (!promo) return undefined;
|
||||
|
||||
function getHeaderText() {
|
||||
if (isGift) {
|
||||
return fromUser?.id === currentUserId
|
||||
? lang('TelegramPremiumUserGiftedPremiumOutboundDialogTitle', [getUserFullName(toUser), monthsAmount])
|
||||
: lang('TelegramPremiumUserGiftedPremiumDialogTitle', [getUserFullName(fromUser), monthsAmount]);
|
||||
}
|
||||
|
||||
return fromUser
|
||||
? lang('TelegramPremiumUserDialogTitle', getUserFullName(fromUser))
|
||||
: lang(isPremium ? 'TelegramPremiumSubscribedTitle' : 'TelegramPremium');
|
||||
}
|
||||
|
||||
function getHeaderDescription() {
|
||||
if (isGift) {
|
||||
return fromUser?.id === currentUserId
|
||||
? lang('TelegramPremiumUserGiftedPremiumOutboundDialogSubtitle', getUserFullName(toUser))
|
||||
: lang('TelegramPremiumUserGiftedPremiumDialogSubtitle');
|
||||
}
|
||||
|
||||
return fromUser
|
||||
? lang('TelegramPremiumUserDialogSubtitle')
|
||||
: lang(isPremium ? 'TelegramPremiumSubscribedSubtitle' : 'TelegramPremiumSubtitle');
|
||||
}
|
||||
|
||||
function renderFooterText() {
|
||||
if (!promo || (isGift && fromUser?.id === currentUserId)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.footerText}>
|
||||
{renderTextWithEntities(promo.statusText, promo.statusEntities)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={styles.root}
|
||||
@ -195,19 +239,10 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
|
||||
</Button>
|
||||
<img className={styles.logo} src={PremiumLogo} alt="" />
|
||||
<h2 className={styles.headerText}>
|
||||
{renderText(
|
||||
fromUser
|
||||
? lang('TelegramPremiumUserDialogTitle', getUserFullName(fromUser))
|
||||
: lang(isPremium ? 'TelegramPremiumSubscribedTitle' : 'TelegramPremium'),
|
||||
['simple_markdown', 'emoji'],
|
||||
)}
|
||||
{renderText(getHeaderText(), ['simple_markdown', 'emoji'])}
|
||||
</h2>
|
||||
<div className={styles.description}>
|
||||
{renderText(
|
||||
lang(fromUser ? 'TelegramPremiumUserDialogSubtitle'
|
||||
: (isPremium ? 'TelegramPremiumSubscribedSubtitle' : 'TelegramPremiumSubtitle')),
|
||||
['simple_markdown'],
|
||||
)}
|
||||
{renderText(getHeaderDescription(), ['simple_markdown', 'emoji'])}
|
||||
</div>
|
||||
<div className={buildClassName(styles.header, isHeaderHidden && styles.hiddenHeader)}>
|
||||
<h2 className={styles.premiumHeaderText}>
|
||||
@ -240,18 +275,7 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
|
||||
{renderText(lang('AboutPremiumDescription2'), ['simple_markdown'])}
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.footerText}>
|
||||
{renderTextWithEntities(
|
||||
promo.statusText,
|
||||
promo.statusEntities,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
)}
|
||||
</div>
|
||||
{renderFooterText()}
|
||||
</div>
|
||||
{!isPremium && (
|
||||
<div className={styles.footer}>
|
||||
@ -280,10 +304,14 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
|
||||
|
||||
export default memo(withGlobal<OwnProps>((global): StateProps => {
|
||||
return {
|
||||
currentUserId: global.currentUserId,
|
||||
promo: global.premiumModal?.promo,
|
||||
isClosing: global.premiumModal?.isClosing,
|
||||
isSuccess: global.premiumModal?.isSuccess,
|
||||
isGift: global.premiumModal?.isGift,
|
||||
monthsAmount: global.premiumModal?.monthsAmount,
|
||||
fromUser: global.premiumModal?.fromUserId ? selectUser(global, global.premiumModal.fromUserId) : undefined,
|
||||
toUser: global.premiumModal?.toUserId ? selectUser(global, global.premiumModal.toUserId) : undefined,
|
||||
initialSection: global.premiumModal?.initialSection,
|
||||
isPremium: selectIsCurrentUserPremium(global),
|
||||
limitChannels: selectPremiumLimit(global, 'channels'),
|
||||
|
||||
@ -2,9 +2,11 @@ import type { FC } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useEffect, useMemo, useRef,
|
||||
} from '../../lib/teact/teact';
|
||||
import { withGlobal } from '../../global';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type { ApiUser, ApiMessage, ApiChat } from '../../api/types';
|
||||
import type {
|
||||
ApiUser, ApiMessage, ApiChat, ApiSticker,
|
||||
} from '../../api/types';
|
||||
import type { FocusDirection } from '../../types';
|
||||
|
||||
import {
|
||||
@ -16,24 +18,27 @@ import {
|
||||
import { getMessageHtmlId, isChatChannel } from '../../global/helpers';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { renderActionMessageText } from '../common/helpers/renderActionMessageText';
|
||||
import { preventMessageInputBlur } from './helpers/preventMessageInputBlur';
|
||||
import useEnsureMessage from '../../hooks/useEnsureMessage';
|
||||
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
import { useOnIntersect } from '../../hooks/useIntersectionObserver';
|
||||
import { useIsIntersecting, useOnIntersect } from '../../hooks/useIntersectionObserver';
|
||||
import useFocusMessage from './message/hooks/useFocusMessage';
|
||||
import useLang from '../../hooks/useLang';
|
||||
|
||||
import ContextMenuContainer from './message/ContextMenuContainer.async';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import useShowTransition from '../../hooks/useShowTransition';
|
||||
import { preventMessageInputBlur } from './helpers/preventMessageInputBlur';
|
||||
|
||||
import ContextMenuContainer from './message/ContextMenuContainer.async';
|
||||
import AnimatedIconFromSticker from '../common/AnimatedIconFromSticker';
|
||||
|
||||
type OwnProps = {
|
||||
message: ApiMessage;
|
||||
observeIntersection?: ObserveFn;
|
||||
observeIntersectionForAnimation?: ObserveFn;
|
||||
isEmbedded?: boolean;
|
||||
appearanceOrder?: number;
|
||||
isLastInList?: boolean;
|
||||
memoFirstUnreadIdRef?: { current: number | undefined };
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
@ -46,6 +51,7 @@ type StateProps = {
|
||||
isFocused: boolean;
|
||||
focusDirection?: FocusDirection;
|
||||
noFocusHighlight?: boolean;
|
||||
premiumGiftSticker?: ApiSticker;
|
||||
};
|
||||
|
||||
const APPEARANCE_DELAY = 10;
|
||||
@ -53,6 +59,7 @@ const APPEARANCE_DELAY = 10;
|
||||
const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||
message,
|
||||
observeIntersection,
|
||||
observeIntersectionForAnimation,
|
||||
isEmbedded,
|
||||
appearanceOrder = 0,
|
||||
isLastInList,
|
||||
@ -65,7 +72,13 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||
isFocused,
|
||||
focusDirection,
|
||||
noFocusHighlight,
|
||||
premiumGiftSticker,
|
||||
memoFirstUnreadIdRef,
|
||||
}) => {
|
||||
const { openPremiumModal, requestConfetti } = getActions();
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
@ -73,10 +86,10 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||
useEnsureMessage(message.chatId, message.replyToMessageId, targetMessage);
|
||||
useFocusMessage(ref, message.chatId, isFocused, focusDirection, noFocusHighlight);
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
const noAppearanceAnimation = appearanceOrder <= 0;
|
||||
const [isShown, markShown] = useFlag(noAppearanceAnimation);
|
||||
const isGift = Boolean(message.content.action?.text.startsWith('ActionGift'));
|
||||
|
||||
useEffect(() => {
|
||||
if (noAppearanceAnimation) {
|
||||
return;
|
||||
@ -84,6 +97,21 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||
|
||||
setTimeout(markShown, appearanceOrder * APPEARANCE_DELAY);
|
||||
}, [appearanceOrder, markShown, noAppearanceAnimation]);
|
||||
|
||||
const isVisible = useIsIntersecting(ref, observeIntersectionForAnimation);
|
||||
|
||||
const shouldShowConfettiRef = useRef((() => {
|
||||
const isUnread = memoFirstUnreadIdRef?.current && message.id >= memoFirstUnreadIdRef.current;
|
||||
return isGift && !message.isOutgoing && isUnread;
|
||||
})());
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisible && shouldShowConfettiRef.current) {
|
||||
shouldShowConfettiRef.current = false;
|
||||
requestConfetti();
|
||||
}
|
||||
}, [isVisible, requestConfetti]);
|
||||
|
||||
const { transitionClassNames } = useShowTransition(isShown, undefined, noAppearanceAnimation, false);
|
||||
|
||||
const targetUsers = useMemo(() => {
|
||||
@ -114,13 +142,41 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||
handleBeforeContextMenu(e);
|
||||
};
|
||||
|
||||
const handlePremiumGiftClick = () => {
|
||||
openPremiumModal({
|
||||
isGift: true,
|
||||
fromUserId: senderUser?.id,
|
||||
toUserId: targetUserIds?.[0],
|
||||
monthsAmount: message.content.action?.months || 0,
|
||||
});
|
||||
};
|
||||
|
||||
if (isEmbedded) {
|
||||
return <span className="embedded-action-message">{content}</span>;
|
||||
}
|
||||
|
||||
function renderGift() {
|
||||
return (
|
||||
<span className="action-message-gift" tabIndex={0} role="button" onClick={handlePremiumGiftClick}>
|
||||
<AnimatedIconFromSticker
|
||||
key={message.id}
|
||||
sticker={premiumGiftSticker}
|
||||
play
|
||||
noLoop
|
||||
nonInteractive
|
||||
/>
|
||||
<strong>{lang('ActionGiftPremiumTitle')}</strong>
|
||||
<span>{lang('ActionGiftPremiumSubtitle', lang('Months', message.content.action?.months, 'i'))}</span>
|
||||
|
||||
<span className="action-message-button">{lang('ActionGiftPremiumView')}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const className = buildClassName(
|
||||
'ActionMessage message-list-item',
|
||||
isFocused && !noFocusHighlight && 'focused',
|
||||
isGift && 'premium-gift',
|
||||
isContextMenuShown && 'has-menu-open',
|
||||
isLastInList && 'last-in-list',
|
||||
transitionClassNames,
|
||||
@ -136,6 +192,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
<span>{content}</span>
|
||||
{isGift && renderGift()}
|
||||
{contextMenuPosition && (
|
||||
<ContextMenuContainer
|
||||
isOpen={isContextMenuOpen}
|
||||
@ -167,6 +224,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
const isChat = chat && (isChatChannel(chat) || userId === message.chatId);
|
||||
const senderUser = !isChat && userId ? selectUser(global, userId) : undefined;
|
||||
const senderChat = isChat ? chat : undefined;
|
||||
const premiumGiftSticker = global.premiumGifts?.stickers?.[0];
|
||||
|
||||
return {
|
||||
usersById,
|
||||
@ -176,6 +234,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
targetUserIds,
|
||||
targetMessage,
|
||||
isFocused,
|
||||
premiumGiftSticker,
|
||||
...(isFocused && { focusDirection, noFocusHighlight }),
|
||||
};
|
||||
},
|
||||
|
||||
@ -11,7 +11,7 @@ import { REPLIES_USER_ID } from '../../config';
|
||||
import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment';
|
||||
import { disableScrolling, enableScrolling } from '../../util/scrollLock';
|
||||
import {
|
||||
selectChat, selectNotifySettings, selectNotifyExceptions, selectUser, selectChatBot,
|
||||
selectChat, selectNotifySettings, selectNotifyExceptions, selectUser, selectChatBot, selectIsPremiumPurchaseBlocked,
|
||||
} from '../../global/selectors';
|
||||
import {
|
||||
isUserId, getCanDeleteChat, selectIsChatMuted, getCanAddContact, isChatChannel, isChatGroup,
|
||||
@ -73,6 +73,7 @@ type StateProps = {
|
||||
canAddContact?: boolean;
|
||||
canReportChat?: boolean;
|
||||
canDeleteChat?: boolean;
|
||||
canGiftPremium?: boolean;
|
||||
hasLinkedChat?: boolean;
|
||||
};
|
||||
|
||||
@ -98,6 +99,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
isMuted,
|
||||
canReportChat,
|
||||
canDeleteChat,
|
||||
canGiftPremium,
|
||||
hasLinkedChat,
|
||||
canAddContact,
|
||||
onSubscribeChannel,
|
||||
@ -116,6 +118,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
openAddContactDialog,
|
||||
requestCall,
|
||||
toggleStatistics,
|
||||
openGiftPremiumModal,
|
||||
} = getActions();
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(true);
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
@ -181,6 +184,11 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
closeMenu();
|
||||
}, [chatId, closeMenu, openLinkedChat]);
|
||||
|
||||
const handleGiftPremiumClick = useCallback(() => {
|
||||
openGiftPremiumModal({ forUserId: chatId });
|
||||
closeMenu();
|
||||
}, [openGiftPremiumModal, chatId, closeMenu]);
|
||||
|
||||
const handleAddContactClick = useCallback(() => {
|
||||
openAddContactDialog({ userId: chatId });
|
||||
closeMenu();
|
||||
@ -358,6 +366,14 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
</MenuItem>
|
||||
)}
|
||||
{botButtons}
|
||||
{canGiftPremium && (
|
||||
<MenuItem
|
||||
icon="gift"
|
||||
onClick={handleGiftPremiumClick}
|
||||
>
|
||||
{lang('GiftPremium')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{canLeave && (
|
||||
<MenuItem
|
||||
destructive
|
||||
@ -402,6 +418,11 @@ export default memo(withGlobal<OwnProps>(
|
||||
const canReportChat = isChatChannel(chat) || isChatGroup(chat) || (user && !user.isSelf);
|
||||
|
||||
const chatBot = chatId !== REPLIES_USER_ID ? selectChatBot(global, chatId) : undefined;
|
||||
const canGiftPremium = Boolean(
|
||||
global.lastSyncTime
|
||||
&& user?.fullInfo?.premiumGifts?.length
|
||||
&& !selectIsPremiumPurchaseBlocked(global),
|
||||
);
|
||||
|
||||
return {
|
||||
chat,
|
||||
@ -410,6 +431,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
canAddContact,
|
||||
canReportChat,
|
||||
canDeleteChat: getCanDeleteChat(chat),
|
||||
canGiftPremium,
|
||||
hasLinkedChat: Boolean(chat?.fullInfo?.linkedChatId),
|
||||
botCommands: chatBot?.fullInfo?.botInfo?.commands,
|
||||
};
|
||||
|
||||
@ -259,6 +259,31 @@
|
||||
}
|
||||
}
|
||||
|
||||
.ActionMessage.premium-gift {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.action-message-gift {
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
line-height: 1rem !important;
|
||||
padding-bottom: 0.75rem !important;
|
||||
margin-top: 0.5rem;
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.action-message-button {
|
||||
display: inline-block;
|
||||
border-radius: var(--border-radius-default);
|
||||
padding: 0.5rem 0.75rem;
|
||||
margin-top: 0.5rem;
|
||||
background-color: var(--pattern-color);
|
||||
}
|
||||
|
||||
.sticky-date {
|
||||
margin-top: 1rem;
|
||||
margin-bottom: 1rem;
|
||||
|
||||
@ -144,6 +144,8 @@ const MessageListContent: FC<OwnProps> = ({
|
||||
key={message.id}
|
||||
message={message}
|
||||
observeIntersection={observeIntersectionForReading}
|
||||
observeIntersectionForAnimation={observeIntersectionForAnimatedStickers}
|
||||
memoFirstUnreadIdRef={memoFirstUnreadIdRef}
|
||||
appearanceOrder={messageCountToAnimate - ++appearanceIndex}
|
||||
isLastInList={isLastInList}
|
||||
/>,
|
||||
|
||||
@ -71,6 +71,7 @@ import UnpinAllMessagesModal from '../common/UnpinAllMessagesModal.async';
|
||||
import SeenByModal from '../common/SeenByModal.async';
|
||||
import EmojiInteractionAnimation from './EmojiInteractionAnimation.async';
|
||||
import ReactorListModal from './ReactorListModal.async';
|
||||
import GiftPremiumModal from '../main/premium/GiftPremiumModal.async';
|
||||
|
||||
import './MiddleColumn.scss';
|
||||
import styles from './MiddleColumn.module.scss';
|
||||
@ -98,6 +99,7 @@ type StateProps = {
|
||||
isSelectModeActive?: boolean;
|
||||
isSeenByModalOpen: boolean;
|
||||
isReactorListModalOpen: boolean;
|
||||
isGiftPremiumModalOpen?: boolean;
|
||||
animationLevel?: number;
|
||||
shouldSkipHistoryAnimations?: boolean;
|
||||
currentTransitionKey: number;
|
||||
@ -140,6 +142,7 @@ const MiddleColumn: FC<StateProps> = ({
|
||||
isSelectModeActive,
|
||||
isSeenByModalOpen,
|
||||
isReactorListModalOpen,
|
||||
isGiftPremiumModalOpen,
|
||||
animationLevel,
|
||||
shouldSkipHistoryAnimations,
|
||||
currentTransitionKey,
|
||||
@ -551,6 +554,7 @@ const MiddleColumn: FC<StateProps> = ({
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<GiftPremiumModal isOpen={isGiftPremiumModalOpen} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -580,6 +584,7 @@ export default memo(withGlobal(
|
||||
isSelectModeActive: selectIsInSelectMode(global),
|
||||
isSeenByModalOpen: Boolean(global.seenByModal),
|
||||
isReactorListModalOpen: Boolean(global.reactorModal),
|
||||
isGiftPremiumModalOpen: global.giftPremiumModal?.isOpen,
|
||||
animationLevel: global.settings.byKey.animationLevel,
|
||||
currentTransitionKey: Math.max(0, messageLists.length - 1),
|
||||
activeEmojiInteractions,
|
||||
|
||||
@ -14,6 +14,7 @@ import './CardInput.scss';
|
||||
|
||||
import mastercardIconPath from '../../assets/mastercard.svg';
|
||||
import visaIconPath from '../../assets/visa.svg';
|
||||
import mirIconPath from '../../assets/mir.svg';
|
||||
|
||||
const CARD_NUMBER_MAX_LENGTH = 23;
|
||||
|
||||
@ -73,6 +74,8 @@ function getCardIcon(cardType: CardType) {
|
||||
return <img src={mastercardIconPath} alt="" />;
|
||||
case CardType.Visa:
|
||||
return <img src={visaIconPath} alt="" />;
|
||||
case CardType.Mir:
|
||||
return <img src={mirIconPath} alt="" />;
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
|
||||
19
src/components/ui/Link.module.scss
Normal file
19
src/components/ui/Link.module.scss
Normal file
@ -0,0 +1,19 @@
|
||||
.link {
|
||||
color: inherit;
|
||||
|
||||
&:hover {
|
||||
color: inherit;
|
||||
|
||||
&:global(.GroupCallLink) {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.isPrimary {
|
||||
color: var(--color-primary);
|
||||
|
||||
&:hover {
|
||||
color: var(--color-primary);
|
||||
}
|
||||
}
|
||||
@ -1,11 +0,0 @@
|
||||
.Link {
|
||||
color: inherit;
|
||||
|
||||
&:hover {
|
||||
color: inherit;
|
||||
|
||||
&.GroupCallLink {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,17 +3,18 @@ import React, { useCallback } from '../../lib/teact/teact';
|
||||
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
|
||||
import './Link.scss';
|
||||
import styles from './Link.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
isRtl?: boolean;
|
||||
isPrimary?: boolean;
|
||||
onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void;
|
||||
};
|
||||
|
||||
const Link: FC<OwnProps> = ({
|
||||
children, className, isRtl, onClick,
|
||||
children, isPrimary, className, isRtl, onClick,
|
||||
}) => {
|
||||
const handleClick = useCallback((e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
e.preventDefault();
|
||||
@ -23,7 +24,7 @@ const Link: FC<OwnProps> = ({
|
||||
return (
|
||||
<a
|
||||
href="#"
|
||||
className={buildClassName('Link', className)}
|
||||
className={buildClassName(styles.link, className, isPrimary && styles.isPrimary)}
|
||||
dir={isRtl ? 'rtl' : 'auto'}
|
||||
onClick={onClick ? handleClick : undefined}
|
||||
>
|
||||
|
||||
@ -642,6 +642,10 @@ addActionHandler('openTelegramLink', (global, actions, payload) => {
|
||||
chatId,
|
||||
messageId,
|
||||
});
|
||||
} else if (part1.startsWith('$')) {
|
||||
actions.openInvoice({
|
||||
slug: part1.substring(1),
|
||||
});
|
||||
} else if (part1 === 'invoice') {
|
||||
actions.openInvoice({
|
||||
slug: part2,
|
||||
|
||||
@ -376,7 +376,9 @@ addActionHandler('closePremiumModal', (global, actions, payload) => {
|
||||
});
|
||||
|
||||
addActionHandler('openPremiumModal', async (global, actions, payload) => {
|
||||
const { initialSection, fromUserId, isSuccess } = payload || {};
|
||||
const {
|
||||
initialSection, fromUserId, isSuccess, isGift, monthsAmount, toUserId,
|
||||
} = payload || {};
|
||||
|
||||
actions.loadPremiumStickers();
|
||||
|
||||
@ -393,7 +395,36 @@ addActionHandler('openPremiumModal', async (global, actions, payload) => {
|
||||
initialSection,
|
||||
isOpen: true,
|
||||
fromUserId,
|
||||
toUserId,
|
||||
isGift,
|
||||
monthsAmount,
|
||||
isSuccess,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
addActionHandler('openGiftPremiumModal', async (global, actions, payload) => {
|
||||
const { forUserId } = payload || {};
|
||||
const result = await callApi('fetchPremiumPromo');
|
||||
if (!result) return;
|
||||
|
||||
global = getGlobal();
|
||||
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
|
||||
|
||||
setGlobal({
|
||||
...global,
|
||||
giftPremiumModal: {
|
||||
isOpen: true,
|
||||
forUserId,
|
||||
monthlyCurrency: result.promo.currency,
|
||||
monthlyAmount: result.promo.monthlyAmount,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
addActionHandler('closeGiftPremiumModal', (global) => {
|
||||
setGlobal({
|
||||
...global,
|
||||
giftPremiumModal: { isOpen: false },
|
||||
});
|
||||
});
|
||||
|
||||
@ -109,6 +109,20 @@ addActionHandler('loadFeaturedStickers', (global) => {
|
||||
void loadFeaturedStickers(hash);
|
||||
});
|
||||
|
||||
addActionHandler('loadPremiumGifts', async () => {
|
||||
const stickerSet = await callApi('fetchPremiumGifts');
|
||||
if (!stickerSet) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { set, stickers } = stickerSet;
|
||||
|
||||
setGlobal({
|
||||
...getGlobal(),
|
||||
premiumGifts: { ...set, stickers },
|
||||
});
|
||||
});
|
||||
|
||||
addActionHandler('loadStickers', (global, actions, payload) => {
|
||||
const { stickerSetId, stickerSetShortName } = payload!;
|
||||
let { stickerSetAccessHash } = payload!;
|
||||
|
||||
@ -7,16 +7,16 @@ addActionHandler('apiUpdate', (global, actions, update) => {
|
||||
switch (update['@type']) {
|
||||
case 'updatePaymentStateCompleted': {
|
||||
const { inputInvoice } = global.payment;
|
||||
if (update.slug && inputInvoice && 'slug' in inputInvoice && inputInvoice.slug !== update.slug) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// On the production host, the payment frame receives a message with the payment event,
|
||||
// after which the payment form closes. In other cases, the payment form must be closed manually.
|
||||
if (!IS_PRODUCTION_HOST) {
|
||||
global = clearPayment(global);
|
||||
}
|
||||
|
||||
if (update.slug && inputInvoice && 'slug' in inputInvoice && inputInvoice.slug !== update.slug) {
|
||||
return !IS_PRODUCTION_HOST ? global : undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
...global,
|
||||
payment: {
|
||||
|
||||
@ -314,6 +314,7 @@ export type GlobalState = {
|
||||
|
||||
animatedEmojis?: ApiStickerSet;
|
||||
animatedEmojiEffects?: ApiStickerSet;
|
||||
premiumGifts?: ApiStickerSet;
|
||||
emojiKeywords: Partial<Record<LangCode, EmojiKeywords>>;
|
||||
|
||||
gifs: {
|
||||
@ -638,9 +639,19 @@ export type GlobalState = {
|
||||
promo: ApiPremiumPromo;
|
||||
initialSection?: string;
|
||||
fromUserId?: string;
|
||||
toUserId?: string;
|
||||
isGift?: boolean;
|
||||
monthsAmount?: number;
|
||||
isSuccess?: boolean;
|
||||
};
|
||||
|
||||
giftPremiumModal?: {
|
||||
isOpen?: boolean;
|
||||
forUserId?: string;
|
||||
monthlyCurrency?: string;
|
||||
monthlyAmount?: string;
|
||||
};
|
||||
|
||||
transcriptions: Record<string, ApiTranscription>;
|
||||
|
||||
limitReachedModal?: {
|
||||
@ -1062,7 +1073,10 @@ export interface ActionPayloads {
|
||||
openPremiumModal: {
|
||||
initialSection?: string;
|
||||
fromUserId?: string;
|
||||
toUserId?: string;
|
||||
isSuccess?: boolean;
|
||||
isGift?: boolean;
|
||||
monthsAmount?: number;
|
||||
};
|
||||
closePremiumModal: never | {
|
||||
isClosed?: boolean;
|
||||
@ -1073,10 +1087,17 @@ export interface ActionPayloads {
|
||||
messageId: number;
|
||||
};
|
||||
|
||||
loadPremiumGifts: never;
|
||||
loadPremiumStickers: {
|
||||
hash?: string;
|
||||
};
|
||||
|
||||
openGiftPremiumModal: {
|
||||
forUserId?: string;
|
||||
};
|
||||
|
||||
closeGiftPremiumModal: never;
|
||||
|
||||
// Invoice
|
||||
openInvoice: ApiInputInvoice;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -51,6 +51,9 @@
|
||||
.icon-volume-3:before {
|
||||
content: "\e991";
|
||||
}
|
||||
.icon-gift:before {
|
||||
content: "\e9ad";
|
||||
}
|
||||
.icon-sort:before {
|
||||
content: "\e9ac";
|
||||
}
|
||||
|
||||
@ -204,14 +204,14 @@ function processTemplate(template: string, value: any) {
|
||||
const initialValue = translationSlices.shift();
|
||||
|
||||
return translationSlices.reduce((result, str, index) => {
|
||||
return `${result}${String(value[index] || '')}${str}`;
|
||||
return `${result}${String(value[index] ?? '')}${str}`;
|
||||
}, initialValue || '');
|
||||
}
|
||||
|
||||
function processTranslation(langString: ApiLangString | undefined, key: string, value?: any, format?: 'i') {
|
||||
const preferedPluralOption = typeof value === 'number' ? getPluralOption(value) : 'value';
|
||||
const preferredPluralOption = typeof value === 'number' ? getPluralOption(value) : 'value';
|
||||
const template = langString ? (
|
||||
langString[preferedPluralOption] || langString.otherValue || langString.value
|
||||
langString[preferredPluralOption] || langString.otherValue || langString.value
|
||||
) : undefined;
|
||||
if (!template || !template.trim()) {
|
||||
const parts = key.split('.');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user