Gifting: Premium Gifting (#4472)
Co-authored-by: zubiden <19638254+zubiden@users.noreply.github.com>
This commit is contained in:
parent
2dc4e9d6cf
commit
34a3b62089
@ -63,6 +63,7 @@ export interface GramJsAppConfig extends LimitsConfig {
|
|||||||
authorization_autoconfirm_period: number;
|
authorization_autoconfirm_period: number;
|
||||||
giveaway_boosts_per_premium: number;
|
giveaway_boosts_per_premium: number;
|
||||||
giveaway_countries_max: number;
|
giveaway_countries_max: number;
|
||||||
|
boosts_per_sent_gift: number;
|
||||||
// Forums
|
// Forums
|
||||||
topics_pinned_limit: number;
|
topics_pinned_limit: number;
|
||||||
// Stories
|
// Stories
|
||||||
@ -127,6 +128,7 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp
|
|||||||
giveawayAddPeersMax: appConfig.giveaway_add_peers_max,
|
giveawayAddPeersMax: appConfig.giveaway_add_peers_max,
|
||||||
giveawayBoostsPerPremium: appConfig.giveaway_boosts_per_premium,
|
giveawayBoostsPerPremium: appConfig.giveaway_boosts_per_premium,
|
||||||
giveawayCountriesMax: appConfig.giveaway_countries_max,
|
giveawayCountriesMax: appConfig.giveaway_countries_max,
|
||||||
|
boostsPerSentGift: appConfig.boosts_per_sent_gift,
|
||||||
canDisplayAutoarchiveSetting: appConfig.autoarchive_setting_available,
|
canDisplayAutoarchiveSetting: appConfig.autoarchive_setting_available,
|
||||||
limits: {
|
limits: {
|
||||||
uploadMaxFileparts: getLimit(appConfig, 'upload_max_fileparts', 'uploadMaxFileparts'),
|
uploadMaxFileparts: getLimit(appConfig, 'upload_max_fileparts', 'uploadMaxFileparts'),
|
||||||
|
|||||||
@ -189,6 +189,7 @@ export interface ApiAppConfig {
|
|||||||
giveawayAddPeersMax: number;
|
giveawayAddPeersMax: number;
|
||||||
giveawayBoostsPerPremium: number;
|
giveawayBoostsPerPremium: number;
|
||||||
giveawayCountriesMax: number;
|
giveawayCountriesMax: number;
|
||||||
|
boostsPerSentGift: number;
|
||||||
premiumPromoOrder: ApiPremiumSection[];
|
premiumPromoOrder: ApiPremiumSection[];
|
||||||
defaultEmojiStatusesStickerSetId: string;
|
defaultEmojiStatusesStickerSetId: string;
|
||||||
maxUniqueReactions: number;
|
maxUniqueReactions: number;
|
||||||
|
|||||||
@ -18,6 +18,7 @@ export { default as DeleteFolderDialog } from '../components/main/DeleteFolderDi
|
|||||||
export { default as PremiumMainModal } from '../components/main/premium/PremiumMainModal';
|
export { default as PremiumMainModal } from '../components/main/premium/PremiumMainModal';
|
||||||
export { default as GiftPremiumModal } from '../components/main/premium/GiftPremiumModal';
|
export { default as GiftPremiumModal } from '../components/main/premium/GiftPremiumModal';
|
||||||
export { default as GiveawayModal } from '../components/main/premium/GiveawayModal';
|
export { default as GiveawayModal } from '../components/main/premium/GiveawayModal';
|
||||||
|
export { default as PremiumGiftingModal } from '../components/main/premium/PremiumGiftingModal';
|
||||||
export { default as AppendEntityPickerModal } from '../components/main/AppendEntityPickerModal';
|
export { default as AppendEntityPickerModal } from '../components/main/AppendEntityPickerModal';
|
||||||
export { default as PremiumLimitReachedModal } from '../components/main/premium/common/PremiumLimitReachedModal';
|
export { default as PremiumLimitReachedModal } from '../components/main/premium/common/PremiumLimitReachedModal';
|
||||||
export { default as StatusPickerMenu } from '../components/left/main/StatusPickerMenu';
|
export { default as StatusPickerMenu } from '../components/left/main/StatusPickerMenu';
|
||||||
|
|||||||
@ -145,7 +145,7 @@ const UserBirthday = ({
|
|||||||
const canGiftPremium = isToday && !user.isPremium && !user.isSelf && !isPremiumPurchaseBlocked;
|
const canGiftPremium = isToday && !user.isPremium && !user.isSelf && !isPremiumPurchaseBlocked;
|
||||||
|
|
||||||
const handleOpenGiftModal = useLastCallback(() => {
|
const handleOpenGiftModal = useLastCallback(() => {
|
||||||
openGiftPremiumModal({ forUserId: user.id });
|
openGiftPremiumModal({ forUserIds: [user.id] });
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleClick = useLastCallback(() => {
|
const handleClick = useLastCallback(() => {
|
||||||
|
|||||||
@ -104,7 +104,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.settings-main-menu-premium .PremiumIcon {
|
.settings-main-menu-premium .PremiumIcon {
|
||||||
margin-right: 2rem;
|
margin-right: 1.25rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.settings-main-menu {
|
.settings-main-menu {
|
||||||
|
|||||||
@ -5,7 +5,10 @@ import { getActions, withGlobal } from '../../../global';
|
|||||||
import { SettingsScreens } from '../../../types';
|
import { SettingsScreens } from '../../../types';
|
||||||
|
|
||||||
import { FAQ_URL, PRIVACY_URL } from '../../../config';
|
import { FAQ_URL, PRIVACY_URL } from '../../../config';
|
||||||
import { selectIsPremiumPurchaseBlocked } from '../../../global/selectors';
|
import {
|
||||||
|
selectIsGiveawayGiftsPurchaseAvailable,
|
||||||
|
selectIsPremiumPurchaseBlocked,
|
||||||
|
} from '../../../global/selectors';
|
||||||
|
|
||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||||
@ -28,6 +31,7 @@ type StateProps = {
|
|||||||
sessionCount: number;
|
sessionCount: number;
|
||||||
currentUserId?: string;
|
currentUserId?: string;
|
||||||
canBuyPremium?: boolean;
|
canBuyPremium?: boolean;
|
||||||
|
isGiveawayAvailable?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SettingsMain: FC<OwnProps & StateProps> = ({
|
const SettingsMain: FC<OwnProps & StateProps> = ({
|
||||||
@ -37,12 +41,14 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
|||||||
currentUserId,
|
currentUserId,
|
||||||
sessionCount,
|
sessionCount,
|
||||||
canBuyPremium,
|
canBuyPremium,
|
||||||
|
isGiveawayAvailable,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
loadProfilePhotos,
|
loadProfilePhotos,
|
||||||
openPremiumModal,
|
openPremiumModal,
|
||||||
openSupportChat,
|
openSupportChat,
|
||||||
openUrl,
|
openUrl,
|
||||||
|
openPremiumGiftingModal,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const [isSupportDialogOpen, openSupportDialog, closeSupportDialog] = useFlag(false);
|
const [isSupportDialogOpen, openSupportDialog, closeSupportDialog] = useFlag(false);
|
||||||
@ -158,6 +164,16 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
|||||||
{lang('TelegramPremium')}
|
{lang('TelegramPremium')}
|
||||||
</ListItem>
|
</ListItem>
|
||||||
)}
|
)}
|
||||||
|
{isGiveawayAvailable && (
|
||||||
|
<ListItem
|
||||||
|
icon="gift"
|
||||||
|
className="settings-main-menu-premium"
|
||||||
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
|
onClick={() => openPremiumGiftingModal()}
|
||||||
|
>
|
||||||
|
{lang('GiftPremiumGifting')}
|
||||||
|
</ListItem>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="settings-main-menu">
|
<div className="settings-main-menu">
|
||||||
<ListItem
|
<ListItem
|
||||||
@ -196,11 +212,13 @@ const SettingsMain: FC<OwnProps & StateProps> = ({
|
|||||||
export default memo(withGlobal<OwnProps>(
|
export default memo(withGlobal<OwnProps>(
|
||||||
(global): StateProps => {
|
(global): StateProps => {
|
||||||
const { currentUserId } = global;
|
const { currentUserId } = global;
|
||||||
|
const isGiveawayAvailable = selectIsGiveawayGiftsPurchaseAvailable(global);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sessionCount: global.activeSessions.orderedHashes.length,
|
sessionCount: global.activeSessions.orderedHashes.length,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
canBuyPremium: !selectIsPremiumPurchaseBlocked(global),
|
canBuyPremium: !selectIsPremiumPurchaseBlocked(global),
|
||||||
|
isGiveawayAvailable,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(SettingsMain));
|
)(SettingsMain));
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import type { FC } from '../../lib/teact/teact';
|
import type { FC } from '../../lib/teact/teact';
|
||||||
import React, {
|
import React, {
|
||||||
memo, useMemo, useState,
|
memo,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
import { getActions, getGlobal, withGlobal } from '../../global';
|
import { getActions, getGlobal, withGlobal } from '../../global';
|
||||||
|
|
||||||
@ -66,8 +68,7 @@ const AppendEntityPickerModal: FC<OwnProps & StateProps> = ({
|
|||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useFlag();
|
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useFlag();
|
||||||
|
|
||||||
const [selectedChannelIds, setSelectedChannelIds] = useState<string[]>([]);
|
const [selectedIds, setSelectedIds] = useState<string[]>([]);
|
||||||
const [selectedMemberIds, setSelectedMemberIds] = useState<string[]>([]);
|
|
||||||
const [pendingChannelId, setPendingChannelId] = useState<string | undefined>(undefined);
|
const [pendingChannelId, setPendingChannelId] = useState<string | undefined>(undefined);
|
||||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||||
|
|
||||||
@ -127,8 +128,8 @@ const AppendEntityPickerModal: FC<OwnProps & StateProps> = ({
|
|||||||
return isChannel || isSuperGroup;
|
return isChannel || isSuperGroup;
|
||||||
}),
|
}),
|
||||||
false,
|
false,
|
||||||
selectedChannelIds);
|
selectedIds);
|
||||||
}, [channelsIds, lang, searchQuery, selectedChannelIds, isSuperGroup, isChannel]);
|
}, [channelsIds, lang, searchQuery, selectedIds, isSuperGroup, isChannel]);
|
||||||
|
|
||||||
const handleCloseButtonClick = useLastCallback(() => {
|
const handleCloseButtonClick = useLastCallback(() => {
|
||||||
onSubmit([]);
|
onSubmit([]);
|
||||||
@ -136,36 +137,36 @@ const AppendEntityPickerModal: FC<OwnProps & StateProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleSendIdList = useLastCallback(() => {
|
const handleSendIdList = useLastCallback(() => {
|
||||||
onSubmit(entityType === 'members' ? selectedMemberIds : selectedChannelIds);
|
onSubmit(selectedIds);
|
||||||
onClose();
|
onClose();
|
||||||
});
|
});
|
||||||
|
|
||||||
const confirmPrivateLinkChannelSelection = useLastCallback(() => {
|
const confirmPrivateLinkChannelSelection = useLastCallback(() => {
|
||||||
if (pendingChannelId) {
|
if (pendingChannelId) {
|
||||||
setSelectedChannelIds((prevIds) => unique([...prevIds, pendingChannelId]));
|
setSelectedIds((prevIds) => unique([...prevIds, pendingChannelId]));
|
||||||
}
|
}
|
||||||
closeConfirmModal();
|
closeConfirmModal();
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSelectedMembersChange = useLastCallback((newSelectedIds: string[]) => {
|
const handleSelectedMemberIdsChange = useLastCallback((newSelectedIds: string[]) => {
|
||||||
if (newSelectedIds.length > selectionLimit) {
|
if (newSelectedIds.length > selectionLimit) {
|
||||||
showNotification({
|
showNotification({
|
||||||
message: lang('BoostingSelectUpToWarningUsers', selectionLimit),
|
message: lang('BoostingSelectUpToWarningUsers', selectionLimit),
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setSelectedMemberIds(newSelectedIds);
|
setSelectedIds(newSelectedIds);
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleSelectedChannelIdsChange = useLastCallback((newSelectedIds: string[]) => {
|
const handleSelectedChannelIdsChange = useLastCallback((newSelectedIds: string[]) => {
|
||||||
const chatsById = getGlobal().chats.byId;
|
const chatsById = getGlobal().chats.byId;
|
||||||
const newlyAddedIds = newSelectedIds.filter((id) => !selectedChannelIds.includes(id));
|
const newlyAddedIds = newSelectedIds.filter((id) => !selectedIds.includes(id));
|
||||||
const privateLinkChannelId = newlyAddedIds.find((id) => {
|
const privateLinkChannelId = newlyAddedIds.find((id) => {
|
||||||
const chat = chatsById[id];
|
const chat = chatsById[id];
|
||||||
return chat && !isChatPublic(chat);
|
return chat && !isChatPublic(chat);
|
||||||
});
|
});
|
||||||
|
|
||||||
if (selectedChannelIds?.length >= selectionLimit) {
|
if (selectedIds?.length >= selectionLimit) {
|
||||||
showNotification({
|
showNotification({
|
||||||
message: lang('BoostingSelectUpToWarningChannelsPlural', selectionLimit),
|
message: lang('BoostingSelectUpToWarningChannelsPlural', selectionLimit),
|
||||||
});
|
});
|
||||||
@ -176,7 +177,7 @@ const AppendEntityPickerModal: FC<OwnProps & StateProps> = ({
|
|||||||
setPendingChannelId(privateLinkChannelId);
|
setPendingChannelId(privateLinkChannelId);
|
||||||
openConfirmModal();
|
openConfirmModal();
|
||||||
} else {
|
} else {
|
||||||
setSelectedChannelIds(newSelectedIds);
|
setSelectedIds(newSelectedIds);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -218,12 +219,12 @@ const AppendEntityPickerModal: FC<OwnProps & StateProps> = ({
|
|||||||
<Picker
|
<Picker
|
||||||
className={styles.picker}
|
className={styles.picker}
|
||||||
itemIds={entityType === 'members' ? displayedMembersIds : displayedChannelIds}
|
itemIds={entityType === 'members' ? displayedMembersIds : displayedChannelIds}
|
||||||
selectedIds={entityType === 'members' ? selectedMemberIds : selectedChannelIds}
|
selectedIds={selectedIds}
|
||||||
filterValue={searchQuery}
|
filterValue={searchQuery}
|
||||||
filterPlaceholder={lang('Search')}
|
filterPlaceholder={lang('Search')}
|
||||||
searchInputId="new-members-picker-search"
|
searchInputId={`${entityType}-picker-search`}
|
||||||
onSelectedIdsChange={entityType === 'channels'
|
onSelectedIdsChange={entityType === 'channels'
|
||||||
? handleSelectedChannelIdsChange : handleSelectedMembersChange}
|
? handleSelectedChannelIdsChange : handleSelectedMemberIdsChange}
|
||||||
onFilterChange={setSearchQuery}
|
onFilterChange={setSearchQuery}
|
||||||
isSearchable
|
isSearchable
|
||||||
/>
|
/>
|
||||||
@ -261,7 +262,7 @@ export default memo(withGlobal<OwnProps>((global, { chatId, entityType }): State
|
|||||||
members = chatFullInfo.members;
|
members = chatFullInfo.members;
|
||||||
adminMembersById = chatFullInfo.adminMembersById;
|
adminMembersById = chatFullInfo.adminMembersById;
|
||||||
}
|
}
|
||||||
} else if (entityType === 'channels') {
|
} if (entityType === 'channels') {
|
||||||
const chat = chatId ? selectChat(global, chatId) : undefined;
|
const chat = chatId ? selectChat(global, chatId) : undefined;
|
||||||
if (chat) {
|
if (chat) {
|
||||||
isChannel = isChatChannel(chat);
|
isChannel = isChatChannel(chat);
|
||||||
|
|||||||
@ -102,6 +102,7 @@ import NewContactModal from './NewContactModal.async';
|
|||||||
import Notifications from './Notifications.async';
|
import Notifications from './Notifications.async';
|
||||||
import PremiumLimitReachedModal from './premium/common/PremiumLimitReachedModal.async';
|
import PremiumLimitReachedModal from './premium/common/PremiumLimitReachedModal.async';
|
||||||
import GiveawayModal from './premium/GiveawayModal.async';
|
import GiveawayModal from './premium/GiveawayModal.async';
|
||||||
|
import PremiumGiftingModal from './premium/PremiumGiftingModal.async';
|
||||||
import PremiumMainModal from './premium/PremiumMainModal.async';
|
import PremiumMainModal from './premium/PremiumMainModal.async';
|
||||||
import SafeLinkModal from './SafeLinkModal.async';
|
import SafeLinkModal from './SafeLinkModal.async';
|
||||||
|
|
||||||
@ -159,6 +160,7 @@ type StateProps = {
|
|||||||
isReactionPickerOpen: boolean;
|
isReactionPickerOpen: boolean;
|
||||||
isAppendModalOpen?: boolean;
|
isAppendModalOpen?: boolean;
|
||||||
isGiveawayModalOpen?: boolean;
|
isGiveawayModalOpen?: boolean;
|
||||||
|
isPremiumGiftingModalOpen?: boolean;
|
||||||
isCurrentUserPremium?: boolean;
|
isCurrentUserPremium?: boolean;
|
||||||
chatlistModal?: TabState['chatlistModal'];
|
chatlistModal?: TabState['chatlistModal'];
|
||||||
boostModal?: TabState['boostModal'];
|
boostModal?: TabState['boostModal'];
|
||||||
@ -218,6 +220,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
|||||||
urlAuth,
|
urlAuth,
|
||||||
isPremiumModalOpen,
|
isPremiumModalOpen,
|
||||||
isGiveawayModalOpen,
|
isGiveawayModalOpen,
|
||||||
|
isPremiumGiftingModalOpen,
|
||||||
isPaymentModalOpen,
|
isPaymentModalOpen,
|
||||||
isReceiptModalOpen,
|
isReceiptModalOpen,
|
||||||
isReactionPickerOpen,
|
isReactionPickerOpen,
|
||||||
@ -603,6 +606,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
|||||||
<MessageListHistoryHandler />
|
<MessageListHistoryHandler />
|
||||||
{isPremiumModalOpen && <PremiumMainModal isOpen={isPremiumModalOpen} />}
|
{isPremiumModalOpen && <PremiumMainModal isOpen={isPremiumModalOpen} />}
|
||||||
{isGiveawayModalOpen && <GiveawayModal isOpen={isGiveawayModalOpen} />}
|
{isGiveawayModalOpen && <GiveawayModal isOpen={isGiveawayModalOpen} />}
|
||||||
|
{isPremiumGiftingModalOpen && <PremiumGiftingModal isOpen={isPremiumGiftingModalOpen} />}
|
||||||
<PremiumLimitReachedModal limit={limitReached} />
|
<PremiumLimitReachedModal limit={limitReached} />
|
||||||
<PaymentModal isOpen={isPaymentModalOpen} onClose={closePaymentModal} />
|
<PaymentModal isOpen={isPaymentModalOpen} onClose={closePaymentModal} />
|
||||||
<ReceiptModal isOpen={isReceiptModalOpen} onClose={clearReceipt} />
|
<ReceiptModal isOpen={isReceiptModalOpen} onClose={clearReceipt} />
|
||||||
@ -646,6 +650,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
ratingPhoneCall,
|
ratingPhoneCall,
|
||||||
premiumModal,
|
premiumModal,
|
||||||
giveawayModal,
|
giveawayModal,
|
||||||
|
giftingModal,
|
||||||
isMasterTab,
|
isMasterTab,
|
||||||
payment,
|
payment,
|
||||||
limitReachedModal,
|
limitReachedModal,
|
||||||
@ -712,6 +717,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
||||||
isPremiumModalOpen: premiumModal?.isOpen,
|
isPremiumModalOpen: premiumModal?.isOpen,
|
||||||
isGiveawayModalOpen: giveawayModal?.isOpen,
|
isGiveawayModalOpen: giveawayModal?.isOpen,
|
||||||
|
isPremiumGiftingModalOpen: giftingModal?.isOpen,
|
||||||
limitReached: limitReachedModal?.limit,
|
limitReached: limitReachedModal?.limit,
|
||||||
isPaymentModalOpen: payment.isPaymentModalOpen,
|
isPaymentModalOpen: payment.isPaymentModalOpen,
|
||||||
isReceiptModalOpen: Boolean(payment.receipt),
|
isReceiptModalOpen: Boolean(payment.receipt),
|
||||||
|
|||||||
@ -10,13 +10,16 @@
|
|||||||
left: 0.5rem;
|
left: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.avatar {
|
.avatars {
|
||||||
margin: 0 auto 1.5rem;
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.headerText {
|
.center {
|
||||||
font-size: 1.5rem;
|
|
||||||
font-weight: 500;
|
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -36,9 +39,9 @@
|
|||||||
margin-bottom: 2.5rem;
|
margin-bottom: 2.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.button {
|
.boostIcon {
|
||||||
height: 3rem;
|
color: var(--color-primary);
|
||||||
background: linear-gradient(88.39deg, #6C93FF -2.56%, #976FFF 51.27%, #DF69D1 107.39%);
|
vertical-align: middle;
|
||||||
font-size: 1rem;
|
line-height: 1.5;
|
||||||
font-weight: 600;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,23 +1,28 @@
|
|||||||
import type { FC } from '../../../lib/teact/teact';
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
import React, {
|
import React, {
|
||||||
memo, useCallback, useEffect, useMemo, useState,
|
memo, useEffect, useMemo, useRef,
|
||||||
|
useState,
|
||||||
} from '../../../lib/teact/teact';
|
} from '../../../lib/teact/teact';
|
||||||
import { getActions, withGlobal } from '../../../global';
|
import { getActions, getGlobal, withGlobal } from '../../../global';
|
||||||
|
|
||||||
import type { ApiPremiumGiftOption, ApiUser } from '../../../api/types';
|
import type {
|
||||||
|
ApiPremiumGiftCodeOption,
|
||||||
|
} from '../../../api/types';
|
||||||
|
|
||||||
import { getUserFirstOrLastName } from '../../../global/helpers';
|
import { BOOST_PER_SENT_GIFT } from '../../../config';
|
||||||
|
import { getUserFullName } from '../../../global/helpers';
|
||||||
import {
|
import {
|
||||||
selectTabState,
|
selectTabState,
|
||||||
selectUser,
|
|
||||||
selectUserFullInfo,
|
|
||||||
} from '../../../global/selectors';
|
} from '../../../global/selectors';
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import { formatCurrency } from '../../../util/formatCurrency';
|
import { formatCurrency } from '../../../util/formatCurrency';
|
||||||
import renderText from '../../common/helpers/renderText';
|
import renderText from '../../common/helpers/renderText';
|
||||||
|
|
||||||
import useLang from '../../../hooks/useLang';
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
|
|
||||||
import Avatar from '../../common/Avatar';
|
import AvatarList from '../../common/AvatarList';
|
||||||
|
import Icon from '../../common/Icon';
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
import Link from '../../ui/Link';
|
import Link from '../../ui/Link';
|
||||||
import Modal from '../../ui/Modal';
|
import Modal from '../../ui/Modal';
|
||||||
@ -30,55 +35,124 @@ export type OwnProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
user?: ApiUser;
|
isCompleted?: boolean;
|
||||||
gifts?: ApiPremiumGiftOption[];
|
gifts?: ApiPremiumGiftCodeOption[] | undefined;
|
||||||
monthlyCurrency?: string;
|
forUserIds?: string[];
|
||||||
monthlyAmount?: number;
|
boostPerSentGift?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const GiftPremiumModal: FC<OwnProps & StateProps> = ({
|
const GiftPremiumModal: FC<OwnProps & StateProps> = ({
|
||||||
isOpen,
|
isOpen,
|
||||||
user,
|
isCompleted,
|
||||||
gifts,
|
gifts,
|
||||||
|
boostPerSentGift = BOOST_PER_SENT_GIFT,
|
||||||
|
forUserIds,
|
||||||
}) => {
|
}) => {
|
||||||
const { openPremiumModal, closeGiftPremiumModal, openUrl } = getActions();
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const dialogRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const {
|
||||||
|
openPremiumModal, closeGiftPremiumModal, openInvoice, requestConfetti,
|
||||||
|
} = getActions();
|
||||||
|
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
const [selectedOption, setSelectedOption] = useState<number | undefined>();
|
const [selectedMonthOption, setSelectedMonthOption] = useState<number | undefined>();
|
||||||
const fullMonthlyAmount = useMemo(() => {
|
|
||||||
if (!gifts?.length) {
|
const selectedUserQuantity = forUserIds && forUserIds.length * boostPerSentGift;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (forUserIds?.length) {
|
||||||
|
setSelectedMonthOption(gifts?.[0].months);
|
||||||
|
}
|
||||||
|
}, [gifts, forUserIds]);
|
||||||
|
|
||||||
|
const giftingUserList = useMemo(() => {
|
||||||
|
const usersById = getGlobal().users.byId;
|
||||||
|
return forUserIds?.map((userId) => usersById[userId]).filter(Boolean);
|
||||||
|
}, [forUserIds]);
|
||||||
|
|
||||||
|
const selectedGift = useMemo(() => {
|
||||||
|
return gifts?.find((gift) => gift.months === selectedMonthOption && gift.users === forUserIds?.length);
|
||||||
|
}, [gifts, selectedMonthOption, forUserIds?.length]);
|
||||||
|
|
||||||
|
const filteredGifts = useMemo(() => {
|
||||||
|
return gifts?.filter((gift) => gift.users
|
||||||
|
=== forUserIds?.length);
|
||||||
|
}, [gifts, forUserIds?.length]);
|
||||||
|
|
||||||
|
const fullMonthlyGiftAmount = useMemo(() => {
|
||||||
|
if (!filteredGifts?.length) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const basicGift = gifts.reduce((acc, gift) => {
|
const basicGift = filteredGifts.reduce((acc, gift) => {
|
||||||
return gift.months < acc.months ? gift : acc;
|
return gift.amount < acc.amount ? gift : acc;
|
||||||
});
|
});
|
||||||
|
|
||||||
return Math.floor(basicGift.amount / basicGift.months);
|
return Math.floor(basicGift.amount / basicGift.months);
|
||||||
}, [gifts]);
|
}, [filteredGifts]);
|
||||||
|
|
||||||
useEffect(() => {
|
const handleSubmit = useLastCallback(() => {
|
||||||
if (isOpen && gifts?.length) {
|
|
||||||
setSelectedOption(gifts[0].months);
|
|
||||||
}
|
|
||||||
}, [gifts, isOpen]);
|
|
||||||
|
|
||||||
const selectedGift = useMemo(() => {
|
|
||||||
return gifts?.find((gift) => gift.months === selectedOption);
|
|
||||||
}, [gifts, selectedOption]);
|
|
||||||
|
|
||||||
const handleSubmit = useCallback(() => {
|
|
||||||
if (!selectedGift) {
|
if (!selectedGift) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
closeGiftPremiumModal();
|
openInvoice({
|
||||||
openUrl({ url: selectedGift.botUrl });
|
type: 'giftcode',
|
||||||
}, [closeGiftPremiumModal, openUrl, selectedGift]);
|
userIds: forUserIds!,
|
||||||
|
currency: selectedGift!.currency,
|
||||||
|
amount: selectedGift!.amount,
|
||||||
|
option: selectedGift!,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const handlePremiumClick = useCallback(() => {
|
const handlePremiumClick = useLastCallback(() => {
|
||||||
openPremiumModal();
|
openPremiumModal();
|
||||||
}, [openPremiumModal]);
|
});
|
||||||
|
|
||||||
|
const showConfetti = useLastCallback(() => {
|
||||||
|
const dialog = dialogRef.current;
|
||||||
|
if (!dialog) return;
|
||||||
|
if (isOpen) {
|
||||||
|
const {
|
||||||
|
top, left, width, height,
|
||||||
|
} = dialog.querySelector('.modal-content')!.getBoundingClientRect();
|
||||||
|
requestConfetti({
|
||||||
|
top,
|
||||||
|
left,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isCompleted) {
|
||||||
|
showConfetti();
|
||||||
|
}
|
||||||
|
}, [isCompleted, showConfetti]);
|
||||||
|
|
||||||
|
const userNameList = useMemo(() => {
|
||||||
|
const usersById = getGlobal().users.byId;
|
||||||
|
return forUserIds?.map((userId) => getUserFullName(usersById[userId])).join(', ');
|
||||||
|
}, [forUserIds]);
|
||||||
|
|
||||||
|
function renderGiftTitle() {
|
||||||
|
if (isCompleted) {
|
||||||
|
return renderText(lang('TelegramPremiumUserGiftedPremiumOutboundDialogTitle',
|
||||||
|
[userNameList, selectedGift?.months]), ['simple_markdown']);
|
||||||
|
}
|
||||||
|
|
||||||
|
return lang('GiftTelegramPremiumTitle');
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderGiftText() {
|
||||||
|
if (isCompleted) {
|
||||||
|
return renderText(lang('TelegramPremiumUserGiftedPremiumOutboundDialogSubtitle', userNameList),
|
||||||
|
['simple_markdown']);
|
||||||
|
}
|
||||||
|
return renderText(lang('GiftPremiumUsersGiveAccessManyZero', userNameList), ['simple_markdown']);
|
||||||
|
}
|
||||||
|
|
||||||
function renderPremiumFeaturesLink() {
|
function renderPremiumFeaturesLink() {
|
||||||
const info = lang('GiftPremiumListFeaturesAndTerms');
|
const info = lang('GiftPremiumListFeaturesAndTerms');
|
||||||
@ -90,7 +164,7 @@ const GiftPremiumModal: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p className={styles.premiumFeatures}>
|
<p className={buildClassName(styles.premiumFeatures, styles.center)}>
|
||||||
{parts[1]}
|
{parts[1]}
|
||||||
<Link isPrimary onClick={handlePremiumClick}>{parts[2]}</Link>
|
<Link isPrimary onClick={handlePremiumClick}>{parts[2]}</Link>
|
||||||
{parts[3]}
|
{parts[3]}
|
||||||
@ -98,8 +172,43 @@ const GiftPremiumModal: FC<OwnProps & StateProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderBoostsPluralText() {
|
||||||
|
const giftParts = renderText(lang('GiftPremiumWillReceiveBoostsPlural', selectedUserQuantity), ['simple_markdown']);
|
||||||
|
return giftParts.map((part) => {
|
||||||
|
if (typeof part === 'string') {
|
||||||
|
return part.split(/(⚡)/g).map((subpart) => {
|
||||||
|
if (subpart === '⚡') {
|
||||||
|
return <Icon name="boost" className={styles.boostIcon} />;
|
||||||
|
}
|
||||||
|
return subpart;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return part;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSubscriptionGiftOptions() {
|
||||||
|
return (
|
||||||
|
<div className={styles.subscriptionOptions}>
|
||||||
|
{filteredGifts?.map((gift) => {
|
||||||
|
return (
|
||||||
|
<PremiumSubscriptionOption
|
||||||
|
className={styles.subscriptionOption}
|
||||||
|
key={gift.months}
|
||||||
|
option={gift}
|
||||||
|
fullMonthlyAmount={fullMonthlyGiftAmount}
|
||||||
|
checked={gift.months === selectedMonthOption}
|
||||||
|
onChange={setSelectedMonthOption}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
dialogRef={dialogRef}
|
||||||
onClose={closeGiftPremiumModal}
|
onClose={closeGiftPremiumModal}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
className={styles.modalDialog}
|
className={styles.modalDialog}
|
||||||
@ -116,53 +225,53 @@ const GiftPremiumModal: FC<OwnProps & StateProps> = ({
|
|||||||
>
|
>
|
||||||
<i className="icon icon-close" />
|
<i className="icon icon-close" />
|
||||||
</Button>
|
</Button>
|
||||||
<Avatar
|
<div className={styles.avatars}>
|
||||||
peer={user}
|
<AvatarList
|
||||||
size="jumbo"
|
size="large"
|
||||||
className={styles.avatar}
|
peers={giftingUserList}
|
||||||
/>
|
/>
|
||||||
<h2 className={styles.headerText}>
|
|
||||||
{lang('GiftTelegramPremiumTitle')}
|
|
||||||
</h2>
|
|
||||||
<p className={styles.description}>
|
|
||||||
{renderText(
|
|
||||||
lang('GiftTelegramPremiumDescription', getUserFirstOrLastName(user)),
|
|
||||||
['emoji', 'simple_markdown'],
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<div className={styles.options}>
|
|
||||||
{gifts?.map((gift) => (
|
|
||||||
<PremiumSubscriptionOption
|
|
||||||
key={gift.amount}
|
|
||||||
option={gift}
|
|
||||||
fullMonthlyAmount={fullMonthlyAmount}
|
|
||||||
checked={gift.months === selectedOption}
|
|
||||||
onChange={setSelectedOption}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
</div>
|
||||||
|
<h2 className={buildClassName(styles.headerText, styles.center)}>
|
||||||
|
{renderGiftTitle()}
|
||||||
|
</h2>
|
||||||
|
<p className={buildClassName(styles.description, styles.center)}>
|
||||||
|
{renderGiftText()}
|
||||||
|
</p>
|
||||||
|
{!isCompleted && (
|
||||||
|
<>
|
||||||
|
<p className={styles.description}>
|
||||||
|
{renderText(renderBoostsPluralText(), ['simple_markdown', 'emoji'])}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<div className={styles.options}>
|
||||||
|
{renderSubscriptionGiftOptions()}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
{renderPremiumFeaturesLink()}
|
{renderPremiumFeaturesLink()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button className={styles.button} isShiny disabled={!selectedOption} onClick={handleSubmit}>
|
{!isCompleted && (
|
||||||
{lang(
|
<Button withPremiumGradient className={styles.button} isShiny disabled={!selectedGift} onClick={handleSubmit}>
|
||||||
'GiftSubscriptionFor',
|
{lang(
|
||||||
selectedGift && formatCurrency(Number(selectedGift.amount), selectedGift.currency, lang.code),
|
'GiftSubscriptionFor', selectedGift
|
||||||
)}
|
&& formatCurrency(selectedGift!.amount, selectedGift.currency, lang.code),
|
||||||
</Button>
|
)}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(withGlobal<OwnProps>((global): StateProps => {
|
export default memo(withGlobal<OwnProps>((global): StateProps => {
|
||||||
const { forUserId } = selectTabState(global).giftPremiumModal || {};
|
const {
|
||||||
const user = forUserId ? selectUser(global, forUserId) : undefined;
|
gifts, forUserIds, isCompleted,
|
||||||
const gifts = user ? selectUserFullInfo(global, user.id)?.premiumGifts : undefined;
|
} = selectTabState(global).giftPremiumModal || {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user,
|
isCompleted,
|
||||||
gifts,
|
gifts,
|
||||||
|
boostPerSentGift: global.appConfig?.boostsPerSentGift,
|
||||||
|
forUserIds,
|
||||||
};
|
};
|
||||||
})(GiftPremiumModal));
|
})(GiftPremiumModal));
|
||||||
|
|||||||
@ -396,7 +396,6 @@ const GiveawayModal: FC<OwnProps & StateProps> = ({
|
|||||||
isGiveaway
|
isGiveaway
|
||||||
key={gift.months}
|
key={gift.months}
|
||||||
option={gift}
|
option={gift}
|
||||||
userCount={gift.users}
|
|
||||||
fullMonthlyAmount={fullMonthlyAmount!}
|
fullMonthlyAmount={fullMonthlyAmount!}
|
||||||
checked={gift.months === selectedMonthOption}
|
checked={gift.months === selectedMonthOption}
|
||||||
onChange={setSelectedMonthOption}
|
onChange={setSelectedMonthOption}
|
||||||
@ -707,6 +706,7 @@ const GiveawayModal: FC<OwnProps & StateProps> = ({
|
|||||||
selectionLimit={countrySelectionLimit}
|
selectionLimit={countrySelectionLimit}
|
||||||
/>
|
/>
|
||||||
<AppendEntityPickerModal
|
<AppendEntityPickerModal
|
||||||
|
key={entityType}
|
||||||
isOpen={isEntityPickerModalOpen}
|
isOpen={isEntityPickerModalOpen}
|
||||||
onClose={closeEntityPickerModal}
|
onClose={closeEntityPickerModal}
|
||||||
entityType={entityType}
|
entityType={entityType}
|
||||||
|
|||||||
18
src/components/main/premium/PremiumGiftingModal.async.tsx
Normal file
18
src/components/main/premium/PremiumGiftingModal.async.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
import React from '../../../lib/teact/teact';
|
||||||
|
|
||||||
|
import type { OwnProps } from './PremiumGiftingModal';
|
||||||
|
|
||||||
|
import { Bundles } from '../../../util/moduleLoader';
|
||||||
|
|
||||||
|
import useModuleLoader from '../../../hooks/useModuleLoader';
|
||||||
|
|
||||||
|
const PremiumGiftingModalAsync: FC<OwnProps> = (props) => {
|
||||||
|
const { isOpen } = props;
|
||||||
|
const PremiumGiftingModal = useModuleLoader(Bundles.Extra, 'PremiumGiftingModal', !isOpen);
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
return PremiumGiftingModal ? <PremiumGiftingModal {...props} /> : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default PremiumGiftingModalAsync;
|
||||||
80
src/components/main/premium/PremiumGiftingModal.module.scss
Normal file
80
src/components/main/premium/PremiumGiftingModal.module.scss
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
.root :global(.modal-content) {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.modal-dialog) {
|
||||||
|
max-width: 55vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root :global(.modal-dialog), .root :global(.modal-content) {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.main {
|
||||||
|
height: 90vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.filter {
|
||||||
|
padding: 0.375rem 1rem 0.25rem 0.75rem;
|
||||||
|
margin-bottom: 0.625rem;
|
||||||
|
background-color: var(--color-background);
|
||||||
|
box-shadow: inset 0 -0.0625rem 0 0 var(--color-background-secondary-accent);
|
||||||
|
border-bottom: 0.625rem solid var(--color-background-secondary);
|
||||||
|
display: flex;
|
||||||
|
flex-flow: row wrap;
|
||||||
|
align-items: center;
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
max-height: 20rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.title {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
width: 100%;
|
||||||
|
background: var(--color-background);
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
z-index: 1;
|
||||||
|
padding: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.picker {
|
||||||
|
height: 75vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.avatars {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 1rem;
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
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;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
139
src/components/main/premium/PremiumGiftingModal.tsx
Normal file
139
src/components/main/premium/PremiumGiftingModal.tsx
Normal file
@ -0,0 +1,139 @@
|
|||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
import React, {
|
||||||
|
memo, useMemo, useState,
|
||||||
|
} from '../../../lib/teact/teact';
|
||||||
|
import { getActions, getGlobal, withGlobal } from '../../../global';
|
||||||
|
|
||||||
|
import { GIVEAWAY_MAX_ADDITIONAL_CHANNELS } from '../../../config';
|
||||||
|
import {
|
||||||
|
filterUsersByName, isUserBot,
|
||||||
|
} from '../../../global/helpers';
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import { unique } from '../../../util/iteratees';
|
||||||
|
import sortChatIds from '../../common/helpers/sortChatIds';
|
||||||
|
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
|
|
||||||
|
import Icon from '../../common/Icon';
|
||||||
|
import Picker from '../../common/Picker';
|
||||||
|
import Button from '../../ui/Button';
|
||||||
|
import Modal from '../../ui/Modal';
|
||||||
|
|
||||||
|
import styles from './PremiumGiftingModal.module.scss';
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
isOpen?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface StateProps {
|
||||||
|
currentUserId?: string;
|
||||||
|
userSelectionLimit?: number;
|
||||||
|
userIds?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const PremiumGiftingModal: FC<OwnProps & StateProps> = ({
|
||||||
|
isOpen,
|
||||||
|
currentUserId,
|
||||||
|
userSelectionLimit = GIVEAWAY_MAX_ADDITIONAL_CHANNELS,
|
||||||
|
userIds,
|
||||||
|
}) => {
|
||||||
|
const { closePremiumGiftingModal, openGiftPremiumModal, showNotification } = getActions();
|
||||||
|
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
const [selectedUserIds, setSelectedUserIds] = useState<string[]>([]);
|
||||||
|
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||||
|
|
||||||
|
const displayedUserIds = useMemo(() => {
|
||||||
|
const usersById = getGlobal().users.byId;
|
||||||
|
const filteredContactIds = userIds ? filterUsersByName(userIds, usersById, searchQuery) : [];
|
||||||
|
|
||||||
|
return sortChatIds(unique(filteredContactIds).filter((userId) => {
|
||||||
|
const user = usersById[userId];
|
||||||
|
if (!user) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return !isUserBot(user) && userId !== currentUserId;
|
||||||
|
}));
|
||||||
|
}, [currentUserId, searchQuery, userIds]);
|
||||||
|
|
||||||
|
const handleSendIdList = useLastCallback(() => {
|
||||||
|
if (selectedUserIds?.length) {
|
||||||
|
openGiftPremiumModal({ forUserIds: selectedUserIds });
|
||||||
|
|
||||||
|
closePremiumGiftingModal();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleSelectedUserIdsChange = useLastCallback((newSelectedIds: string[]) => {
|
||||||
|
if (newSelectedIds.length > userSelectionLimit) {
|
||||||
|
showNotification({
|
||||||
|
message: lang('BoostingSelectUpToWarningUsers', userSelectionLimit),
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setSelectedUserIds(newSelectedIds);
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderSearchField() {
|
||||||
|
return (
|
||||||
|
<div className={styles.filter} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||||
|
<Button
|
||||||
|
round
|
||||||
|
size="smaller"
|
||||||
|
color="translucent"
|
||||||
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
|
onClick={() => closePremiumGiftingModal()}
|
||||||
|
ariaLabel={lang('Close')}
|
||||||
|
>
|
||||||
|
<Icon name="close" />
|
||||||
|
</Button>
|
||||||
|
<h3 className={styles.title}>{lang('GiftTelegramPremiumTitle')}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
className={styles.root}
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={closePremiumGiftingModal}
|
||||||
|
onEnter={handleSendIdList}
|
||||||
|
>
|
||||||
|
<div className={styles.main}>
|
||||||
|
{renderSearchField()}
|
||||||
|
<div className={buildClassName(styles.main, 'custom-scroll')}>
|
||||||
|
<Picker
|
||||||
|
className={styles.picker}
|
||||||
|
itemIds={displayedUserIds}
|
||||||
|
selectedIds={selectedUserIds}
|
||||||
|
filterValue={searchQuery}
|
||||||
|
filterPlaceholder={lang('Search')}
|
||||||
|
searchInputId="users-picker-search"
|
||||||
|
onSelectedIdsChange={handleSelectedUserIdsChange}
|
||||||
|
onFilterChange={setSearchQuery}
|
||||||
|
isSearchable
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className={styles.buttons}>
|
||||||
|
<Button withPremiumGradient size="smaller" onClick={handleSendIdList} disabled={!selectedUserIds?.length}>
|
||||||
|
{lang('Continue')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>((global): StateProps => {
|
||||||
|
const { currentUserId } = global;
|
||||||
|
|
||||||
|
return {
|
||||||
|
currentUserId,
|
||||||
|
userIds: global.contactList?.userIds,
|
||||||
|
userSelectionLimit: global.appConfig?.giveawayAddPeersMax,
|
||||||
|
};
|
||||||
|
})(PremiumGiftingModal));
|
||||||
@ -176,3 +176,4 @@
|
|||||||
.subscriptionOption {
|
.subscriptionOption {
|
||||||
margin: 0.8125rem;
|
margin: 0.8125rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -14,7 +14,6 @@ import styles from './PremiumSubscriptionOption.module.scss';
|
|||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
option: ApiPremiumGiftOption | ApiPremiumGiftCodeOption;
|
option: ApiPremiumGiftOption | ApiPremiumGiftCodeOption;
|
||||||
isGiveaway?: boolean;
|
isGiveaway?: boolean;
|
||||||
userCount?: number;
|
|
||||||
checked?: boolean;
|
checked?: boolean;
|
||||||
fullMonthlyAmount?: number;
|
fullMonthlyAmount?: number;
|
||||||
className?: string;
|
className?: string;
|
||||||
@ -23,14 +22,16 @@ type OwnProps = {
|
|||||||
|
|
||||||
const PremiumSubscriptionOption: FC<OwnProps> = ({
|
const PremiumSubscriptionOption: FC<OwnProps> = ({
|
||||||
option, checked, fullMonthlyAmount,
|
option, checked, fullMonthlyAmount,
|
||||||
onChange, className, isGiveaway, userCount,
|
onChange, className, isGiveaway,
|
||||||
}) => {
|
}) => {
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
|
|
||||||
const {
|
const {
|
||||||
months, amount, currency,
|
months, amount, currency,
|
||||||
} = option;
|
} = option;
|
||||||
|
const users = 'users' in option ? option.users : undefined;
|
||||||
const perMonth = Math.floor(amount / months);
|
const perMonth = Math.floor(amount / months);
|
||||||
|
const isUserCountPlural = users ? users > 1 : undefined;
|
||||||
|
|
||||||
const discount = useMemo(() => {
|
const discount = useMemo(() => {
|
||||||
return fullMonthlyAmount && fullMonthlyAmount > perMonth
|
return fullMonthlyAmount && fullMonthlyAmount > perMonth
|
||||||
@ -63,9 +64,9 @@ const PremiumSubscriptionOption: FC<OwnProps> = ({
|
|||||||
/>
|
/>
|
||||||
<div className={styles.content}>
|
<div className={styles.content}>
|
||||||
<div className={styles.month}>
|
<div className={styles.month}>
|
||||||
{Boolean(discount) && isGiveaway && (
|
{Boolean(discount) && (
|
||||||
<span
|
<span
|
||||||
className={buildClassName(styles.giveawayDiscount, isGiveaway && styles.discount)}
|
className={buildClassName(styles.giveawayDiscount, styles.discount)}
|
||||||
title={lang('GiftDiscount')}
|
title={lang('GiftDiscount')}
|
||||||
> −{discount}%
|
> −{discount}%
|
||||||
</span>
|
</span>
|
||||||
@ -73,11 +74,11 @@ const PremiumSubscriptionOption: FC<OwnProps> = ({
|
|||||||
{lang('Months', months)}
|
{lang('Months', months)}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.perMonth}>
|
<div className={styles.perMonth}>
|
||||||
{isGiveaway ? `${formatCurrency(amount, currency, lang.code)} x ${userCount!}`
|
{(isGiveaway || isUserCountPlural) ? `${formatCurrency(amount, currency, lang.code)} x ${users!}`
|
||||||
: lang('PricePerMonth', formatCurrency(perMonth, currency, lang.code))}
|
: lang('PricePerMonth', formatCurrency(perMonth, currency, lang.code))}
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.amount}>
|
<div className={styles.amount}>
|
||||||
{formatCurrency(isGiveaway ? amount * userCount! : amount, currency, lang.code)}
|
{formatCurrency(amount, currency, lang.code)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
|
|||||||
@ -313,7 +313,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleGiftPremiumClick = useLastCallback(() => {
|
const handleGiftPremiumClick = useLastCallback(() => {
|
||||||
openGiftPremiumModal({ forUserId: chatId });
|
openGiftPremiumModal({ forUserIds: [chatId] });
|
||||||
closeMenu();
|
closeMenu();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -656,7 +656,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
providerName = url.startsWith(DONATE_PROVIDER_URL) ? DONATE_PROVIDER : undefined;
|
providerName = url.startsWith(DONATE_PROVIDER_URL) ? DONATE_PROVIDER : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const chat = inputInvoice && 'chatId' in inputInvoice ? selectChat(global, inputInvoice.chatId) : undefined;
|
const chat = inputInvoice && 'chatId' in inputInvoice ? selectChat(global, inputInvoice.chatId!) : undefined;
|
||||||
const isProviderError = Boolean(invoice && (!providerName || !SUPPORTED_PROVIDERS.has(providerName)));
|
const isProviderError = Boolean(invoice && (!providerName || !SUPPORTED_PROVIDERS.has(providerName)));
|
||||||
const { needCardholderName, needCountry, needZip } = (nativeParams || {});
|
const { needCardholderName, needCountry, needZip } = (nativeParams || {});
|
||||||
const {
|
const {
|
||||||
|
|||||||
@ -318,6 +318,7 @@ export const GIVEAWAY_BOOST_PER_PREMIUM = 4;
|
|||||||
export const GIVEAWAY_MAX_ADDITIONAL_CHANNELS = 10;
|
export const GIVEAWAY_MAX_ADDITIONAL_CHANNELS = 10;
|
||||||
export const GIVEAWAY_MAX_ADDITIONAL_USERS = 10;
|
export const GIVEAWAY_MAX_ADDITIONAL_USERS = 10;
|
||||||
export const GIVEAWAY_MAX_ADDITIONAL_COUNTRIES = 10;
|
export const GIVEAWAY_MAX_ADDITIONAL_COUNTRIES = 10;
|
||||||
|
export const BOOST_PER_SENT_GIFT = 3;
|
||||||
|
|
||||||
export const LIGHT_THEME_BG_COLOR = '#99BA92';
|
export const LIGHT_THEME_BG_COLOR = '#99BA92';
|
||||||
export const DARK_THEME_BG_COLOR = '#0F0F0F';
|
export const DARK_THEME_BG_COLOR = '#0F0F0F';
|
||||||
|
|||||||
@ -422,18 +422,46 @@ addActionHandler('closeGiveawayModal', (global, actions, payload): ActionReturnT
|
|||||||
}, tabId);
|
}, tabId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('openPremiumGiftingModal', (global, actions, payload): ActionReturnType => {
|
||||||
|
const {
|
||||||
|
tabId = getCurrentTabId(),
|
||||||
|
} = payload || {};
|
||||||
|
|
||||||
|
global = getGlobal();
|
||||||
|
|
||||||
|
global = updateTabState(global, {
|
||||||
|
giftingModal: {
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
}, tabId);
|
||||||
|
setGlobal(global);
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('closePremiumGiftingModal', (global, actions, payload): ActionReturnType => {
|
||||||
|
const { tabId = getCurrentTabId() } = payload || {};
|
||||||
|
|
||||||
|
return updateTabState(global, {
|
||||||
|
giftingModal: undefined,
|
||||||
|
}, tabId);
|
||||||
|
});
|
||||||
|
|
||||||
addActionHandler('openGiftPremiumModal', async (global, actions, payload): Promise<void> => {
|
addActionHandler('openGiftPremiumModal', async (global, actions, payload): Promise<void> => {
|
||||||
const { forUserId, tabId = getCurrentTabId() } = payload || {};
|
const {
|
||||||
|
forUserIds, tabId = getCurrentTabId(),
|
||||||
|
} = payload || {};
|
||||||
const result = await callApi('fetchPremiumPromo');
|
const result = await callApi('fetchPremiumPromo');
|
||||||
if (!result) return;
|
if (!result) return;
|
||||||
|
|
||||||
global = getGlobal();
|
global = getGlobal();
|
||||||
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
|
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
|
||||||
|
|
||||||
|
const gifts = await callApi('getPremiumGiftCodeOptions', {});
|
||||||
|
|
||||||
global = updateTabState(global, {
|
global = updateTabState(global, {
|
||||||
giftPremiumModal: {
|
giftPremiumModal: {
|
||||||
isOpen: true,
|
isOpen: true,
|
||||||
forUserId,
|
forUserIds,
|
||||||
|
gifts,
|
||||||
},
|
},
|
||||||
}, tabId);
|
}, tabId);
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
|
|||||||
@ -1,9 +1,10 @@
|
|||||||
import type { ActionReturnType } from '../../types';
|
import type { ActionReturnType } from '../../types';
|
||||||
|
|
||||||
|
import { areDeepEqual } from '../../../util/areDeepEqual';
|
||||||
import { formatCurrency } from '../../../util/formatCurrency';
|
import { formatCurrency } from '../../../util/formatCurrency';
|
||||||
import * as langProvider from '../../../util/langProvider';
|
import * as langProvider from '../../../util/langProvider';
|
||||||
import { IS_PRODUCTION_HOST } from '../../../util/windowEnvironment';
|
import { IS_PRODUCTION_HOST } from '../../../util/windowEnvironment';
|
||||||
import { addActionHandler } from '../../index';
|
import { addActionHandler, setGlobal } from '../../index';
|
||||||
import { closeInvoice } from '../../reducers';
|
import { closeInvoice } from '../../reducers';
|
||||||
import { updateTabState } from '../../reducers/tabs';
|
import { updateTabState } from '../../reducers/tabs';
|
||||||
import { selectChatMessage, selectTabState } from '../../selectors';
|
import { selectChatMessage, selectTabState } from '../../selectors';
|
||||||
@ -30,6 +31,24 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (inputInvoice && inputInvoice.type === 'giftcode') {
|
||||||
|
if (!inputInvoice.userIds) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const giftModalState = selectTabState(global, tabId).giftPremiumModal;
|
||||||
|
|
||||||
|
if (giftModalState && giftModalState.isOpen
|
||||||
|
&& areDeepEqual(inputInvoice.userIds, giftModalState.forUserIds)) {
|
||||||
|
global = updateTabState(global, {
|
||||||
|
giftPremiumModal: {
|
||||||
|
...giftModalState,
|
||||||
|
isCompleted: true,
|
||||||
|
},
|
||||||
|
}, tabId);
|
||||||
|
setGlobal(global);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// On the production host, the payment frame receives a message with the payment event,
|
// 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.
|
// after which the payment form closes. In other cases, the payment form must be closed manually.
|
||||||
// Closing the invoice will cause the closing of the Payment Modal dialog and then closing the payment.
|
// Closing the invoice will cause the closing of the Payment Modal dialog and then closing the payment.
|
||||||
|
|||||||
@ -641,9 +641,15 @@ export type TabState = {
|
|||||||
prepaidGiveaway?: ApiPrepaidGiveaway;
|
prepaidGiveaway?: ApiPrepaidGiveaway;
|
||||||
};
|
};
|
||||||
|
|
||||||
giftPremiumModal?: {
|
giftingModal?: {
|
||||||
isOpen?: boolean;
|
isOpen?: boolean;
|
||||||
forUserId?: string;
|
};
|
||||||
|
|
||||||
|
giftPremiumModal?: {
|
||||||
|
isCompleted?: boolean;
|
||||||
|
isOpen?: boolean;
|
||||||
|
forUserIds?: string[];
|
||||||
|
gifts?: ApiPremiumGiftCodeOption[];
|
||||||
};
|
};
|
||||||
|
|
||||||
limitReachedModal?: {
|
limitReachedModal?: {
|
||||||
@ -2966,6 +2972,9 @@ export interface ActionPayloads {
|
|||||||
} & WithTabId);
|
} & WithTabId);
|
||||||
closeGiveawayModal: WithTabId | undefined;
|
closeGiveawayModal: WithTabId | undefined;
|
||||||
|
|
||||||
|
openPremiumGiftingModal: WithTabId | undefined;
|
||||||
|
closePremiumGiftingModal: WithTabId | undefined;
|
||||||
|
|
||||||
transcribeAudio: {
|
transcribeAudio: {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
messageId: number;
|
messageId: number;
|
||||||
@ -2976,7 +2985,9 @@ export interface ActionPayloads {
|
|||||||
loadPremiumStickers: undefined;
|
loadPremiumStickers: undefined;
|
||||||
|
|
||||||
openGiftPremiumModal: ({
|
openGiftPremiumModal: ({
|
||||||
forUserId?: string;
|
chatId?: string;
|
||||||
|
forMultipleUsers?: boolean;
|
||||||
|
forUserIds?: string[];
|
||||||
} & WithTabId) | undefined;
|
} & WithTabId) | undefined;
|
||||||
|
|
||||||
closeGiftPremiumModal: WithTabId | undefined;
|
closeGiftPremiumModal: WithTabId | undefined;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user