Giveaway: Creating giveaway in channels (#4339)
Co-authored-by: zubiden <19638254+zubiden@users.noreply.github.com>
This commit is contained in:
parent
9807dfb5d5
commit
97d3d31f10
@ -52,6 +52,8 @@ export interface GramJsAppConfig extends LimitsConfig {
|
||||
autologin_token: string;
|
||||
url_auth_domains: string[];
|
||||
premium_purchase_blocked: boolean;
|
||||
giveaway_gifts_purchase_available: boolean;
|
||||
giveaway_add_peers_max: number;
|
||||
premium_bot_username: string;
|
||||
premium_invoice_slug: string;
|
||||
premium_promo_order: string[];
|
||||
@ -59,6 +61,8 @@ export interface GramJsAppConfig extends LimitsConfig {
|
||||
hidden_members_group_size_min: number;
|
||||
autoarchive_setting_available: boolean;
|
||||
authorization_autoconfirm_period: number;
|
||||
giveaway_boosts_per_premium: number;
|
||||
giveaway_countries_max: number;
|
||||
// Forums
|
||||
topics_pinned_limit: number;
|
||||
// Stories
|
||||
@ -109,11 +113,15 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp
|
||||
premiumInvoiceSlug: appConfig.premium_invoice_slug,
|
||||
premiumPromoOrder: appConfig.premium_promo_order as ApiPremiumSection[],
|
||||
isPremiumPurchaseBlocked: appConfig.premium_purchase_blocked,
|
||||
isGiveawayGiftsPurchaseAvailable: appConfig.giveaway_gifts_purchase_available,
|
||||
defaultEmojiStatusesStickerSetId: appConfig.default_emoji_statuses_stickerset_id,
|
||||
topicsPinnedLimit: appConfig.topics_pinned_limit,
|
||||
maxUserReactionsDefault: appConfig.reactions_user_max_default,
|
||||
maxUserReactionsPremium: appConfig.reactions_user_max_premium,
|
||||
hiddenMembersMinCount: appConfig.hidden_members_group_size_min,
|
||||
giveawayAddPeersMax: appConfig.giveaway_add_peers_max,
|
||||
giveawayBoostsPerPremium: appConfig.giveaway_boosts_per_premium,
|
||||
giveawayCountriesMax: appConfig.giveaway_countries_max,
|
||||
canDisplayAutoarchiveSetting: appConfig.autoarchive_setting_available,
|
||||
limits: {
|
||||
uploadMaxFileparts: getLimit(appConfig, 'upload_max_fileparts', 'uploadMaxFileparts'),
|
||||
|
||||
@ -6,7 +6,7 @@ import type {
|
||||
ApiCheckedGiftCode,
|
||||
ApiGiveawayInfo,
|
||||
ApiInvoice, ApiLabeledPrice, ApiMyBoost, ApiPaymentCredentials,
|
||||
ApiPaymentForm, ApiPaymentSavedInfo, ApiPremiumPromo, ApiPremiumSubscriptionOption,
|
||||
ApiPaymentForm, ApiPaymentSavedInfo, ApiPremiumGiftCodeOption, ApiPremiumPromo, ApiPremiumSubscriptionOption,
|
||||
ApiReceipt,
|
||||
} from '../../types';
|
||||
|
||||
@ -14,7 +14,7 @@ import { buildApiMessageEntity } from './common';
|
||||
import { omitVirtualClassFields } from './helpers';
|
||||
import { buildApiDocument, buildApiWebDocument } from './messageContent';
|
||||
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
|
||||
import { buildStatisticsPercentage } from './statistics';
|
||||
import { buildPrepaidGiveaway, buildStatisticsPercentage } from './statistics';
|
||||
|
||||
export function buildShippingOptions(shippingOptions: GramJs.ShippingOption[] | undefined) {
|
||||
if (!shippingOptions) {
|
||||
@ -209,7 +209,7 @@ export function buildApiPaymentCredentials(credentials: GramJs.PaymentSavedCrede
|
||||
|
||||
export function buildApiBoostsStatus(boostStatus: GramJs.premium.BoostsStatus): ApiBoostsStatus {
|
||||
const {
|
||||
level, boostUrl, boosts, myBoost, currentLevelBoosts, nextLevelBoosts, premiumAudience,
|
||||
level, boostUrl, boosts, myBoost, currentLevelBoosts, nextLevelBoosts, premiumAudience, prepaidGiveaways,
|
||||
} = boostStatus;
|
||||
return {
|
||||
level,
|
||||
@ -219,6 +219,7 @@ export function buildApiBoostsStatus(boostStatus: GramJs.premium.BoostsStatus):
|
||||
boostUrl,
|
||||
nextLevelBoosts,
|
||||
...(premiumAudience && { premiumSubscribers: buildStatisticsPercentage(premiumAudience) }),
|
||||
...(prepaidGiveaways && { prepaidGiveaways: prepaidGiveaways.map(buildPrepaidGiveaway) }),
|
||||
};
|
||||
}
|
||||
|
||||
@ -295,3 +296,16 @@ export function buildApiCheckedGiftCode(giftcode: GramJs.payments.TypeCheckedGif
|
||||
giveawayMessageId: giveawayMsgId,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildApiPremiumGiftCodeOption(option: GramJs.PremiumGiftCodeOption): ApiPremiumGiftCodeOption {
|
||||
const {
|
||||
amount, currency, months, users,
|
||||
} = option;
|
||||
|
||||
return {
|
||||
amount: amount.toJSNumber(),
|
||||
currency,
|
||||
months,
|
||||
users,
|
||||
};
|
||||
}
|
||||
|
||||
@ -6,7 +6,7 @@ import type {
|
||||
ApiMessagePublicForward,
|
||||
ApiPostStatistics,
|
||||
ApiStoryPublicForward,
|
||||
StatisticsGraph,
|
||||
PrepaidGiveaway, StatisticsGraph,
|
||||
StatisticsMessageInteractionCounter,
|
||||
StatisticsOverviewItem,
|
||||
StatisticsOverviewPercentage,
|
||||
@ -214,6 +214,15 @@ export function buildStatisticsPercentage(data: GramJs.StatsPercentValue): Stati
|
||||
};
|
||||
}
|
||||
|
||||
export function buildPrepaidGiveaway(prepaidGiveaway: GramJs.PrepaidGiveaway): PrepaidGiveaway {
|
||||
return {
|
||||
id: prepaidGiveaway.id.toString(),
|
||||
date: prepaidGiveaway.date,
|
||||
months: prepaidGiveaway.months,
|
||||
quantity: prepaidGiveaway.quantity,
|
||||
};
|
||||
}
|
||||
|
||||
function getOverviewPeriod(data: GramJs.StatsDateRangeDays): StatisticsOverviewPeriod {
|
||||
return {
|
||||
maxDate: data.maxDate,
|
||||
|
||||
@ -12,12 +12,13 @@ import type {
|
||||
ApiFormattedText,
|
||||
ApiGroupCall,
|
||||
ApiInputReplyInfo,
|
||||
ApiInputStorePaymentPurpose,
|
||||
ApiMessageEntity,
|
||||
ApiNewPoll,
|
||||
ApiPhoneCall,
|
||||
ApiPhoto,
|
||||
ApiPoll,
|
||||
ApiReaction,
|
||||
ApiPremiumGiftCodeOption, ApiReaction,
|
||||
ApiReportReason,
|
||||
ApiRequestInputInvoice,
|
||||
ApiSendMessageAction,
|
||||
@ -566,16 +567,69 @@ export function buildInputPhoneCall({ id, accessHash }: ApiPhoneCall) {
|
||||
});
|
||||
}
|
||||
|
||||
export function buildInputStorePaymentPurpose(purpose: ApiInputStorePaymentPurpose):
|
||||
GramJs.TypeInputStorePaymentPurpose {
|
||||
if (purpose.type === 'giftcode') {
|
||||
return new GramJs.InputStorePaymentPremiumGiftCode({
|
||||
users: purpose.users.map((user) => buildInputEntity(user.id, user.accessHash) as GramJs.InputUser),
|
||||
boostPeer: purpose.boostChannel
|
||||
? buildInputPeer(purpose.boostChannel.id, purpose.boostChannel.accessHash)
|
||||
: undefined,
|
||||
currency: purpose.currency,
|
||||
amount: BigInt(purpose.amount),
|
||||
});
|
||||
}
|
||||
|
||||
const randomId = generateRandomBigInt();
|
||||
|
||||
return new GramJs.InputStorePaymentPremiumGiveaway({
|
||||
boostPeer: buildInputPeer(purpose.chat.id, purpose.chat.accessHash),
|
||||
additionalPeers: purpose.additionalChannels?.map((chat) => buildInputPeer(chat.id, chat.accessHash)),
|
||||
countriesIso2: purpose.countries,
|
||||
prizeDescription: purpose.prizeDescription,
|
||||
onlyNewSubscribers: purpose.isOnlyForNewSubscribers || undefined,
|
||||
winnersAreVisible: purpose.areWinnersVisible || undefined,
|
||||
untilDate: purpose.untilDate,
|
||||
currency: purpose.currency,
|
||||
amount: BigInt(purpose.amount),
|
||||
randomId,
|
||||
});
|
||||
}
|
||||
|
||||
function buildPremiumGiftCodeOption(optionData: ApiPremiumGiftCodeOption) {
|
||||
return new GramJs.PremiumGiftCodeOption({
|
||||
users: optionData.users,
|
||||
months: optionData.months,
|
||||
currency: optionData.currency,
|
||||
amount: BigInt(optionData.amount),
|
||||
});
|
||||
}
|
||||
|
||||
export function buildInputInvoice(invoice: ApiRequestInputInvoice) {
|
||||
if ('slug' in invoice) {
|
||||
return new GramJs.InputInvoiceSlug({
|
||||
slug: invoice.slug,
|
||||
});
|
||||
} else {
|
||||
return new GramJs.InputInvoiceMessage({
|
||||
peer: buildInputPeer(invoice.chat.id, invoice.chat.accessHash),
|
||||
msgId: invoice.messageId,
|
||||
});
|
||||
switch (invoice.type) {
|
||||
case 'message': {
|
||||
return new GramJs.InputInvoiceMessage({
|
||||
peer: buildInputPeer(invoice.chat.id, invoice.chat.accessHash),
|
||||
msgId: invoice.messageId,
|
||||
});
|
||||
}
|
||||
|
||||
case 'slug': {
|
||||
return new GramJs.InputInvoiceSlug({
|
||||
slug: invoice.slug,
|
||||
});
|
||||
}
|
||||
|
||||
case 'giveaway':
|
||||
default: {
|
||||
const purpose = buildInputStorePaymentPurpose(invoice.purpose);
|
||||
const option = buildPremiumGiftCodeOption(invoice.option);
|
||||
|
||||
return new GramJs.InputInvoicePremiumGiftCode({
|
||||
purpose,
|
||||
option,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -101,5 +101,6 @@ export * from './stories';
|
||||
|
||||
export {
|
||||
validateRequestedInfo, sendPaymentForm, getPaymentForm, getReceipt, fetchPremiumPromo, fetchTemporaryPaymentPassword,
|
||||
applyBoost, fetchBoostsList, fetchBoostsStatus, fetchGiveawayInfo, fetchMyBoosts, applyGiftCode, checkGiftCode,
|
||||
applyBoost, fetchBoostList, fetchBoostStatus, fetchGiveawayInfo, fetchMyBoosts, applyGiftCode, checkGiftCode,
|
||||
getPremiumGiftCodeOptions, launchPrepaidGiveaway,
|
||||
} from './payments';
|
||||
|
||||
@ -2,7 +2,7 @@ import BigInt from 'big-integer';
|
||||
import { Api as GramJs } from '../../../lib/gramjs';
|
||||
|
||||
import type {
|
||||
ApiChat, ApiPeer, ApiRequestInputInvoice,
|
||||
ApiChat, ApiInputStorePaymentPurpose, ApiPeer, ApiRequestInputInvoice,
|
||||
OnApiUpdate,
|
||||
} from '../../types';
|
||||
|
||||
@ -15,12 +15,15 @@ import {
|
||||
buildApiInvoiceFromForm,
|
||||
buildApiMyBoost,
|
||||
buildApiPaymentForm,
|
||||
buildApiPremiumGiftCodeOption,
|
||||
buildApiPremiumPromo,
|
||||
buildApiReceipt,
|
||||
buildShippingOptions,
|
||||
} from '../apiBuilders/payments';
|
||||
import { buildApiUser } from '../apiBuilders/users';
|
||||
import { buildInputInvoice, buildInputPeer, buildShippingInfo } from '../gramjsBuilders';
|
||||
import {
|
||||
buildInputInvoice, buildInputPeer, buildInputStorePaymentPurpose, buildShippingInfo,
|
||||
} from '../gramjsBuilders';
|
||||
import {
|
||||
addEntitiesToLocalDb,
|
||||
deserializeBytes,
|
||||
@ -241,7 +244,7 @@ export async function applyBoost({
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchBoostsStatus({
|
||||
export async function fetchBoostStatus({
|
||||
chat,
|
||||
}: {
|
||||
chat: ApiChat;
|
||||
@ -257,7 +260,7 @@ export async function fetchBoostsStatus({
|
||||
return buildApiBoostsStatus(result);
|
||||
}
|
||||
|
||||
export async function fetchBoostsList({
|
||||
export async function fetchBoostList({
|
||||
chat,
|
||||
offset = '',
|
||||
limit,
|
||||
@ -348,3 +351,37 @@ export function applyGiftCode({
|
||||
shouldReturnTrue: true,
|
||||
});
|
||||
}
|
||||
|
||||
export async function getPremiumGiftCodeOptions({
|
||||
chat,
|
||||
}: {
|
||||
chat?: ApiChat;
|
||||
}) {
|
||||
const result = await invokeRequest(new GramJs.payments.GetPremiumGiftCodeOptions({
|
||||
boostPeer: chat && buildInputPeer(chat.id, chat.accessHash),
|
||||
}));
|
||||
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return result.map(buildApiPremiumGiftCodeOption);
|
||||
}
|
||||
|
||||
export function launchPrepaidGiveaway({
|
||||
chat,
|
||||
giveawayId,
|
||||
paymentPurpose,
|
||||
}: {
|
||||
chat: ApiChat;
|
||||
giveawayId: string;
|
||||
paymentPurpose: ApiInputStorePaymentPurpose;
|
||||
}) {
|
||||
return invokeRequest(new GramJs.payments.LaunchPrepaidGiveaway({
|
||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||
giveawayId: BigInt(giveawayId),
|
||||
purpose: buildInputStorePaymentPurpose(paymentPurpose),
|
||||
}), {
|
||||
shouldReturnTrue: true,
|
||||
});
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import type { ThreadId } from '../../types';
|
||||
import type { ApiWebDocument } from './bots';
|
||||
import type { ApiGroupCall, PhoneCallAction } from './calls';
|
||||
import type { ApiChat } from './chats';
|
||||
import type { ApiInputStorePaymentPurpose, ApiPremiumGiftCodeOption } from './payments';
|
||||
import type { ApiMessageStoryData, ApiWebPageStoryData } from './stories';
|
||||
|
||||
export interface ApiDimensions {
|
||||
@ -170,22 +171,66 @@ export interface ApiPoll {
|
||||
};
|
||||
}
|
||||
|
||||
// First type used for state, second - for API requests
|
||||
export type ApiInputInvoice = {
|
||||
/* Used for Invoice UI */
|
||||
export type ApiInputInvoiceMessage = {
|
||||
type: 'message';
|
||||
chatId: string;
|
||||
messageId: number;
|
||||
isExtendedMedia?: boolean;
|
||||
} | {
|
||||
};
|
||||
|
||||
export type ApiInputInvoiceSlug = {
|
||||
type: 'slug';
|
||||
slug: string;
|
||||
};
|
||||
|
||||
export type ApiRequestInputInvoice = {
|
||||
export type ApiInputInvoiceGiveaway = {
|
||||
type: 'giveaway';
|
||||
chatId: string;
|
||||
additionalChannelIds?: string[];
|
||||
isOnlyForNewSubscribers?: boolean;
|
||||
areWinnersVisible?: boolean;
|
||||
prizeDescription?: string;
|
||||
countries?: string[];
|
||||
untilDate: number;
|
||||
currency: string;
|
||||
amount: number;
|
||||
option: ApiPremiumGiftCodeOption;
|
||||
};
|
||||
|
||||
export type ApiInputInvoiceGiftCode = {
|
||||
type: 'giftcode';
|
||||
userIds: string[];
|
||||
boostChannelId?: string;
|
||||
currency: string;
|
||||
amount: number;
|
||||
option: ApiPremiumGiftCodeOption;
|
||||
};
|
||||
|
||||
export type ApiInputInvoice = ApiInputInvoiceMessage | ApiInputInvoiceSlug | ApiInputInvoiceGiveaway
|
||||
| ApiInputInvoiceGiftCode;
|
||||
|
||||
/* Used for Invoice request */
|
||||
export type ApiRequestInputInvoiceMessage = {
|
||||
type: 'message';
|
||||
chat: ApiChat;
|
||||
messageId: number;
|
||||
} | {
|
||||
};
|
||||
|
||||
export type ApiRequestInputInvoiceSlug = {
|
||||
type: 'slug';
|
||||
slug: string;
|
||||
};
|
||||
|
||||
export type ApiRequestInputInvoiceGiveaway = {
|
||||
type: 'giveaway';
|
||||
purpose: ApiInputStorePaymentPurpose;
|
||||
option: ApiPremiumGiftCodeOption;
|
||||
};
|
||||
|
||||
export type ApiRequestInputInvoice = ApiRequestInputInvoiceMessage | ApiRequestInputInvoiceSlug
|
||||
| ApiRequestInputInvoiceGiveaway;
|
||||
|
||||
export interface ApiInvoice {
|
||||
text: string;
|
||||
title: string;
|
||||
|
||||
@ -185,6 +185,10 @@ export interface ApiAppConfig {
|
||||
premiumInvoiceSlug: string;
|
||||
premiumBotUsername: string;
|
||||
isPremiumPurchaseBlocked: boolean;
|
||||
isGiveawayGiftsPurchaseAvailable: boolean;
|
||||
giveawayAddPeersMax: number;
|
||||
giveawayBoostsPerPremium: number;
|
||||
giveawayCountriesMax: number;
|
||||
premiumPromoOrder: ApiPremiumSection[];
|
||||
defaultEmojiStatusesStickerSetId: string;
|
||||
maxUniqueReactions: number;
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import type { ApiPremiumSection } from '../../global/types';
|
||||
import type { ApiInvoiceContainer } from '../../types';
|
||||
import type { ApiWebDocument } from './bots';
|
||||
import type { ApiChat } from './chats';
|
||||
import type { ApiDocument, ApiMessageEntity, ApiPaymentCredentials } from './messages';
|
||||
import type { StatisticsOverviewPercentage } from './statistics';
|
||||
import type { PrepaidGiveaway, StatisticsOverviewPercentage } from './statistics';
|
||||
import type { ApiUser } from './users';
|
||||
|
||||
export interface ApiShippingAddress {
|
||||
streetLine1: string;
|
||||
@ -81,6 +83,36 @@ export interface ApiPremiumSubscriptionOption {
|
||||
botUrl: string;
|
||||
}
|
||||
|
||||
export type ApiInputStorePaymentGiveaway = {
|
||||
type: 'giveaway';
|
||||
isOnlyForNewSubscribers?: boolean;
|
||||
areWinnersVisible?: boolean;
|
||||
chat: ApiChat;
|
||||
additionalChannels?: ApiChat[];
|
||||
countries?: string[];
|
||||
prizeDescription?: string;
|
||||
untilDate: number;
|
||||
currency: string;
|
||||
amount: number;
|
||||
};
|
||||
|
||||
export type ApiInputStorePaymentGiftcode = {
|
||||
type: 'giftcode';
|
||||
users: ApiUser[];
|
||||
boostChannel?: ApiChat;
|
||||
currency: string;
|
||||
amount: number;
|
||||
};
|
||||
|
||||
export type ApiInputStorePaymentPurpose = ApiInputStorePaymentGiveaway | ApiInputStorePaymentGiftcode;
|
||||
|
||||
export interface ApiPremiumGiftCodeOption {
|
||||
users: number;
|
||||
months: number;
|
||||
currency: string;
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export type ApiBoostsStatus = {
|
||||
level: number;
|
||||
currentLevelBoosts: number;
|
||||
@ -89,6 +121,7 @@ export type ApiBoostsStatus = {
|
||||
hasMyBoost?: boolean;
|
||||
boostUrl: string;
|
||||
premiumSubscribers?: StatisticsOverviewPercentage;
|
||||
prepaidGiveaways?: PrepaidGiveaway[];
|
||||
};
|
||||
|
||||
export type ApiMyBoost = {
|
||||
@ -131,3 +164,10 @@ export type ApiCheckedGiftCode = {
|
||||
months: number;
|
||||
usedAt?: number;
|
||||
};
|
||||
|
||||
export interface ApiPrepaidGiveaway {
|
||||
id: string;
|
||||
months: number;
|
||||
quantity: number;
|
||||
date: number;
|
||||
}
|
||||
|
||||
@ -54,6 +54,7 @@ export interface ApiBoostStatistics {
|
||||
boosts: number;
|
||||
premiumSubscribers: StatisticsOverviewPercentage;
|
||||
remainingBoosts: number;
|
||||
prepaidGiveaways: PrepaidGiveaway[];
|
||||
}
|
||||
|
||||
export interface ApiMessagePublicForward {
|
||||
@ -105,6 +106,13 @@ export interface StatisticsOverviewPercentage {
|
||||
percentage: string;
|
||||
}
|
||||
|
||||
export interface PrepaidGiveaway {
|
||||
id: string;
|
||||
months: number;
|
||||
quantity: number;
|
||||
date: number;
|
||||
}
|
||||
|
||||
export interface StatisticsOverviewPeriod {
|
||||
maxDate: number;
|
||||
minDate: number;
|
||||
|
||||
16
src/assets/premium/GiftBlueRound.svg
Normal file
16
src/assets/premium/GiftBlueRound.svg
Normal file
@ -0,0 +1,16 @@
|
||||
<svg width="42" height="42" viewBox="0 0 42 42" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_3714_1812)">
|
||||
<circle cx="21" cy="21" r="21" fill="url(#paint0_linear_3714_1812)"/>
|
||||
<path d="M19.8104 14.6019H18.0718C17.186 14.6019 16.4983 14.3775 16.0088 13.9287C15.5193 13.4798 15.2745 12.9528 15.2745 12.3477C15.2745 11.7556 15.4718 11.2919 15.8663 10.9566C16.2608 10.6213 16.7716 10.4536 17.3987 10.4536C18.0637 10.4536 18.6319 10.687 19.1033 11.1536C19.5747 11.6203 19.8104 12.2464 19.8104 13.032V14.6019H21.976V13.032C21.976 12.2464 22.0625 11.6203 22.5343 11.1536C23.0061 10.687 23.5777 10.4536 24.2491 10.4536C24.8691 10.4536 25.3761 10.6213 25.7702 10.9566C26.1642 11.2919 26.3613 11.7556 26.3613 12.3477C26.3613 12.9528 26.1167 13.4798 25.6275 13.9287C25.1384 14.3775 24.4509 14.6019 23.5651 14.6019H21.976H27.3553C27.6828 14.2976 27.9407 13.9414 28.1289 13.5332C28.3172 13.1251 28.4113 12.6776 28.4113 12.1909C28.4113 11.4767 28.2318 10.8412 27.8729 10.2845C27.5139 9.7278 27.0326 9.29235 26.4289 8.97814C25.8252 8.66394 25.1494 8.50684 24.4017 8.50684C23.578 8.50684 22.8469 8.71684 22.2084 9.13684C21.5698 9.55685 21.1083 10.1509 20.824 10.9191C20.5397 10.1509 20.0764 9.55685 19.4341 9.13684C18.7918 8.71684 18.0589 8.50684 17.2354 8.50684C16.4948 8.50684 15.8208 8.66394 15.2136 8.97814C14.6063 9.29235 14.1231 9.7278 13.7641 10.2845C13.4051 10.8412 13.2256 11.4767 13.2256 12.1909C13.2256 12.6776 13.3197 13.1251 13.508 13.5332C13.6962 13.9414 13.9542 14.2976 14.2817 14.6019H19.8104Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.8065 14.5815V17.6802C19.8065 17.7095 19.8136 17.7383 19.8272 17.7642C19.8736 17.8526 19.9829 17.8867 20.0713 17.8403L20.5552 17.5864C20.7656 17.476 21.0168 17.476 21.2272 17.5864L21.7111 17.8403C21.737 17.8539 21.7659 17.861 21.7951 17.861C21.895 17.861 21.9759 17.7801 21.9759 17.6802V14.5815H30.2919C31.0906 14.5815 31.7382 15.2291 31.7382 16.0278V18.9203C31.7382 19.7191 31.0906 20.3666 30.2919 20.3666H29.5688L29.5689 20.3847C29.5105 20.3728 29.45 20.3666 29.388 20.3666L25.853 20.366C26.5258 20.5657 26.9995 21.1976 26.9801 21.9201L26.9762 21.9965C26.9717 22.0565 26.9639 22.116 26.9527 22.1745L29.388 22.1744C29.45 22.1744 29.5105 22.1682 29.5689 22.1563L29.5688 28.6826C29.5688 30.2801 28.2737 31.5751 26.6762 31.5751H21.9759V28.6307C21.9759 28.4026 21.8684 28.1892 21.6881 28.0533L21.6371 28.0182L21.0834 27.6707C20.9659 27.597 20.8165 27.597 20.699 27.6707L20.1453 28.0182C19.9345 28.1505 19.8065 28.3819 19.8065 28.6307V31.5751H15.1062C13.5087 31.5751 12.2136 30.2801 12.2136 28.6826V22.1744L14.8451 22.1744C14.7669 21.7644 14.8508 21.3259 15.1087 20.9637L15.1625 20.8923C15.3642 20.64 15.6366 20.4579 15.9408 20.3664L12.0329 20.3666H11.4905C10.6918 20.3666 10.0442 19.7191 10.0442 18.9203V16.0278C10.0442 15.2291 10.6918 14.5815 11.4905 14.5815H19.8065ZM20.4385 18.8781L19.4063 21.2551C19.3332 21.4233 19.1731 21.5371 18.9903 21.5507L16.3586 21.7472C16.2218 21.7574 16.0954 21.8241 16.0098 21.9312C15.8396 22.1441 15.8742 22.4547 16.0871 22.6249L16.9531 23.3172C17.3653 23.6467 17.8959 23.7903 18.4181 23.7135L20.7655 23.3685C20.8642 23.3539 20.961 23.4052 21.0044 23.4951C21.0592 23.6087 21.0115 23.7452 20.898 23.8L18.8773 24.7754C18.4277 24.9924 18.0952 25.3947 17.9668 25.877L17.6241 27.1635C17.5903 27.2906 17.6086 27.4259 17.6749 27.5395C17.8125 27.7749 18.1148 27.8541 18.3501 27.7166L20.6422 26.3771C20.796 26.2872 20.9864 26.2872 21.1402 26.3771L23.4512 27.7277C23.5629 27.7929 23.6957 27.8117 23.821 27.7801C24.0853 27.7134 24.2455 27.4451 24.1788 27.1808L23.563 24.7403C23.516 24.5542 23.5812 24.3577 23.7301 24.2366L25.7138 22.6234C25.8191 22.5378 25.8844 22.4126 25.8945 22.2773C25.9148 22.0054 25.7109 21.7686 25.4391 21.7483L22.7921 21.5507C22.6093 21.5371 22.4492 21.4233 22.3762 21.2551L21.3439 18.8781C21.2941 18.7634 21.2025 18.6718 21.0878 18.622C20.8378 18.5134 20.5471 18.6281 20.4385 18.8781Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_3714_1812" x1="21" y1="0" x2="21" y2="42" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#5CAFFA"/>
|
||||
<stop offset="1" stop-color="#408ACF"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_3714_1812">
|
||||
<rect width="42" height="42" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.1 KiB |
21
src/assets/premium/GiftGreenRound.svg
Normal file
21
src/assets/premium/GiftGreenRound.svg
Normal file
@ -0,0 +1,21 @@
|
||||
<svg width="42" height="42" viewBox="0 0 42 42" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2257_22597)">
|
||||
<g clip-path="url(#clip1_2257_22597)">
|
||||
<circle cx="21" cy="21" r="21" fill="url(#paint0_linear_2257_22597)"/>
|
||||
<path d="M19.8104 14.6024H18.0718C17.186 14.6024 16.4983 14.378 16.0088 13.9292C15.5193 13.4803 15.2745 12.9533 15.2745 12.3482C15.2745 11.7561 15.4718 11.2924 15.8663 10.9571C16.2608 10.6218 16.7716 10.4541 17.3987 10.4541C18.0637 10.4541 18.6319 10.6874 19.1033 11.1541C19.5747 11.6208 19.8104 12.2469 19.8104 13.0324V14.6024H21.976V13.0324C21.976 12.2469 22.0625 11.6208 22.5343 11.1541C23.0061 10.6874 23.5777 10.4541 24.2491 10.4541C24.8691 10.4541 25.3761 10.6218 25.7702 10.9571C26.1642 11.2924 26.3613 11.7561 26.3613 12.3482C26.3613 12.9533 26.1167 13.4803 25.6276 13.9292C25.1384 14.378 24.4509 14.6024 23.5651 14.6024H21.976H27.3553C27.6828 14.2981 27.9407 13.9419 28.1289 13.5337C28.3172 13.1255 28.4113 12.6781 28.4113 12.1914C28.4113 11.4772 28.2318 10.8417 27.8729 10.285C27.5139 9.72829 27.0326 9.29283 26.4289 8.97863C25.8252 8.66443 25.1494 8.50732 24.4017 8.50732C23.5781 8.50732 22.8469 8.71733 22.2084 9.13733C21.5698 9.55734 21.1083 10.1514 20.824 10.9196C20.5397 10.1514 20.0764 9.55734 19.4341 9.13733C18.7918 8.71733 18.0589 8.50732 17.2354 8.50732C16.4948 8.50732 15.8209 8.66443 15.2136 8.97863C14.6063 9.29283 14.1231 9.72829 13.7641 10.285C13.4051 10.8417 13.2256 11.4772 13.2256 12.1914C13.2256 12.6781 13.3197 13.1255 13.508 13.5337C13.6962 13.9419 13.9542 14.2981 14.2818 14.6024H19.8104Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.8064 14.5815V17.6802C19.8064 17.7095 19.8136 17.7383 19.8271 17.7642C19.8735 17.8526 19.9828 17.8867 20.0712 17.8403L20.5552 17.5864C20.7656 17.476 21.0167 17.476 21.2271 17.5864L21.7111 17.8403C21.737 17.8539 21.7658 17.861 21.7951 17.861C21.8949 17.861 21.9758 17.7801 21.9758 17.6802V14.5815H30.2918C31.0906 14.5815 31.7381 15.2291 31.7381 16.0278V18.9203C31.7381 19.7191 31.0906 20.3666 30.2918 20.3666H29.5687L29.5689 20.3847C29.5104 20.3728 29.4499 20.3666 29.3879 20.3666L25.8529 20.366C26.5257 20.5657 26.9995 21.1976 26.9801 21.9201L26.9762 21.9965C26.9717 22.0565 26.9638 22.116 26.9526 22.1745L29.3879 22.1744C29.4499 22.1744 29.5104 22.1682 29.5689 22.1563L29.5687 28.6826C29.5687 30.2801 28.2737 31.5751 26.6762 31.5751H21.9758V28.6307C21.9758 28.4026 21.8683 28.1892 21.688 28.0533L21.6371 28.0182L21.0833 27.6707C20.9658 27.597 20.8165 27.597 20.699 27.6707L20.1452 28.0182C19.9344 28.1505 19.8064 28.3819 19.8064 28.6307V31.5751H15.1061C13.5086 31.5751 12.2136 30.2801 12.2136 28.6826V22.1744L14.8451 22.1744C14.7668 21.7644 14.8508 21.3259 15.1086 20.9637L15.1625 20.8923C15.3642 20.64 15.6365 20.4579 15.9408 20.3664L12.0328 20.3666H11.4905C10.6917 20.3666 10.0442 19.7191 10.0442 18.9203V16.0278C10.0442 15.2291 10.6917 14.5815 11.4905 14.5815H19.8064ZM20.4384 18.8781L19.4062 21.2551C19.3332 21.4233 19.173 21.5371 18.9902 21.5507L16.3585 21.7472C16.2217 21.7574 16.0954 21.8241 16.0097 21.9312C15.8395 22.1441 15.8741 22.4547 16.0871 22.6249L16.953 23.3172C17.3653 23.6467 17.8959 23.7903 18.418 23.7135L20.7654 23.3685C20.8642 23.3539 20.9609 23.4052 21.0043 23.4951C21.0591 23.6087 21.0115 23.7452 20.8979 23.8L18.8772 24.7754C18.4277 24.9924 18.0952 25.3947 17.9667 25.877L17.6241 27.1635C17.5902 27.2906 17.6085 27.4259 17.6749 27.5395C17.8124 27.7749 18.1147 27.8541 18.35 27.7166L20.6421 26.3771C20.796 26.2872 20.9863 26.2872 21.1402 26.3771L23.4512 27.7277C23.5628 27.7929 23.6956 27.8117 23.821 27.7801C24.0853 27.7134 24.2455 27.4451 24.1788 27.1808L23.5629 24.7403C23.516 24.5542 23.5812 24.3577 23.7301 24.2366L25.7137 22.6234C25.819 22.5378 25.8844 22.4126 25.8945 22.2773C25.9148 22.0054 25.7109 21.7686 25.439 21.7483L22.7921 21.5507C22.6093 21.5371 22.4491 21.4233 22.3761 21.2551L21.3439 18.8781C21.294 18.7634 21.2025 18.6718 21.0877 18.622C20.8377 18.5134 20.547 18.6281 20.4384 18.8781Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_2257_22597" x1="21" y1="0" x2="21" y2="42" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#9AD164"/>
|
||||
<stop offset="1" stop-color="#46BA43"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_2257_22597">
|
||||
<rect width="42" height="42" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_2257_22597">
|
||||
<rect width="42" height="42" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
21
src/assets/premium/GiftRedRound.svg
Normal file
21
src/assets/premium/GiftRedRound.svg
Normal file
@ -0,0 +1,21 @@
|
||||
<svg width="42" height="42" viewBox="0 0 42 42" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_2257_22664)">
|
||||
<g clip-path="url(#clip1_2257_22664)">
|
||||
<circle cx="21" cy="21" r="21" fill="url(#paint0_linear_2257_22664)"/>
|
||||
<path d="M19.8104 14.6024H18.0718C17.186 14.6024 16.4983 14.378 16.0088 13.9292C15.5193 13.4803 15.2745 12.9533 15.2745 12.3482C15.2745 11.7561 15.4718 11.2924 15.8663 10.9571C16.2608 10.6218 16.7716 10.4541 17.3987 10.4541C18.0637 10.4541 18.6319 10.6874 19.1033 11.1541C19.5747 11.6208 19.8104 12.2469 19.8104 13.0324V14.6024H21.976V13.0324C21.976 12.2469 22.0625 11.6208 22.5343 11.1541C23.0061 10.6874 23.5777 10.4541 24.2491 10.4541C24.8691 10.4541 25.3761 10.6218 25.7702 10.9571C26.1642 11.2924 26.3613 11.7561 26.3613 12.3482C26.3613 12.9533 26.1167 13.4803 25.6276 13.9292C25.1384 14.378 24.4509 14.6024 23.5651 14.6024H21.976H27.3553C27.6828 14.2981 27.9407 13.9419 28.1289 13.5337C28.3172 13.1255 28.4113 12.6781 28.4113 12.1914C28.4113 11.4772 28.2318 10.8417 27.8729 10.285C27.5139 9.72829 27.0326 9.29283 26.4289 8.97863C25.8252 8.66443 25.1494 8.50732 24.4017 8.50732C23.5781 8.50732 22.8469 8.71733 22.2084 9.13733C21.5698 9.55734 21.1083 10.1514 20.824 10.9196C20.5397 10.1514 20.0764 9.55734 19.4341 9.13733C18.7918 8.71733 18.0589 8.50732 17.2354 8.50732C16.4948 8.50732 15.8209 8.66443 15.2136 8.97863C14.6063 9.29283 14.1231 9.72829 13.7641 10.285C13.4051 10.8417 13.2256 11.4772 13.2256 12.1914C13.2256 12.6781 13.3197 13.1255 13.508 13.5337C13.6962 13.9419 13.9542 14.2981 14.2818 14.6024H19.8104Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M19.8064 14.5815V17.6802C19.8064 17.7095 19.8136 17.7383 19.8271 17.7642C19.8735 17.8526 19.9828 17.8867 20.0712 17.8403L20.5552 17.5864C20.7656 17.476 21.0167 17.476 21.2271 17.5864L21.7111 17.8403C21.737 17.8539 21.7658 17.861 21.7951 17.861C21.8949 17.861 21.9758 17.7801 21.9758 17.6802V14.5815H30.2918C31.0906 14.5815 31.7381 15.2291 31.7381 16.0278V18.9203C31.7381 19.7191 31.0906 20.3666 30.2918 20.3666H29.5687L29.5689 20.3847C29.5104 20.3728 29.4499 20.3666 29.3879 20.3666L25.8529 20.366C26.5257 20.5657 26.9995 21.1976 26.9801 21.9201L26.9762 21.9965C26.9717 22.0565 26.9638 22.116 26.9526 22.1745L29.3879 22.1744C29.4499 22.1744 29.5104 22.1682 29.5689 22.1563L29.5687 28.6826C29.5687 30.2801 28.2737 31.5751 26.6762 31.5751H21.9758V28.6307C21.9758 28.4026 21.8683 28.1892 21.688 28.0533L21.6371 28.0182L21.0833 27.6707C20.9658 27.597 20.8165 27.597 20.699 27.6707L20.1452 28.0182C19.9344 28.1505 19.8064 28.3819 19.8064 28.6307V31.5751H15.1061C13.5086 31.5751 12.2136 30.2801 12.2136 28.6826V22.1744L14.8451 22.1744C14.7668 21.7644 14.8508 21.3259 15.1086 20.9637L15.1625 20.8923C15.3642 20.64 15.6365 20.4579 15.9408 20.3664L12.0328 20.3666H11.4905C10.6917 20.3666 10.0442 19.7191 10.0442 18.9203V16.0278C10.0442 15.2291 10.6917 14.5815 11.4905 14.5815H19.8064ZM20.4384 18.8781L19.4062 21.2551C19.3332 21.4233 19.173 21.5371 18.9902 21.5507L16.3585 21.7472C16.2217 21.7574 16.0954 21.8241 16.0097 21.9312C15.8395 22.1441 15.8741 22.4547 16.0871 22.6249L16.953 23.3172C17.3653 23.6467 17.8959 23.7903 18.418 23.7135L20.7654 23.3685C20.8642 23.3539 20.9609 23.4052 21.0043 23.4951C21.0591 23.6087 21.0115 23.7452 20.8979 23.8L18.8772 24.7754C18.4277 24.9924 18.0952 25.3947 17.9667 25.877L17.6241 27.1635C17.5902 27.2906 17.6085 27.4259 17.6749 27.5395C17.8124 27.7749 18.1147 27.8541 18.35 27.7166L20.6421 26.3771C20.796 26.2872 20.9863 26.2872 21.1402 26.3771L23.4512 27.7277C23.5628 27.7929 23.6956 27.8117 23.821 27.7801C24.0853 27.7134 24.2455 27.4451 24.1788 27.1808L23.5629 24.7403C23.516 24.5542 23.5812 24.3577 23.7301 24.2366L25.7137 22.6234C25.819 22.5378 25.8844 22.4126 25.8945 22.2773C25.9148 22.0054 25.7109 21.7686 25.439 21.7483L22.7921 21.5507C22.6093 21.5371 22.4491 21.4233 22.3761 21.2551L21.3439 18.8781C21.294 18.7634 21.2025 18.6718 21.0877 18.622C20.8377 18.5134 20.547 18.6281 20.4384 18.8781Z" fill="white"/>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_2257_22664" x1="21" y1="0" x2="21" y2="42" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF845E"/>
|
||||
<stop offset="1" stop-color="#D45246"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_2257_22664">
|
||||
<rect width="42" height="42" fill="white"/>
|
||||
</clipPath>
|
||||
<clipPath id="clip1_2257_22664">
|
||||
<rect width="42" height="42" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 4.3 KiB |
15
src/assets/premium/GiveawayUsersRound.svg
Normal file
15
src/assets/premium/GiveawayUsersRound.svg
Normal file
@ -0,0 +1,15 @@
|
||||
<svg width="42" height="42" viewBox="0 0 42 42" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g clip-path="url(#clip0_3714_1830)">
|
||||
<circle cx="21" cy="21" r="21" fill="url(#paint0_linear_3714_1830)"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M17.5858 18.3749C19.6155 18.3749 21.2608 16.7295 21.2608 14.6999C21.2608 12.6703 19.6155 11.0249 17.5858 11.0249C15.5562 11.0249 13.9108 12.6703 13.9108 14.6999C13.9108 16.7295 15.5562 18.3749 17.5858 18.3749ZM25.9877 18.3749C27.7274 18.3749 29.1377 16.9646 29.1377 15.2249C29.1377 13.4852 27.7274 12.0749 25.9877 12.0749C24.248 12.0749 22.8377 13.4852 22.8377 15.2249C22.8377 16.9646 24.248 18.3749 25.9877 18.3749ZM22.3466 22.0789C23.8955 22.9225 24.8683 24.1514 25.4793 25.3623C25.8218 26.0412 25.8486 26.709 25.648 27.2999C25.2367 28.5114 23.87 29.3999 22.3108 29.3999H12.8608C10.5412 29.3999 8.6475 27.4332 9.69238 25.3623C10.7785 23.2096 13.0083 20.9999 17.5858 20.9999C19.4037 20.9999 20.8512 21.3484 22.004 21.9034C22.0078 21.9052 22.0116 21.9071 22.0154 21.9089C22.1286 21.9636 22.239 22.0203 22.3466 22.0789ZM23.6023 21.2025C25.116 22.1853 26.0961 23.4846 26.7261 24.7332C27.1679 25.6088 27.2607 26.4923 27.0924 27.2999H29.6615C31.4012 27.2999 32.8196 25.8279 32.0503 24.2676C31.2301 22.6041 29.5262 20.8763 25.9865 20.8763C25.0693 20.8763 24.2753 20.9924 23.588 21.1933L23.6023 21.2025Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient id="paint0_linear_3714_1830" x1="21" y1="0" x2="21" y2="42" gradientUnits="userSpaceOnUse">
|
||||
<stop stop-color="#FF8AAC"/>
|
||||
<stop offset="1" stop-color="#D95574"/>
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_3714_1830">
|
||||
<rect width="42" height="42" fill="white"/>
|
||||
</clipPath>
|
||||
</defs>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
@ -17,6 +17,8 @@ export { default as AttachBotInstallModal } from '../components/modals/attachBot
|
||||
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 GiveawayModal } from '../components/main/premium/GiveawayModal';
|
||||
export { default as AppendEntityPickerModal } from '../components/main/AppendEntityPickerModal';
|
||||
export { default as PremiumLimitReachedModal } from '../components/main/premium/common/PremiumLimitReachedModal';
|
||||
export { default as StatusPickerMenu } from '../components/left/main/StatusPickerMenu';
|
||||
export { default as BoostModal } from '../components/modals/boost/BoostModal';
|
||||
@ -31,6 +33,7 @@ export { default as UnpinAllMessagesModal } from '../components/common/UnpinAllM
|
||||
export { default as MessageSelectToolbar } from '../components/middle/MessageSelectToolbar';
|
||||
export { default as SeenByModal } from '../components/common/SeenByModal';
|
||||
export { default as PrivacySettingsNoticeModal } from '../components/common/PrivacySettingsNoticeModal';
|
||||
export { default as CountryPickerModal } from '../components/common/CountryPickerModal';
|
||||
export { default as ReactorListModal } from '../components/middle/ReactorListModal';
|
||||
export { default as EmojiInteractionAnimation } from '../components/middle/EmojiInteractionAnimation';
|
||||
export { default as ChatLanguageModal } from '../components/middle/ChatLanguageModal';
|
||||
|
||||
@ -74,7 +74,7 @@ const CalendarModal: FC<OwnProps> = ({
|
||||
|
||||
const passedSelectedDate = useMemo(() => (selectedAt ? new Date(selectedAt) : new Date()), [selectedAt]);
|
||||
const prevIsOpen = usePrevious(isOpen);
|
||||
const [isTimeInputFocused, markTimeInputAsFocused, unmarkTimeInputAsFocused] = useFlag(false);
|
||||
const [isTimeInputFocused, markTimeInputAsFocused] = useFlag(false);
|
||||
|
||||
const [selectedDate, setSelectedDate] = useState<Date>(passedSelectedDate);
|
||||
const [currentMonthAndYear, setCurrentMonthAndYear] = useState<Date>(
|
||||
@ -91,6 +91,9 @@ const CalendarModal: FC<OwnProps> = ({
|
||||
const currentYear = currentMonthAndYear.getFullYear();
|
||||
const currentMonth = currentMonthAndYear.getMonth();
|
||||
|
||||
const isDisabled = (isFutureMode && selectedDate.getTime() < minDate.getTime())
|
||||
|| (isPastMode && selectedDate.getTime() > maxDate.getTime());
|
||||
|
||||
useEffect(() => {
|
||||
if (!prevIsOpen && isOpen) {
|
||||
setSelectedDate(passedSelectedDate);
|
||||
@ -169,8 +172,14 @@ const CalendarModal: FC<OwnProps> = ({
|
||||
}
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
onSubmit(selectedDate);
|
||||
}, [onSubmit, selectedDate]);
|
||||
if (isFutureMode && selectedDate < minDate) {
|
||||
onSubmit(minDate);
|
||||
} else if (isPastMode && selectedDate > maxDate) {
|
||||
onSubmit(maxDate);
|
||||
} else {
|
||||
onSubmit(selectedDate);
|
||||
}
|
||||
}, [isFutureMode, isPastMode, minDate, maxDate, onSubmit, selectedDate]);
|
||||
|
||||
const handleChangeHours = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const value = e.target.value.replace(/[^\d]+/g, '');
|
||||
@ -220,7 +229,6 @@ const CalendarModal: FC<OwnProps> = ({
|
||||
value={selectedHours}
|
||||
onChange={handleChangeHours}
|
||||
onFocus={markTimeInputAsFocused}
|
||||
onBlur={unmarkTimeInputAsFocused}
|
||||
/>
|
||||
:
|
||||
<input
|
||||
@ -230,7 +238,6 @@ const CalendarModal: FC<OwnProps> = ({
|
||||
value={selectedMinutes}
|
||||
onChange={handleChangeMinutes}
|
||||
onFocus={markTimeInputAsFocused}
|
||||
onBlur={unmarkTimeInputAsFocused}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
@ -322,14 +329,19 @@ const CalendarModal: FC<OwnProps> = ({
|
||||
{withTimePicker && renderTimePicker()}
|
||||
|
||||
<div className="footer">
|
||||
<Button onClick={handleSubmit}>
|
||||
{submitLabel}
|
||||
</Button>
|
||||
{secondButtonLabel && (
|
||||
<Button onClick={onSecondButtonClick} isText>
|
||||
{secondButtonLabel}
|
||||
<div className="footer">
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={isDisabled}
|
||||
>
|
||||
{submitLabel}
|
||||
</Button>
|
||||
)}
|
||||
{secondButtonLabel && (
|
||||
<Button onClick={onSecondButtonClick} isText>
|
||||
{secondButtonLabel}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
|
||||
18
src/components/common/CountryPickerModal.async.tsx
Normal file
18
src/components/common/CountryPickerModal.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 './CountryPickerModal';
|
||||
|
||||
import { Bundles } from '../../util/moduleLoader';
|
||||
|
||||
import useModuleLoader from '../../hooks/useModuleLoader';
|
||||
|
||||
const CountryPickerModalAsync: FC<OwnProps> = (props) => {
|
||||
const { isOpen } = props;
|
||||
const CountryPickerModal = useModuleLoader(Bundles.Extra, 'CountryPickerModal', !isOpen);
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading,react/jsx-no-undef
|
||||
return CountryPickerModal ? <CountryPickerModal {...props} /> : undefined;
|
||||
};
|
||||
|
||||
export default CountryPickerModalAsync;
|
||||
29
src/components/common/CountryPickerModal.module.scss
Normal file
29
src/components/common/CountryPickerModal.module.scss
Normal file
@ -0,0 +1,29 @@
|
||||
.root :global(.modal-dialog) {
|
||||
max-width: 55vh;
|
||||
}
|
||||
|
||||
.pickerSelector {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.pickerTitle {
|
||||
flex: 1;
|
||||
margin: 0 0 0 1.25rem;
|
||||
font-size: 1.25rem;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
margin-left: 0.75rem;
|
||||
}
|
||||
}
|
||||
|
||||
.picker {
|
||||
height: 70vh;
|
||||
}
|
||||
|
||||
.footer {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
118
src/components/common/CountryPickerModal.tsx
Normal file
118
src/components/common/CountryPickerModal.tsx
Normal file
@ -0,0 +1,118 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useMemo, useState,
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions } from '../../global';
|
||||
|
||||
import type { ApiCountry } from '../../api/types';
|
||||
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import usePrevious from '../../hooks/usePrevious';
|
||||
|
||||
import Button from '../ui/Button';
|
||||
import Modal from '../ui/Modal';
|
||||
import Icon from './Icon';
|
||||
import Picker from './Picker';
|
||||
|
||||
import styles from './CountryPickerModal.module.scss';
|
||||
|
||||
export type OwnProps = {
|
||||
isOpen: boolean;
|
||||
onClose: () => void;
|
||||
onSubmit: (value: string[]) => void;
|
||||
countryList: ApiCountry[];
|
||||
selectionLimit?: number | undefined;
|
||||
};
|
||||
|
||||
const CountryPickerModal: FC<OwnProps> = ({
|
||||
isOpen,
|
||||
onClose,
|
||||
onSubmit,
|
||||
countryList,
|
||||
selectionLimit,
|
||||
}) => {
|
||||
const { showNotification } = getActions();
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
const [selectedCountryIds, setSelectedCountryIds] = useState<string[]>([]);
|
||||
const prevSelectedCountryIds = usePrevious(selectedCountryIds);
|
||||
const noPickerScrollRestore = prevSelectedCountryIds === selectedCountryIds;
|
||||
|
||||
const displayedIds = useMemo(() => {
|
||||
if (!countryList) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return countryList
|
||||
.filter((country) => !country.isHidden)
|
||||
.map((country) => country.iso2);
|
||||
}, [countryList]);
|
||||
|
||||
const handleSelectedIdsChange = useLastCallback((newSelectedIds: string[]) => {
|
||||
if (selectionLimit && newSelectedIds.length > selectionLimit) {
|
||||
showNotification({
|
||||
message: lang('BoostingSelectUpToWarningCountries', selectionLimit),
|
||||
});
|
||||
return;
|
||||
}
|
||||
setSelectedCountryIds(newSelectedIds);
|
||||
});
|
||||
|
||||
const handleSubmit = useLastCallback(() => {
|
||||
onSubmit(selectedCountryIds);
|
||||
onClose();
|
||||
});
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={styles.root}
|
||||
isOpen={isOpen}
|
||||
onClose={onClose}
|
||||
onEnter={handleSubmit}
|
||||
>
|
||||
<div className={styles.container}>
|
||||
<div className={styles.pickerSelector}>
|
||||
<Button
|
||||
round
|
||||
size="smaller"
|
||||
color="translucent"
|
||||
onClick={onClose}
|
||||
>
|
||||
<Icon name="close" />
|
||||
</Button>
|
||||
|
||||
<h4 className={styles.pickerTitle}>
|
||||
{lang('BoostingSelectCountry')}
|
||||
</h4>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={buildClassName(styles.main, 'custom-scroll')}>
|
||||
<Picker
|
||||
className={styles.picker}
|
||||
itemIds={displayedIds}
|
||||
selectedIds={selectedCountryIds}
|
||||
onSelectedIdsChange={handleSelectedIdsChange}
|
||||
noScrollRestore={noPickerScrollRestore}
|
||||
isCountryList
|
||||
countryList={countryList}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.footer}>
|
||||
<Button
|
||||
size="smaller"
|
||||
onClick={handleSubmit}
|
||||
>
|
||||
{lang('SelectCountries.OK')}
|
||||
</Button>
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(CountryPickerModal);
|
||||
@ -3,9 +3,12 @@ import React, {
|
||||
memo, useEffect, useMemo, useRef,
|
||||
} from '../../lib/teact/teact';
|
||||
|
||||
import type { ApiCountry } from '../../api/types';
|
||||
|
||||
import { requestMeasure } from '../../lib/fasterdom/fasterdom';
|
||||
import { isUserId } from '../../global/helpers';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { buildCollectionByKey } from '../../util/iteratees';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
|
||||
|
||||
import useInfiniteScroll from '../../hooks/useInfiniteScroll';
|
||||
@ -42,6 +45,8 @@ type OwnProps = {
|
||||
onFilterChange?: (value: string) => void;
|
||||
onDisabledClick?: (id: string) => void;
|
||||
onLoadMore?: () => void;
|
||||
isCountryList?: boolean;
|
||||
countryList?: ApiCountry[];
|
||||
};
|
||||
|
||||
// Focus slows down animation, also it breaks transition layout in Chrome
|
||||
@ -69,6 +74,8 @@ const Picker: FC<OwnProps> = ({
|
||||
onFilterChange,
|
||||
onDisabledClick,
|
||||
onLoadMore,
|
||||
isCountryList,
|
||||
countryList,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
@ -92,17 +99,18 @@ const Picker: FC<OwnProps> = ({
|
||||
const lockedIdsSet = useMemo(() => new Set(lockedIds), [lockedIds]);
|
||||
|
||||
const sortedItemIds = useMemo(() => {
|
||||
return itemIds.sort((a, b) => {
|
||||
const aIsLocked = lockedIdsSet.has(a);
|
||||
const bIsLocked = lockedIdsSet.has(b);
|
||||
if (aIsLocked && !bIsLocked) {
|
||||
return -1;
|
||||
const lockedBucket: string[] = [];
|
||||
const unlockedBucket: string[] = [];
|
||||
|
||||
itemIds.forEach((id) => {
|
||||
if (lockedIdsSet.has(id)) {
|
||||
lockedBucket.push(id);
|
||||
} else {
|
||||
unlockedBucket.push(id);
|
||||
}
|
||||
if (!aIsLocked && bIsLocked) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
});
|
||||
|
||||
return lockedBucket.concat(unlockedBucket);
|
||||
}, [itemIds, lockedIdsSet]);
|
||||
|
||||
const handleItemClick = useLastCallback((id: string) => {
|
||||
@ -130,6 +138,22 @@ const Picker: FC<OwnProps> = ({
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
const countriesByIso = useMemo(() => {
|
||||
if (!countryList) return undefined;
|
||||
return buildCollectionByKey(countryList, 'iso2');
|
||||
}, [countryList]);
|
||||
|
||||
const renderChatInfo = (id: string) => {
|
||||
if (isCountryList && countriesByIso) {
|
||||
const country = countriesByIso[id];
|
||||
return <div>{country.defaultName}</div>;
|
||||
} else if (isUserId(id)) {
|
||||
return <PrivateChatInfo forceShowSelf={forceShowSelf} userId={id} />;
|
||||
} else {
|
||||
return <GroupChatInfo chatId={id} />;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={buildClassName('Picker', className)}>
|
||||
{isSearchable && (
|
||||
@ -194,11 +218,7 @@ const Picker: FC<OwnProps> = ({
|
||||
ripple
|
||||
>
|
||||
{!isRoundCheckbox ? renderCheckbox() : undefined}
|
||||
{isUserId(id) ? (
|
||||
<PrivateChatInfo forceShowSelf={forceShowSelf} userId={id} />
|
||||
) : (
|
||||
<GroupChatInfo chatId={id} />
|
||||
)}
|
||||
{renderChatInfo(id)}
|
||||
{isRoundCheckbox ? renderCheckbox() : undefined}
|
||||
</ListItem>
|
||||
);
|
||||
|
||||
@ -2,7 +2,7 @@ import type { ApiBoostsStatus } from '../../../api/types';
|
||||
|
||||
export function getBoostProgressInfo(boostInfo: ApiBoostsStatus, freezeOnLevelUp?: boolean) {
|
||||
const {
|
||||
level, boosts, currentLevelBoosts, nextLevelBoosts, hasMyBoost,
|
||||
level, boosts, currentLevelBoosts, nextLevelBoosts, hasMyBoost, prepaidGiveaways,
|
||||
} = boostInfo;
|
||||
|
||||
const isJustUpgraded = freezeOnLevelUp && boosts === currentLevelBoosts && hasMyBoost;
|
||||
@ -23,5 +23,6 @@ export function getBoostProgressInfo(boostInfo: ApiBoostsStatus, freezeOnLevelUp
|
||||
levelProgress,
|
||||
remainingBoosts,
|
||||
isMaxLevel,
|
||||
prepaidGiveaways,
|
||||
};
|
||||
}
|
||||
|
||||
18
src/components/main/AppendEntityPicker.async.tsx
Normal file
18
src/components/main/AppendEntityPicker.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 './AppendEntityPickerModal';
|
||||
|
||||
import { Bundles } from '../../util/moduleLoader';
|
||||
|
||||
import useModuleLoader from '../../hooks/useModuleLoader';
|
||||
|
||||
const AppendEntityPickerModalAsync: FC<OwnProps> = (props) => {
|
||||
const { isOpen } = props;
|
||||
const AppendEntityPickerModal = useModuleLoader(Bundles.Extra, 'AppendEntityPickerModal', !isOpen);
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return AppendEntityPickerModal ? <AppendEntityPickerModal {...props} /> : undefined;
|
||||
};
|
||||
|
||||
export default AppendEntityPickerModalAsync;
|
||||
46
src/components/main/AppendEntityPicker.module.scss
Normal file
46
src/components/main/AppendEntityPicker.module.scss
Normal file
@ -0,0 +1,46 @@
|
||||
.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: 70vh;
|
||||
}
|
||||
277
src/components/main/AppendEntityPickerModal.tsx
Normal file
277
src/components/main/AppendEntityPickerModal.tsx
Normal file
@ -0,0 +1,277 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
memo,
|
||||
useMemo,
|
||||
useState,
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions, getGlobal, withGlobal } from '../../global';
|
||||
|
||||
import type { ApiChat, ApiChatMember, ApiUserStatus } from '../../api/types';
|
||||
|
||||
import {
|
||||
filterChatsByName,
|
||||
filterUsersByName, isChatChannel, isChatPublic, isUserBot, sortUserIds,
|
||||
} from '../../global/helpers';
|
||||
import { selectChat, selectChatFullInfo } from '../../global/selectors';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { unique } from '../../util/iteratees';
|
||||
import sortChatIds from '../common/helpers/sortChatIds';
|
||||
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
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 ConfirmDialog from '../ui/ConfirmDialog';
|
||||
import Modal from '../ui/Modal';
|
||||
|
||||
import styles from './AppendEntityPicker.module.scss';
|
||||
|
||||
export type OwnProps = {
|
||||
isOpen?: boolean;
|
||||
onClose: () => void;
|
||||
chatId?: string;
|
||||
entityType: 'members' | 'channels' | undefined;
|
||||
onSubmit: (value: string[]) => void;
|
||||
selectionLimit: number;
|
||||
};
|
||||
|
||||
interface StateProps {
|
||||
chatId?: string;
|
||||
members?: ApiChatMember[];
|
||||
adminMembersById?: Record<string, ApiChatMember>;
|
||||
userStatusesById: Record<string, ApiUserStatus>;
|
||||
channelList?: (ApiChat | undefined)[] | undefined;
|
||||
isChannel?: boolean;
|
||||
currentUserId?: string | undefined;
|
||||
}
|
||||
|
||||
const AppendEntityPickerModal: FC<OwnProps & StateProps> = ({
|
||||
chatId,
|
||||
isOpen,
|
||||
onClose,
|
||||
members,
|
||||
adminMembersById,
|
||||
userStatusesById,
|
||||
entityType,
|
||||
isChannel,
|
||||
onSubmit,
|
||||
currentUserId,
|
||||
selectionLimit,
|
||||
}) => {
|
||||
const { showNotification } = getActions();
|
||||
|
||||
const lang = useLang();
|
||||
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useFlag();
|
||||
|
||||
const [selectedChannelIds, setSelectedChannelIds] = useState<string[]>([]);
|
||||
const [selectedMemberIds, setSelectedMemberIds] = useState<string[]>([]);
|
||||
const [pendingChannelId, setPendingChannelId] = useState<string | undefined>(undefined);
|
||||
const [searchQuery, setSearchQuery] = useState<string>('');
|
||||
|
||||
const channelsIds = useMemo(() => {
|
||||
const chatsById = getGlobal().chats.byId;
|
||||
const activeChatIds = getGlobal().chats.listIds.active;
|
||||
|
||||
return activeChatIds!.map((id) => chatsById[id])
|
||||
.filter((chat) => chat && isChatChannel(chat) && chat.id !== chatId)
|
||||
.map((chat) => chat!.id);
|
||||
}, [chatId]);
|
||||
|
||||
const adminIds = useMemo(() => {
|
||||
return adminMembersById && Object.keys(adminMembersById);
|
||||
}, [adminMembersById]);
|
||||
|
||||
const memberIds = useMemo(() => {
|
||||
const usersById = getGlobal().users.byId;
|
||||
if (!members || !usersById) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const userIds = sortUserIds(
|
||||
members.map(({ userId }) => userId),
|
||||
usersById,
|
||||
userStatusesById,
|
||||
);
|
||||
|
||||
return adminIds ? userIds.filter((userId) => userId !== currentUserId) : userIds;
|
||||
}, [adminIds, currentUserId, members, userStatusesById]);
|
||||
|
||||
const displayedMembersIds = useMemo(() => {
|
||||
const usersById = getGlobal().users.byId;
|
||||
const filteredContactIds = memberIds ? filterUsersByName(memberIds, usersById, searchQuery) : [];
|
||||
|
||||
return sortChatIds(unique(filteredContactIds).filter((userId) => {
|
||||
const user = usersById[userId];
|
||||
if (!user) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !isUserBot(user);
|
||||
}));
|
||||
}, [memberIds, searchQuery]);
|
||||
|
||||
const displayedChannelIds = useMemo(() => {
|
||||
const chatsById = getGlobal().chats.byId;
|
||||
const foundChannelIds = channelsIds ? filterChatsByName(lang, channelsIds, chatsById, searchQuery) : [];
|
||||
|
||||
return sortChatIds(unique(foundChannelIds).filter((contactId) => {
|
||||
const chat = chatsById[contactId];
|
||||
if (!chat) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return isChannel;
|
||||
}),
|
||||
false,
|
||||
selectedChannelIds);
|
||||
}, [channelsIds, lang, searchQuery, selectedChannelIds, isChannel]);
|
||||
|
||||
const handleCloseButtonClick = useLastCallback(() => {
|
||||
onSubmit([]);
|
||||
onClose();
|
||||
});
|
||||
|
||||
const handleSendIdList = useLastCallback(() => {
|
||||
onSubmit(entityType === 'members' ? selectedMemberIds : selectedChannelIds);
|
||||
onClose();
|
||||
});
|
||||
|
||||
const confirmPrivateLinkChannelSelection = useLastCallback(() => {
|
||||
if (pendingChannelId) {
|
||||
setSelectedChannelIds((prevIds) => unique([...prevIds, pendingChannelId]));
|
||||
}
|
||||
closeConfirmModal();
|
||||
});
|
||||
|
||||
const handleSelectedMembersChange = useLastCallback((newSelectedIds: string[]) => {
|
||||
if (newSelectedIds.length > selectionLimit) {
|
||||
showNotification({
|
||||
message: lang('BoostingSelectUpToWarningUsers', selectionLimit),
|
||||
});
|
||||
return;
|
||||
}
|
||||
setSelectedMemberIds(newSelectedIds);
|
||||
});
|
||||
|
||||
const handleSelectedChannelIdsChange = useLastCallback((newSelectedIds: string[]) => {
|
||||
const chatsById = getGlobal().chats.byId;
|
||||
const newlyAddedIds = newSelectedIds.filter((id) => !selectedChannelIds.includes(id));
|
||||
const privateLinkChannelId = newlyAddedIds.find((id) => {
|
||||
const chat = chatsById[id];
|
||||
return chat && !isChatPublic(chat);
|
||||
});
|
||||
|
||||
if (selectedChannelIds?.length >= selectionLimit) {
|
||||
showNotification({
|
||||
message: lang('BoostingSelectUpToWarningChannelsPlural', selectionLimit),
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (privateLinkChannelId) {
|
||||
setPendingChannelId(privateLinkChannelId);
|
||||
openConfirmModal();
|
||||
} else {
|
||||
setSelectedChannelIds(newSelectedIds);
|
||||
}
|
||||
});
|
||||
|
||||
const handleClose = useLastCallback(() => {
|
||||
onSubmit([]);
|
||||
onClose();
|
||||
});
|
||||
|
||||
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={handleCloseButtonClick}
|
||||
ariaLabel={lang('Close')}
|
||||
>
|
||||
<Icon name="close" />
|
||||
</Button>
|
||||
<h3 className={styles.title}>{lang(entityType === 'channels'
|
||||
? 'RequestPeer.ChooseChannelTitle' : 'BoostingAwardSpecificUsers')}
|
||||
</h3>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={styles.root}
|
||||
isOpen={isOpen}
|
||||
onClose={handleClose}
|
||||
onEnter={handleSendIdList}
|
||||
>
|
||||
<div className={styles.main}>
|
||||
{renderSearchField()}
|
||||
<div className={buildClassName(styles.main, 'custom-scroll')}>
|
||||
<Picker
|
||||
className={styles.picker}
|
||||
itemIds={entityType === 'members' ? displayedMembersIds : displayedChannelIds}
|
||||
selectedIds={entityType === 'members' ? selectedMemberIds : selectedChannelIds}
|
||||
filterValue={searchQuery}
|
||||
filterPlaceholder={lang('Search')}
|
||||
searchInputId="new-members-picker-search"
|
||||
onSelectedIdsChange={entityType === 'channels'
|
||||
? handleSelectedChannelIdsChange : handleSelectedMembersChange}
|
||||
onFilterChange={setSearchQuery}
|
||||
isSearchable
|
||||
/>
|
||||
</div>
|
||||
<div className={styles.buttons}>
|
||||
<Button size="smaller" onClick={handleSendIdList}>
|
||||
{lang('Save')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
<ConfirmDialog
|
||||
title={lang('BoostingGiveawayPrivateChannel')}
|
||||
text={lang('BoostingGiveawayPrivateChannelWarning')}
|
||||
confirmLabel={lang('Add')}
|
||||
isOpen={isConfirmModalOpen}
|
||||
onClose={closeConfirmModal}
|
||||
confirmHandler={confirmPrivateLinkChannelSelection}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>((global, { chatId, entityType }): StateProps => {
|
||||
const { statusesById: userStatusesById } = global.users;
|
||||
let isChannel;
|
||||
let members: ApiChatMember[] | undefined;
|
||||
let adminMembersById: Record<string, ApiChatMember> | undefined;
|
||||
let currentUserId: string | undefined;
|
||||
|
||||
if (entityType === 'members') {
|
||||
currentUserId = global.currentUserId;
|
||||
const chatFullInfo = chatId ? selectChatFullInfo(global, chatId) : undefined;
|
||||
if (chatFullInfo) {
|
||||
members = chatFullInfo.members;
|
||||
adminMembersById = chatFullInfo.adminMembersById;
|
||||
}
|
||||
} else if (entityType === 'channels') {
|
||||
const chat = chatId ? selectChat(global, chatId) : undefined;
|
||||
if (chat) {
|
||||
isChannel = isChatChannel(chat);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
chatId,
|
||||
members,
|
||||
adminMembersById,
|
||||
userStatusesById,
|
||||
isChannel,
|
||||
currentUserId,
|
||||
};
|
||||
})(AppendEntityPickerModal));
|
||||
@ -101,6 +101,7 @@ import InviteViaLinkModal from './InviteViaLinkModal.async';
|
||||
import NewContactModal from './NewContactModal.async';
|
||||
import Notifications from './Notifications.async';
|
||||
import PremiumLimitReachedModal from './premium/common/PremiumLimitReachedModal.async';
|
||||
import GiveawayModal from './premium/GiveawayModal.async';
|
||||
import PremiumMainModal from './premium/PremiumMainModal.async';
|
||||
import SafeLinkModal from './SafeLinkModal.async';
|
||||
|
||||
@ -156,6 +157,8 @@ type StateProps = {
|
||||
isPaymentModalOpen?: boolean;
|
||||
isReceiptModalOpen?: boolean;
|
||||
isReactionPickerOpen: boolean;
|
||||
isAppendModalOpen?: boolean;
|
||||
isGiveawayModalOpen?: boolean;
|
||||
isCurrentUserPremium?: boolean;
|
||||
chatlistModal?: TabState['chatlistModal'];
|
||||
boostModal?: TabState['boostModal'];
|
||||
@ -214,6 +217,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
currentUserName,
|
||||
urlAuth,
|
||||
isPremiumModalOpen,
|
||||
isGiveawayModalOpen,
|
||||
isPaymentModalOpen,
|
||||
isReceiptModalOpen,
|
||||
isReactionPickerOpen,
|
||||
@ -595,7 +599,8 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
<AttachBotInstallModal bot={attachBotToInstall} />
|
||||
<AttachBotRecipientPicker requestedAttachBotInChat={requestedAttachBotInChat} />
|
||||
<MessageListHistoryHandler />
|
||||
<PremiumMainModal isOpen={isPremiumModalOpen} />
|
||||
{isPremiumModalOpen && <PremiumMainModal isOpen={isPremiumModalOpen} />}
|
||||
{isGiveawayModalOpen && <GiveawayModal isOpen={isGiveawayModalOpen} />}
|
||||
<PremiumLimitReachedModal limit={limitReached} />
|
||||
<PaymentModal isOpen={isPaymentModalOpen} onClose={closePaymentModal} />
|
||||
<ReceiptModal isOpen={isReceiptModalOpen} onClose={clearReceipt} />
|
||||
@ -638,6 +643,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
newContact,
|
||||
ratingPhoneCall,
|
||||
premiumModal,
|
||||
giveawayModal,
|
||||
isMasterTab,
|
||||
payment,
|
||||
limitReachedModal,
|
||||
@ -703,6 +709,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
urlAuth,
|
||||
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
||||
isPremiumModalOpen: premiumModal?.isOpen,
|
||||
isGiveawayModalOpen: giveawayModal?.isOpen,
|
||||
limitReached: limitReachedModal?.limit,
|
||||
isPaymentModalOpen: payment.isPaymentModalOpen,
|
||||
isReceiptModalOpen: Boolean(payment.receipt),
|
||||
|
||||
@ -15,7 +15,6 @@ import {
|
||||
import { formatCurrency } from '../../../util/formatCurrency';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
|
||||
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
|
||||
import Avatar from '../../common/Avatar';
|
||||
@ -45,31 +44,28 @@ const GiftPremiumModal: FC<OwnProps & StateProps> = ({
|
||||
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) {
|
||||
if (!gifts?.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const basicGift = renderedGifts.reduce((acc, gift) => {
|
||||
return gift.months < firstGift.months ? gift : firstGift;
|
||||
}, firstGift);
|
||||
const basicGift = gifts.reduce((acc, gift) => {
|
||||
return gift.months < acc.months ? gift : acc;
|
||||
});
|
||||
|
||||
return Math.floor(basicGift.amount / basicGift.months);
|
||||
}, [firstGift, renderedGifts]);
|
||||
}, [gifts]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setSelectedOption(firstGift?.months);
|
||||
if (isOpen && gifts?.length) {
|
||||
setSelectedOption(gifts[0].months);
|
||||
}
|
||||
}, [firstGift?.months, isOpen]);
|
||||
}, [gifts, isOpen]);
|
||||
|
||||
const selectedGift = useMemo(() => {
|
||||
return renderedGifts?.find((gift) => gift.months === selectedOption);
|
||||
}, [renderedGifts, selectedOption]);
|
||||
return gifts?.find((gift) => gift.months === selectedOption);
|
||||
}, [gifts, selectedOption]);
|
||||
|
||||
const handleSubmit = useCallback(() => {
|
||||
if (!selectedGift) {
|
||||
@ -121,7 +117,7 @@ const GiftPremiumModal: FC<OwnProps & StateProps> = ({
|
||||
<i className="icon icon-close" />
|
||||
</Button>
|
||||
<Avatar
|
||||
peer={renderedUser}
|
||||
peer={user}
|
||||
size="jumbo"
|
||||
className={styles.avatar}
|
||||
/>
|
||||
@ -130,13 +126,13 @@ const GiftPremiumModal: FC<OwnProps & StateProps> = ({
|
||||
</h2>
|
||||
<p className={styles.description}>
|
||||
{renderText(
|
||||
lang('GiftTelegramPremiumDescription', getUserFirstOrLastName(renderedUser)),
|
||||
lang('GiftTelegramPremiumDescription', getUserFirstOrLastName(user)),
|
||||
['emoji', 'simple_markdown'],
|
||||
)}
|
||||
</p>
|
||||
|
||||
<div className={styles.options}>
|
||||
{renderedGifts?.map((gift) => (
|
||||
{gifts?.map((gift) => (
|
||||
<PremiumSubscriptionOption
|
||||
key={gift.amount}
|
||||
option={gift}
|
||||
|
||||
18
src/components/main/premium/GiveawayModal.async.tsx
Normal file
18
src/components/main/premium/GiveawayModal.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 './GiveawayModal';
|
||||
|
||||
import { Bundles } from '../../../util/moduleLoader';
|
||||
|
||||
import useModuleLoader from '../../../hooks/useModuleLoader';
|
||||
|
||||
const GiveawayModalAsync: FC<OwnProps> = (props) => {
|
||||
const { isOpen } = props;
|
||||
const GiveawayModal = useModuleLoader(Bundles.Extra, 'GiveawayModal', !isOpen);
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return GiveawayModal ? <GiveawayModal {...props} /> : undefined;
|
||||
};
|
||||
|
||||
export default GiveawayModalAsync;
|
||||
280
src/components/main/premium/GiveawayModal.module.scss
Normal file
280
src/components/main/premium/GiveawayModal.module.scss
Normal file
@ -0,0 +1,280 @@
|
||||
@use '../../../styles/mixins';
|
||||
|
||||
.root {
|
||||
--premium-gradient: linear-gradient(88.39deg, #6C93FF -2.56%, #976FFF 51.27%, #DF69D1 107.39%);
|
||||
--premium-feature-background: linear-gradient(65.85deg, #6C93FF -0.24%, #976FFF 53.99%, #DF69D1 110.53%);
|
||||
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.root :global(.modal-content) {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.root :global(.modal-dialog) {
|
||||
height: min(calc(55vh + 41px + 193px), 90vh);
|
||||
max-width: 26.25rem;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.button {
|
||||
font-weight: 500;
|
||||
font-size: 1rem;
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
.main {
|
||||
height: 100%;
|
||||
overflow-y: scroll;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin: 1rem;
|
||||
width: 6.25rem;
|
||||
height: 6.25rem;
|
||||
min-height: 6.25rem;
|
||||
}
|
||||
|
||||
.header-text {
|
||||
font-size: 1.5rem;
|
||||
font-weight: 500;
|
||||
text-align: center;
|
||||
margin-inline: 0.5rem;
|
||||
}
|
||||
|
||||
.description {
|
||||
text-align: center;
|
||||
margin-inline: 2.5rem;
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.header {
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border-bottom: 0.0625rem solid var(--color-borders);
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
left: 0;
|
||||
top: 0;
|
||||
height: 3.5rem;
|
||||
padding: 0.5rem;
|
||||
background: var(--color-background);
|
||||
transition: 0.25s ease-out transform;
|
||||
}
|
||||
|
||||
.hidden-header {
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
|
||||
.close-button {
|
||||
position: absolute;
|
||||
top: 0.5rem;
|
||||
left: 0.5rem;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.premium-header-text {
|
||||
font-size: 1.25rem;
|
||||
font-weight: 500;
|
||||
margin: 0 0 0 3rem;
|
||||
unicode-bidi: plaintext;
|
||||
}
|
||||
|
||||
.primary-footer-text {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
.section {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
padding: 0 0.375rem;
|
||||
border-top: 0.0625rem solid var(--color-borders);
|
||||
}
|
||||
|
||||
.types {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
background: var(--color-background);
|
||||
border-top: 0.0625rem solid var(--color-borders);
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
padding: 1rem;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.options {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.giveawayTitle {
|
||||
margin: 0.8125rem 0 0 1rem;
|
||||
font-size: 1rem;
|
||||
color: var(--color-links);
|
||||
|
||||
@include mixins.adapt-margin-to-scrollbar(0.8125rem);
|
||||
}
|
||||
|
||||
.subscription {
|
||||
margin: 1rem 1.625rem 1.3125rem 1.5rem;
|
||||
font-size: 1rem;
|
||||
font-weight: 400;
|
||||
color: var(--color-text-secondary);
|
||||
|
||||
@include mixins.adapt-margin-to-scrollbar(1rem);
|
||||
}
|
||||
|
||||
.subscriptionOption {
|
||||
margin-bottom: 0;
|
||||
padding-inline: 3.5rem 1rem;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
padding: 0 0.375rem 0 1.125rem;
|
||||
margin-bottom: 1.3125rem;
|
||||
}
|
||||
|
||||
.info {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.titleInfo {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.month {
|
||||
color: var(--color-text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.quantity {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.floatingBadge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-links);
|
||||
background-color: rgba(78, 142, 229, 0.1);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.floatingBadgeColor {
|
||||
margin: 0.8125rem 0.8125rem 0 1rem;
|
||||
padding: 0.1875rem 0.75rem 0.25rem 0.5rem;
|
||||
border-radius: 1rem;
|
||||
background-color: rgba(78, 142, 229, 0.1);
|
||||
|
||||
@include mixins.adapt-margin-to-scrollbar(0.8125rem);
|
||||
}
|
||||
|
||||
.floatingBadgeButtonColor {
|
||||
padding: 0.25rem 0.375rem 0.25rem 0.1875rem;
|
||||
border-radius: 0.375rem;
|
||||
background-color: white;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.floatingBadgeIcon {
|
||||
font-size: 1.125rem;
|
||||
margin-right: 0.1875rem;
|
||||
}
|
||||
|
||||
.floatingBadgeValue {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.subscriptionFooter {
|
||||
margin-bottom: 6rem;
|
||||
}
|
||||
|
||||
.premiumFeatures {
|
||||
margin-bottom: 0;
|
||||
font-size: 1rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.dateButton {
|
||||
margin-bottom: 1rem;
|
||||
padding-right: 1.1875rem;
|
||||
display: flex;
|
||||
padding-left: 1rem;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--color-text);;
|
||||
font-size: 1rem;
|
||||
text-transform: initial;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.checkboxSection {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.prizesSection {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 1rem;
|
||||
}
|
||||
|
||||
.prizesInput {
|
||||
flex-grow: 1;
|
||||
margin-left: 1rem;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.subLabelClassName {
|
||||
margin-top: 0.1875rem;
|
||||
color: var(--color-links) !important;
|
||||
}
|
||||
|
||||
.prepaidImg {
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
}
|
||||
|
||||
.addButton {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.addChannel {
|
||||
margin-inline-start: initial !important;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.root :global(.modal-dialog) {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: 100% !important;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.root .transition {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
741
src/components/main/premium/GiveawayModal.tsx
Normal file
741
src/components/main/premium/GiveawayModal.tsx
Normal file
@ -0,0 +1,741 @@
|
||||
import type { ChangeEvent } from 'react';
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useEffect, useMemo, useRef, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, getGlobal, withGlobal } from '../../../global';
|
||||
|
||||
import type {
|
||||
ApiCountry, ApiPremiumGiftCodeOption, ApiPrepaidGiveaway, ApiUser,
|
||||
} from '../../../api/types';
|
||||
|
||||
import {
|
||||
GIVEAWAY_BOOST_PER_PREMIUM,
|
||||
GIVEAWAY_MAX_ADDITIONAL_CHANNELS,
|
||||
GIVEAWAY_MAX_ADDITIONAL_COUNTRIES,
|
||||
GIVEAWAY_MAX_ADDITIONAL_USERS,
|
||||
} from '../../../config';
|
||||
import { getUserFullName } from '../../../global/helpers';
|
||||
import {
|
||||
selectTabState,
|
||||
} from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { formatDateTimeToString } from '../../../util/date/dateFormat';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import CalendarModal from '../../common/CalendarModal';
|
||||
import CountryPickerModal from '../../common/CountryPickerModal';
|
||||
import GroupChatInfo from '../../common/GroupChatInfo';
|
||||
import Icon from '../../common/Icon';
|
||||
import Button from '../../ui/Button';
|
||||
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||
import InputText from '../../ui/InputText';
|
||||
import Link from '../../ui/Link';
|
||||
import ListItem from '../../ui/ListItem';
|
||||
import Modal from '../../ui/Modal';
|
||||
import RadioGroup from '../../ui/RadioGroup';
|
||||
import RangeSliderWithMarks from '../../ui/RangeSliderWithMarks';
|
||||
import Switcher from '../../ui/Switcher';
|
||||
import AppendEntityPickerModal from '../AppendEntityPickerModal';
|
||||
import GiveawayTypeOption from './GiveawayTypeOption';
|
||||
import PremiumSubscriptionOption from './PremiumSubscriptionOption';
|
||||
|
||||
import styles from './GiveawayModal.module.scss';
|
||||
|
||||
import GiftBlueRound from '../../../assets/premium/GiftBlueRound.svg';
|
||||
import GiftGreenRound from '../../../assets/premium/GiftGreenRound.svg';
|
||||
import GiftRedRound from '../../../assets/premium/GiftRedRound.svg';
|
||||
import GiveawayUsersRound from '../../../assets/premium/GiveawayUsersRound.svg';
|
||||
import PremiumLogo from '../../../assets/premium/PremiumLogo.svg';
|
||||
|
||||
export type OwnProps = {
|
||||
isOpen?: boolean;
|
||||
userIds?: string[];
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
chatId?: string;
|
||||
gifts?: ApiPremiumGiftCodeOption[];
|
||||
isOpen?: boolean;
|
||||
fromUser?: ApiUser;
|
||||
selectedMemberList?: string[] | undefined;
|
||||
selectedChannelList?: string[] | undefined;
|
||||
giveawayBoostPerPremiumLimit?: number;
|
||||
userSelectionLimit?: number;
|
||||
countryList: ApiCountry[];
|
||||
prepaidGiveaway?: ApiPrepaidGiveaway;
|
||||
countrySelectionLimit: number | undefined;
|
||||
};
|
||||
|
||||
type GiveawayAction = 'createRandomlyUsers' | 'createSpecificUsers';
|
||||
type ApiGiveawayType = 'random_users' | 'specific_users';
|
||||
type SubscribersType = 'all' | 'new';
|
||||
|
||||
interface TypeOption {
|
||||
name: string;
|
||||
text: string;
|
||||
value: ApiGiveawayType;
|
||||
img: string;
|
||||
actions?: GiveawayAction;
|
||||
isLink: boolean;
|
||||
onClickAction?: () => void;
|
||||
}
|
||||
|
||||
const DEFAULT_CUSTOM_EXPIRE_DATE = 86400 * 3 * 1000; // 3 days
|
||||
const MAX_ADDITIONAL_CHANNELS = 9;
|
||||
const DEFAULT_BOOST_COUNT = 5;
|
||||
|
||||
const GIVEAWAY_IMG_LIST: { [key: number]: string } = {
|
||||
3: GiftGreenRound,
|
||||
6: GiftBlueRound,
|
||||
12: GiftRedRound,
|
||||
};
|
||||
|
||||
const GiveawayModal: FC<OwnProps & StateProps> = ({
|
||||
chatId,
|
||||
gifts,
|
||||
isOpen,
|
||||
selectedMemberList,
|
||||
selectedChannelList,
|
||||
giveawayBoostPerPremiumLimit = GIVEAWAY_BOOST_PER_PREMIUM,
|
||||
countryList,
|
||||
prepaidGiveaway,
|
||||
countrySelectionLimit = GIVEAWAY_MAX_ADDITIONAL_COUNTRIES,
|
||||
userSelectionLimit = GIVEAWAY_MAX_ADDITIONAL_USERS,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const dialogRef = useRef<HTMLDivElement>(null);
|
||||
const {
|
||||
closeGiveawayModal, openInvoice, openPremiumModal,
|
||||
launchPrepaidGiveaway,
|
||||
} = getActions();
|
||||
|
||||
const lang = useLang();
|
||||
const [isCalendarOpened, openCalendar, closeCalendar] = useFlag();
|
||||
const [isCountryPickerModalOpen, openCountryPickerModal, closeCountryPickerModal] = useFlag();
|
||||
const [isConfirmModalOpen, openConfirmModal, closeConfirmModal] = useFlag();
|
||||
const [isEntityPickerModalOpen, openEntityPickerModal, closeEntityPickerModal] = useFlag();
|
||||
const [entityType, setEntityType] = useState<'members' | 'channels' | undefined>(undefined);
|
||||
|
||||
const TYPE_OPTIONS: TypeOption[] = [{
|
||||
name: 'BoostingCreateGiveaway',
|
||||
text: 'BoostingWinnersRandomly',
|
||||
value: 'random_users',
|
||||
img: GiftBlueRound,
|
||||
actions: 'createRandomlyUsers',
|
||||
isLink: false,
|
||||
}, {
|
||||
name: 'BoostingAwardSpecificUsers',
|
||||
text: 'BoostingSelectRecipients',
|
||||
value: 'specific_users',
|
||||
img: GiveawayUsersRound,
|
||||
actions: 'createSpecificUsers',
|
||||
isLink: true,
|
||||
onClickAction: () => {
|
||||
openEntityPickerModal();
|
||||
setEntityType('members');
|
||||
},
|
||||
}];
|
||||
|
||||
const [customExpireDate, setCustomExpireDate] = useState<number>(Date.now() + DEFAULT_CUSTOM_EXPIRE_DATE);
|
||||
const [isHeaderHidden, setHeaderHidden] = useState(true);
|
||||
const [selectedUserCount, setSelectedUserCount] = useState<number>(DEFAULT_BOOST_COUNT);
|
||||
const [selectedGiveawayOption, setGiveawayOption] = useState<ApiGiveawayType>(TYPE_OPTIONS[0].value);
|
||||
const [selectedSubscriberOption, setSelectedSubscriberOption] = useState<SubscribersType>('all');
|
||||
const [selectedMonthOption, setSelectedMonthOption] = useState<number | undefined>();
|
||||
const [selectedUserIds, setSelectedUserIds] = useState<string[]>([]);
|
||||
const [selectedChannelIds, setSelectedChannelIds] = useState<string[]>([]);
|
||||
const [selectedCountriesIds, setSelectedCountriesIds] = useState<string[] | undefined>([]);
|
||||
const [shouldShowWinners, setShouldShowWinners] = useState<boolean>(false);
|
||||
const [shouldShowPrizes, setShouldShowPrizes] = useState<boolean>(false);
|
||||
const [prizeDescription, setPrizeDescription] = useState<string | undefined>(undefined);
|
||||
const [dataPrepaidGiveaway, setDataPrepaidGiveaway] = useState<ApiPrepaidGiveaway | undefined>(undefined);
|
||||
const boostQuantity = selectedUserCount * giveawayBoostPerPremiumLimit;
|
||||
const isRandomUsers = selectedGiveawayOption === 'random_users';
|
||||
|
||||
const SUBSCRIBER_OPTIONS = useMemo(() => [
|
||||
{
|
||||
value: 'all',
|
||||
label: lang('BoostingAllSubscribers'),
|
||||
subLabel: selectedCountriesIds && selectedCountriesIds.length > 0
|
||||
? lang('Giveaway.ReceiverType.Countries', selectedCountriesIds.length)
|
||||
: lang('BoostingFromAllCountries'),
|
||||
},
|
||||
{
|
||||
value: 'new',
|
||||
label: lang('BoostingNewSubscribers'),
|
||||
subLabel: selectedCountriesIds && selectedCountriesIds.length > 0
|
||||
? lang('Giveaway.ReceiverType.Countries', selectedCountriesIds.length)
|
||||
: lang('BoostingFromAllCountries'),
|
||||
},
|
||||
], [lang, selectedCountriesIds]);
|
||||
|
||||
const monthQuantity = lang('Months', selectedMonthOption);
|
||||
|
||||
const selectedGift = useMemo(() => {
|
||||
return gifts!.find((gift) => gift.months === selectedMonthOption && gift.users === selectedUserCount);
|
||||
}, [gifts, selectedMonthOption, selectedUserCount]);
|
||||
|
||||
const filteredGifts = useMemo(() => {
|
||||
return gifts?.filter((gift) => gift.users
|
||||
=== (selectedUserIds?.length ? selectedUserIds?.length : selectedUserCount));
|
||||
}, [gifts, selectedUserIds, selectedUserCount]);
|
||||
|
||||
const fullMonthlyAmount = useMemo(() => {
|
||||
if (!filteredGifts?.length) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const basicGift = filteredGifts.reduce((acc, gift) => {
|
||||
return gift.amount < acc.amount ? gift : acc;
|
||||
});
|
||||
|
||||
return Math.floor(basicGift.amount / basicGift.months);
|
||||
}, [filteredGifts]);
|
||||
|
||||
const userCountOptions = useMemo(() => {
|
||||
const uniqueUserCounts = new Set(gifts?.map((gift) => gift.users));
|
||||
return Array.from(uniqueUserCounts).sort((a, b) => a - b);
|
||||
}, [gifts]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen) {
|
||||
setSelectedMonthOption(prepaidGiveaway ? prepaidGiveaway.months : gifts?.[0].months);
|
||||
}
|
||||
}, [gifts, isOpen, prepaidGiveaway]);
|
||||
|
||||
useEffect(() => {
|
||||
if (prepaidGiveaway) {
|
||||
setSelectedUserCount(prepaidGiveaway.quantity);
|
||||
setDataPrepaidGiveaway(prepaidGiveaway);
|
||||
}
|
||||
}, [prepaidGiveaway]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedMemberList) {
|
||||
setSelectedUserIds(selectedMemberList);
|
||||
}
|
||||
}, [selectedMemberList]);
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedChannelList) {
|
||||
setSelectedChannelIds(selectedChannelList);
|
||||
}
|
||||
}, [selectedChannelList]);
|
||||
|
||||
const handlePremiumClick = useLastCallback(() => {
|
||||
openPremiumModal();
|
||||
});
|
||||
|
||||
const handleClick = useLastCallback(() => {
|
||||
if (selectedUserIds?.length) {
|
||||
openInvoice({
|
||||
type: 'giftcode',
|
||||
boostChannelId: chatId!,
|
||||
userIds: selectedUserIds,
|
||||
currency: selectedGift!.currency,
|
||||
amount: selectedGift!.amount,
|
||||
option: selectedGift!,
|
||||
});
|
||||
} else {
|
||||
openInvoice({
|
||||
type: 'giveaway',
|
||||
chatId: chatId!,
|
||||
additionalChannelIds: selectedChannelIds,
|
||||
isOnlyForNewSubscribers: selectedSubscriberOption === 'new',
|
||||
countries: selectedCountriesIds,
|
||||
areWinnersVisible: shouldShowWinners,
|
||||
prizeDescription,
|
||||
untilDate: customExpireDate / 1000,
|
||||
currency: selectedGift!.currency,
|
||||
amount: selectedGift!.amount,
|
||||
option: selectedGift!,
|
||||
});
|
||||
}
|
||||
|
||||
closeGiveawayModal();
|
||||
});
|
||||
|
||||
const confirmLaunchPrepaidGiveaway = useLastCallback(() => {
|
||||
launchPrepaidGiveaway({
|
||||
chatId: chatId!,
|
||||
giveawayId: dataPrepaidGiveaway!.id,
|
||||
paymentPurpose: {
|
||||
additionalChannelIds: selectedChannelIds,
|
||||
countries: selectedCountriesIds,
|
||||
prizeDescription,
|
||||
areWinnersVisible: shouldShowWinners,
|
||||
untilDate: customExpireDate / 1000,
|
||||
currency: selectedGift!.currency,
|
||||
amount: selectedGift!.amount,
|
||||
},
|
||||
});
|
||||
|
||||
closeConfirmModal();
|
||||
closeGiveawayModal();
|
||||
});
|
||||
|
||||
const handleUserCountChange = useLastCallback((newValue) => {
|
||||
setSelectedUserCount(newValue);
|
||||
});
|
||||
|
||||
const handlePrizeDescriptionChange = useLastCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
setPrizeDescription(e.target.value);
|
||||
});
|
||||
|
||||
const userNames = useMemo(() => {
|
||||
const usersById = getGlobal().users.byId;
|
||||
return selectedUserIds?.map((userId) => getUserFullName(usersById[userId])).join(', ');
|
||||
}, [selectedUserIds]);
|
||||
|
||||
const handleAdd = useLastCallback(() => {
|
||||
openEntityPickerModal();
|
||||
setEntityType('channels');
|
||||
});
|
||||
|
||||
function handleScroll(e: React.UIEvent<HTMLDivElement>) {
|
||||
const { scrollTop } = e.currentTarget;
|
||||
|
||||
setHeaderHidden(scrollTop <= 150);
|
||||
}
|
||||
|
||||
const handleChangeSubscriberOption = useLastCallback((value) => {
|
||||
setSelectedSubscriberOption(value);
|
||||
});
|
||||
|
||||
const handleChangeTypeOption = useLastCallback((value: ApiGiveawayType) => {
|
||||
setGiveawayOption(value);
|
||||
setSelectedUserIds([]);
|
||||
});
|
||||
|
||||
const handleExpireDateChange = useLastCallback((date: Date) => {
|
||||
setCustomExpireDate(date.getTime());
|
||||
closeCalendar();
|
||||
});
|
||||
|
||||
const handleSetCountriesListChange = useLastCallback((value: string[]) => {
|
||||
setSelectedCountriesIds(value);
|
||||
});
|
||||
|
||||
const handleSetIdsListChange = useLastCallback((value: string[]) => {
|
||||
return entityType === 'members'
|
||||
? (value?.length ? setSelectedUserIds(value) : setGiveawayOption('random_users'))
|
||||
: setSelectedChannelIds(value);
|
||||
});
|
||||
|
||||
const handleClose = useLastCallback(() => {
|
||||
closeGiveawayModal();
|
||||
});
|
||||
|
||||
const handleShouldShowWinnersChange = useLastCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
setShouldShowWinners(e.target.checked);
|
||||
});
|
||||
|
||||
const handleShouldShowPrizesChange = useLastCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
setShouldShowPrizes(e.target.checked);
|
||||
});
|
||||
|
||||
const onClickActionHandler = useLastCallback(() => {
|
||||
openCountryPickerModal();
|
||||
});
|
||||
|
||||
if (!gifts) return undefined;
|
||||
|
||||
function renderTypeOptions() {
|
||||
return (
|
||||
<div className={styles.options}>
|
||||
{TYPE_OPTIONS.map((option) => {
|
||||
return (
|
||||
<GiveawayTypeOption
|
||||
key={option.name}
|
||||
name={option.name}
|
||||
text={option.text}
|
||||
option={option.value}
|
||||
img={option.img}
|
||||
onChange={handleChangeTypeOption}
|
||||
checked={selectedGiveawayOption === option.value}
|
||||
isLink={option.isLink}
|
||||
userNames={userNames}
|
||||
selectedMemberIds={selectedUserIds}
|
||||
onClickAction={option.onClickAction}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderSubscribersOptions() {
|
||||
return (
|
||||
<div className={styles.options}>
|
||||
<RadioGroup
|
||||
name="subscribers"
|
||||
options={SUBSCRIBER_OPTIONS}
|
||||
selected={selectedSubscriberOption}
|
||||
onChange={handleChangeSubscriberOption}
|
||||
onClickAction={onClickActionHandler}
|
||||
subLabelClassName={styles.subLabelClassName}
|
||||
isLink
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderSubscriptionOptions() {
|
||||
return (
|
||||
<div className={styles.options}>
|
||||
{filteredGifts?.map((gift) => (
|
||||
<PremiumSubscriptionOption
|
||||
isGiveaway
|
||||
key={gift.months}
|
||||
option={gift}
|
||||
userCount={gift.users}
|
||||
fullMonthlyAmount={fullMonthlyAmount!}
|
||||
checked={gift.months === selectedMonthOption}
|
||||
onChange={setSelectedMonthOption}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderPremiumFeaturesLink() {
|
||||
const info = lang('GiftPremiumListFeaturesAndTerms');
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
function deleteParticipantsHandler(id: string) {
|
||||
const filteredChannelIds = selectedChannelIds.filter((channelId) => channelId !== id);
|
||||
setSelectedChannelIds(filteredChannelIds);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
className={styles.root}
|
||||
onClose={handleClose}
|
||||
isOpen={isOpen}
|
||||
dialogRef={dialogRef}
|
||||
>
|
||||
<div className={styles.main} onScroll={handleScroll}>
|
||||
<Button
|
||||
round
|
||||
size="smaller"
|
||||
className={styles.closeButton}
|
||||
color="translucent"
|
||||
onClick={handleClose}
|
||||
ariaLabel={lang('Close')}
|
||||
>
|
||||
<Icon name="close" />
|
||||
</Button>
|
||||
<img className={styles.logo} src={PremiumLogo} alt="" draggable={false} />
|
||||
<h2 className={styles.headerText}>
|
||||
{renderText(lang('BoostingBoostsViaGifts'))}
|
||||
</h2>
|
||||
<div className={styles.description}>
|
||||
{renderText(lang('BoostingGetMoreBoost'))}
|
||||
</div>
|
||||
<div className={buildClassName(styles.header, isHeaderHidden && styles.hiddenHeader)}>
|
||||
<h2 className={styles.premiumHeaderText}>
|
||||
{lang('BoostingBoostsViaGifts')}
|
||||
</h2>
|
||||
</div>
|
||||
{dataPrepaidGiveaway ? (
|
||||
<div className={styles.status}>
|
||||
<div>
|
||||
<img className={styles.prepaidImg} src={GIVEAWAY_IMG_LIST[dataPrepaidGiveaway.months]} alt="" />
|
||||
</div>
|
||||
<div className={styles.info}>
|
||||
<h3 className={styles.title}>
|
||||
{lang('BoostingTelegramPremiumCountPlural', dataPrepaidGiveaway.quantity)}
|
||||
</h3>
|
||||
<p className={styles.month}>{lang('PrepaidGiveawayMonths', dataPrepaidGiveaway.months)}</p>
|
||||
</div>
|
||||
<div className={styles.quantity}>
|
||||
<div className={buildClassName(styles.floatingBadge, styles.floatingBadgeColor)}>
|
||||
<Icon name="boost" className={styles.floatingBadgeIcon} />
|
||||
<div className={styles.floatingBadgeValue} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{dataPrepaidGiveaway.quantity * (giveawayBoostPerPremiumLimit ?? GIVEAWAY_BOOST_PER_PREMIUM)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className={buildClassName(styles.section, styles.types)}>
|
||||
{renderTypeOptions()}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isRandomUsers && (
|
||||
<>
|
||||
{!dataPrepaidGiveaway && (
|
||||
<>
|
||||
<div className={styles.section}>
|
||||
<div className={styles.quantity}>
|
||||
<h2 className={styles.giveawayTitle}>
|
||||
{lang('BoostingQuantityPrizes')}
|
||||
</h2>
|
||||
<div className={buildClassName(styles.floatingBadge, styles.floatingBadgeColor)}>
|
||||
<Icon name="boost" className={styles.floatingBadgeIcon} />
|
||||
<div className={styles.floatingBadgeValue} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{boostQuantity}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<RangeSliderWithMarks
|
||||
rangeCount={selectedUserCount}
|
||||
marks={userCountOptions}
|
||||
onChange={handleUserCountChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className={styles.subscription}>
|
||||
{renderText(lang('BoostingChooseHowMany'))}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
<div className={styles.section}>
|
||||
<h2 className={styles.giveawayTitle}>
|
||||
{lang('BoostingChannelsIncludedGiveaway')}
|
||||
</h2>
|
||||
|
||||
<ListItem
|
||||
inactive
|
||||
className="chat-item-clickable contact-list-item"
|
||||
>
|
||||
<GroupChatInfo
|
||||
chatId={chatId!}
|
||||
status={lang('BoostingChannelWillReceiveBoost', boostQuantity, 'i')}
|
||||
/>
|
||||
</ListItem>
|
||||
|
||||
{selectedChannelIds?.map((channelId) => {
|
||||
return (
|
||||
<ListItem
|
||||
ripple
|
||||
key={channelId}
|
||||
className="chat-item-clickable contact-list-item"
|
||||
/* eslint-disable-next-line react/jsx-no-bind */
|
||||
onClick={() => deleteParticipantsHandler(channelId)}
|
||||
rightElement={(<Icon name="close" />)}
|
||||
>
|
||||
<GroupChatInfo
|
||||
chatId={channelId.toString()}
|
||||
/>
|
||||
</ListItem>
|
||||
);
|
||||
})}
|
||||
|
||||
{selectedChannelIds.length < MAX_ADDITIONAL_CHANNELS && (
|
||||
<ListItem
|
||||
icon="add"
|
||||
ripple
|
||||
onClick={handleAdd}
|
||||
className={styles.addButton}
|
||||
iconClassName={styles.addChannel}
|
||||
>
|
||||
{lang('BoostingAddChannel')}
|
||||
</ListItem>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className={styles.section}>
|
||||
<h2 className={styles.giveawayTitle}>
|
||||
{lang('BoostingEligibleUsers')}
|
||||
</h2>
|
||||
|
||||
{renderSubscribersOptions()}
|
||||
</div>
|
||||
|
||||
<div className={styles.subscription}>
|
||||
{renderText(lang('BoostGift.LimitSubscribersInfo'))}
|
||||
</div>
|
||||
|
||||
<div className={styles.section}>
|
||||
<div className={styles.checkboxSection}>
|
||||
<h2 className={styles.title}>
|
||||
{lang('BoostingGiveawayAdditionalPrizes')}
|
||||
</h2>
|
||||
|
||||
<Switcher
|
||||
label={lang('BoostingGiveawayAdditionalPrizes')}
|
||||
checked={shouldShowPrizes}
|
||||
onChange={handleShouldShowPrizesChange}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{shouldShowPrizes && (
|
||||
<div className={styles.prizesSection}>
|
||||
<h2 className={styles.title}>
|
||||
{dataPrepaidGiveaway ? dataPrepaidGiveaway.quantity : selectedUserCount}
|
||||
</h2>
|
||||
<InputText
|
||||
className={styles.prizesInput}
|
||||
value={prizeDescription}
|
||||
onChange={handlePrizeDescriptionChange}
|
||||
label={lang('BoostingGiveawayEnterYourPrize')}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{shouldShowPrizes ? (
|
||||
<div className={styles.subscription}>
|
||||
{prizeDescription?.length ? renderText(lang('BoostingGiveawayAdditionPrizeCountNameHint',
|
||||
dataPrepaidGiveaway
|
||||
? [dataPrepaidGiveaway.quantity, prizeDescription, monthQuantity]
|
||||
: [selectedUserCount, prizeDescription, monthQuantity],
|
||||
undefined,
|
||||
selectedMonthOption), ['simple_markdown']) : renderText(lang('BoostingGiveawayAdditionPrizeCountHint',
|
||||
dataPrepaidGiveaway
|
||||
? [dataPrepaidGiveaway.quantity, monthQuantity]
|
||||
: [selectedUserCount, monthQuantity],
|
||||
undefined,
|
||||
selectedMonthOption), ['simple_markdown'])}
|
||||
</div>
|
||||
) : (
|
||||
<div className={styles.subscription}>
|
||||
{renderText(lang('BoostingGiveawayAdditionPrizeHint'))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className={styles.section}>
|
||||
<div className={styles.checkboxSection}>
|
||||
<h2 className={styles.title}>
|
||||
{lang('BoostingGiveawayShowWinners')}
|
||||
</h2>
|
||||
|
||||
<Switcher
|
||||
label={lang('BoostingGiveawayAdditionalPrizes')}
|
||||
checked={shouldShowWinners}
|
||||
onChange={handleShouldShowWinnersChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={styles.subscription}>
|
||||
{renderText(lang('BoostingGiveawayShowWinnersHint'))}
|
||||
</div>
|
||||
|
||||
<div className={buildClassName(styles.section, dataPrepaidGiveaway && styles.subscriptionFooter)}>
|
||||
<h2 className={styles.giveawayTitle}>
|
||||
{lang('BoostingDateWhenGiveawayEnds')}
|
||||
</h2>
|
||||
|
||||
<Button
|
||||
ariaLabel={lang('BoostGift.DateEnds')}
|
||||
className={buildClassName(styles.dateButton, 'expire-limit')}
|
||||
isText
|
||||
onClick={openCalendar}
|
||||
>
|
||||
<h3 className={styles.title}>
|
||||
{lang('BoostGift.DateEnds')}
|
||||
</h3>
|
||||
{formatDateTimeToString(customExpireDate, lang.code)}
|
||||
</Button>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{!dataPrepaidGiveaway && (
|
||||
<>
|
||||
<div className={styles.section}>
|
||||
<h2 className={styles.giveawayTitle}>
|
||||
{lang('BoostingDurationOfPremium')}
|
||||
</h2>
|
||||
|
||||
{renderSubscriptionOptions()}
|
||||
</div>
|
||||
|
||||
<div className={buildClassName(styles.subscription, styles.subscriptionFooter)}>
|
||||
{renderPremiumFeaturesLink()}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{selectedGiveawayOption && (
|
||||
<div className={styles.footer}>
|
||||
<Button className={styles.button} onClick={dataPrepaidGiveaway ? openConfirmModal : handleClick}>
|
||||
{lang('BoostingStartGiveaway')}
|
||||
<div className={styles.quantity}>
|
||||
<div className={buildClassName(styles.floatingBadge, styles.floatingBadgeButtonColor)}>
|
||||
<Icon name="boost" className={styles.floatingBadgeIcon} />
|
||||
<div className={styles.floatingBadgeValue} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{dataPrepaidGiveaway ? dataPrepaidGiveaway.quantity : boostQuantity}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<CalendarModal
|
||||
isOpen={isCalendarOpened}
|
||||
isFutureMode
|
||||
withTimePicker
|
||||
onClose={closeCalendar}
|
||||
onSubmit={handleExpireDateChange}
|
||||
selectedAt={customExpireDate}
|
||||
submitButtonLabel={lang('Save')}
|
||||
/>
|
||||
<CountryPickerModal
|
||||
isOpen={isCountryPickerModalOpen}
|
||||
onClose={closeCountryPickerModal}
|
||||
countryList={countryList}
|
||||
onSubmit={handleSetCountriesListChange}
|
||||
selectionLimit={countrySelectionLimit}
|
||||
/>
|
||||
<AppendEntityPickerModal
|
||||
isOpen={isEntityPickerModalOpen}
|
||||
onClose={closeEntityPickerModal}
|
||||
entityType={entityType}
|
||||
chatId={chatId}
|
||||
onSubmit={handleSetIdsListChange}
|
||||
selectionLimit={entityType === 'members' ? userSelectionLimit : GIVEAWAY_MAX_ADDITIONAL_CHANNELS}
|
||||
/>
|
||||
<ConfirmDialog
|
||||
title={lang('BoostingStartGiveawayConfirmTitle')}
|
||||
text={lang('BoostingStartGiveawayConfirmText')}
|
||||
confirmLabel={lang('Start')}
|
||||
isOpen={isConfirmModalOpen}
|
||||
onClose={closeConfirmModal}
|
||||
confirmHandler={confirmLaunchPrepaidGiveaway}
|
||||
/>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>((global): StateProps => {
|
||||
const {
|
||||
giveawayModal,
|
||||
} = selectTabState(global);
|
||||
|
||||
return {
|
||||
chatId: giveawayModal?.chatId,
|
||||
gifts: giveawayModal?.gifts,
|
||||
selectedMemberList: giveawayModal?.selectedMemberIds,
|
||||
selectedChannelList: giveawayModal?.selectedChannelIds,
|
||||
giveawayBoostPerPremiumLimit: global.appConfig?.giveawayBoostsPerPremium,
|
||||
userSelectionLimit: global.appConfig?.giveawayAddPeersMax,
|
||||
countrySelectionLimit: global.appConfig?.giveawayCountriesMax,
|
||||
countryList: global.countryList.general,
|
||||
prepaidGiveaway: giveawayModal?.prepaidGiveaway,
|
||||
};
|
||||
})(GiveawayModal));
|
||||
99
src/components/main/premium/GiveawayTypeOption.module.scss
Normal file
99
src/components/main/premium/GiveawayTypeOption.module.scss
Normal file
@ -0,0 +1,99 @@
|
||||
.wrapper {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding-inline: 3.5rem 1rem;
|
||||
border: none;
|
||||
margin-bottom: 0;
|
||||
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
.input {
|
||||
position: absolute;
|
||||
z-index: var(--z-below);
|
||||
opacity: 0;
|
||||
&:checked ~ .content {
|
||||
&::before {
|
||||
border-color: var(--color-primary);
|
||||
}
|
||||
|
||||
&::after {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.input:not(:checked) ~ .content.notChecked::before,
|
||||
.input:not(:checked) ~ .content.notChecked::after {
|
||||
border-color: transparent;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0.5rem 0;
|
||||
gap: 1.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;
|
||||
}
|
||||
}
|
||||
|
||||
.giveaway {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
font-size: 0.875rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.title {
|
||||
color: var(--color-text);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.link {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
color: var(--color-links);
|
||||
}
|
||||
|
||||
.optionImg {
|
||||
width: 2.75rem;
|
||||
height: 2.75rem;
|
||||
}
|
||||
|
||||
@media (max-width: 450px) {
|
||||
.contentText {
|
||||
line-height: 1rem;
|
||||
}
|
||||
}
|
||||
90
src/components/main/premium/GiveawayTypeOption.tsx
Normal file
90
src/components/main/premium/GiveawayTypeOption.tsx
Normal file
@ -0,0 +1,90 @@
|
||||
import type { ChangeEvent } from 'react';
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { memo } from '../../../lib/teact/teact';
|
||||
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import Icon from '../../common/Icon';
|
||||
|
||||
import styles from './GiveawayTypeOption.module.scss';
|
||||
|
||||
type ApiGiveawayType = 'random_users' | 'specific_users';
|
||||
|
||||
type OwnProps = {
|
||||
option: ApiGiveawayType;
|
||||
name: string;
|
||||
text: string;
|
||||
img: string;
|
||||
checked?: boolean;
|
||||
isLink: boolean;
|
||||
className?: string;
|
||||
onChange: (value: ApiGiveawayType) => void;
|
||||
onClickAction?: () => void;
|
||||
userNames?: string;
|
||||
selectedMemberIds?: string[];
|
||||
};
|
||||
|
||||
const GiveawayTypeOption: FC<OwnProps> = ({
|
||||
option, checked,
|
||||
name, text, img,
|
||||
isLink, onChange, onClickAction, className,
|
||||
userNames, selectedMemberIds,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
|
||||
let displayText: string | undefined = lang(text);
|
||||
if (isLink && selectedMemberIds?.length) {
|
||||
displayText = selectedMemberIds.length > 2 ? `${selectedMemberIds.length}` : userNames;
|
||||
}
|
||||
|
||||
const handleChange = useLastCallback((e: ChangeEvent<HTMLInputElement>) => {
|
||||
if (e.target.checked) {
|
||||
onChange(option);
|
||||
}
|
||||
});
|
||||
|
||||
const handleClick = useLastCallback(() => {
|
||||
onClickAction?.();
|
||||
});
|
||||
|
||||
return (
|
||||
<label
|
||||
className={buildClassName(styles.wrapper, className)}
|
||||
dir={lang.isRtl ? 'rtl' : undefined}
|
||||
onClick={handleClick}
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
>
|
||||
<input
|
||||
className={styles.input}
|
||||
type="radio"
|
||||
name="giveaway_option"
|
||||
value={option}
|
||||
checked={checked}
|
||||
onChange={handleChange}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
<div className={styles.content}>
|
||||
<img className={styles.optionImg} src={img} alt="" draggable={false} />
|
||||
<div className={styles.giveaway}>
|
||||
<h3 className={styles.title}>
|
||||
{lang(`${name}`)}
|
||||
</h3>
|
||||
{isLink ? (
|
||||
<div className={styles.link}>
|
||||
<span>{displayText}</span>
|
||||
<Icon name="next" />
|
||||
</div>
|
||||
) : (
|
||||
<span className={styles.contentText}>{displayText}</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(GiveawayTypeOption);
|
||||
@ -166,6 +166,7 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
|
||||
|
||||
if (premiumSlug) {
|
||||
openInvoice({
|
||||
type: 'slug',
|
||||
slug: premiumSlug,
|
||||
});
|
||||
} else if (premiumBotUsername) {
|
||||
|
||||
@ -12,6 +12,16 @@
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
.giveawayWrapper {
|
||||
position: relative;
|
||||
display: block;
|
||||
padding-inline: 3.6875rem 1rem;
|
||||
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
|
||||
.active {
|
||||
box-shadow: 0 0 0 0.125rem var(--color-primary);
|
||||
}
|
||||
@ -108,3 +118,10 @@
|
||||
margin-inline-end: 0.5rem;
|
||||
align-self: baseline;
|
||||
}
|
||||
|
||||
.giveawayDiscount {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 500;
|
||||
border-radius: 0.375rem;
|
||||
padding: 0.1875rem;
|
||||
}
|
||||
|
||||
@ -2,6 +2,8 @@ import type { ChangeEvent } from 'react';
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { memo, useCallback, useMemo } from '../../../lib/teact/teact';
|
||||
|
||||
import type { ApiPremiumGiftCodeOption, ApiPremiumGiftOption } from '../../../api/types';
|
||||
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { formatCurrency } from '../../../util/formatCurrency';
|
||||
|
||||
@ -10,11 +12,9 @@ import useLang from '../../../hooks/useLang';
|
||||
import styles from './PremiumSubscriptionOption.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
option: {
|
||||
months: number;
|
||||
currency: string;
|
||||
amount: number;
|
||||
};
|
||||
option: ApiPremiumGiftOption | ApiPremiumGiftCodeOption;
|
||||
isGiveaway?: boolean;
|
||||
userCount?: number;
|
||||
checked?: boolean;
|
||||
fullMonthlyAmount?: number;
|
||||
className?: string;
|
||||
@ -22,11 +22,14 @@ type OwnProps = {
|
||||
};
|
||||
|
||||
const PremiumSubscriptionOption: FC<OwnProps> = ({
|
||||
option, checked, fullMonthlyAmount, onChange, className,
|
||||
option, checked, fullMonthlyAmount,
|
||||
onChange, className, isGiveaway, userCount,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
|
||||
const { months, amount, currency } = option;
|
||||
const {
|
||||
months, amount, currency,
|
||||
} = option;
|
||||
const perMonth = Math.floor(amount / months);
|
||||
|
||||
const discount = useMemo(() => {
|
||||
@ -44,8 +47,8 @@ const PremiumSubscriptionOption: FC<OwnProps> = ({
|
||||
return (
|
||||
<label
|
||||
className={buildClassName(
|
||||
styles.wrapper,
|
||||
checked && styles.active,
|
||||
isGiveaway ? styles.giveawayWrapper : styles.wrapper,
|
||||
(checked && !isGiveaway) && styles.active,
|
||||
className,
|
||||
)}
|
||||
dir={lang.isRtl ? 'rtl' : undefined}
|
||||
@ -53,20 +56,29 @@ const PremiumSubscriptionOption: FC<OwnProps> = ({
|
||||
<input
|
||||
className={styles.input}
|
||||
type="radio"
|
||||
name="gift_option"
|
||||
name="subscription_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))}
|
||||
{Boolean(discount) && (
|
||||
<span className={styles.discount} title={lang('GiftDiscount')}> −{discount}% </span>
|
||||
<div className={styles.month}>
|
||||
{Boolean(discount) && isGiveaway && (
|
||||
<span
|
||||
className={buildClassName(styles.giveawayDiscount, isGiveaway && styles.discount)}
|
||||
title={lang('GiftDiscount')}
|
||||
> −{discount}%
|
||||
</span>
|
||||
)}
|
||||
{lang('Months', months)}
|
||||
</div>
|
||||
<div className={styles.perMonth}>
|
||||
{isGiveaway ? `${formatCurrency(amount, currency, lang.code)} x ${userCount!}`
|
||||
: lang('PricePerMonth', formatCurrency(perMonth, currency, lang.code))}
|
||||
</div>
|
||||
<div className={styles.amount}>
|
||||
{formatCurrency(isGiveaway ? amount * userCount! : amount, currency, lang.code)}
|
||||
</div>
|
||||
<div className={styles.amount}>{formatCurrency(amount, currency, lang.code)}</div>
|
||||
</div>
|
||||
</label>
|
||||
);
|
||||
|
||||
@ -15,7 +15,7 @@
|
||||
}
|
||||
|
||||
.pinned-message-border-wrapper {
|
||||
background-color: var(--color-primary-opacity);
|
||||
background-color: var(--color-primary-opacity-hover);
|
||||
position: relative;
|
||||
will-change: transform;
|
||||
transition: transform 0.25s ease-in-out;
|
||||
|
||||
@ -52,6 +52,7 @@ const InvoiceMediaPreview: FC<OwnProps> = ({
|
||||
|
||||
const handleClick = useLastCallback(() => {
|
||||
openInvoice({
|
||||
type: 'message',
|
||||
chatId,
|
||||
messageId: id,
|
||||
isExtendedMedia: true,
|
||||
|
||||
@ -191,6 +191,7 @@ const useWebAppFrame = (
|
||||
slug: eventData.slug,
|
||||
});
|
||||
openInvoice({
|
||||
type: 'slug',
|
||||
slug: eventData.slug,
|
||||
});
|
||||
}
|
||||
|
||||
@ -38,3 +38,52 @@
|
||||
.loadMoreSpinner {
|
||||
margin-inline-end: 1rem;
|
||||
}
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 1rem;
|
||||
justify-content: space-between;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.info {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.month {
|
||||
color: var(--color-text-secondary);
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.quantity {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.floatingBadge {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--color-links);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.floatingBadgeButtonColor {
|
||||
padding: 0.25rem 0.75rem 0.375rem 0.5625rem;
|
||||
border-radius: 1rem;
|
||||
background-color: var(--color-primary-opacity);
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.floatingBadgeIcon {
|
||||
font-size: 1.125rem;
|
||||
margin-right: 0.1875rem;
|
||||
}
|
||||
|
||||
.floatingBadgeValue {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import React, { memo, useMemo } from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiBoostStatistics } from '../../../api/types';
|
||||
import type { ApiBoostStatistics, ApiPrepaidGiveaway } from '../../../api/types';
|
||||
import type { TabState } from '../../../global/types';
|
||||
|
||||
import { selectTabState } from '../../../global/selectors';
|
||||
import { GIVEAWAY_BOOST_PER_PREMIUM } from '../../../config';
|
||||
import { selectIsGiveawayGiftsPurchaseAvailable, selectTabState } from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { formatDateAtTime } from '../../../util/date/dateFormat';
|
||||
import { getBoostProgressInfo } from '../../common/helpers/boostInfo';
|
||||
@ -23,14 +24,32 @@ import StatisticsOverview from './StatisticsOverview';
|
||||
|
||||
import styles from './BoostStatistics.module.scss';
|
||||
|
||||
import GiftBlueRound from '../../../assets/premium/GiftBlueRound.svg';
|
||||
import GiftGreenRound from '../../../assets/premium/GiftGreenRound.svg';
|
||||
import GiftRedRound from '../../../assets/premium/GiftRedRound.svg';
|
||||
|
||||
type StateProps = {
|
||||
boostStatistics: TabState['boostStatistics'];
|
||||
isGiveawayAvailable?: boolean;
|
||||
chatId: string;
|
||||
giveawayBoostsPerPremium?: number;
|
||||
};
|
||||
|
||||
const GIVEAWAY_IMG_LIST: { [key: number]: string } = {
|
||||
3: GiftGreenRound,
|
||||
6: GiftBlueRound,
|
||||
12: GiftRedRound,
|
||||
};
|
||||
|
||||
const BoostStatistics = ({
|
||||
boostStatistics,
|
||||
isGiveawayAvailable,
|
||||
chatId,
|
||||
giveawayBoostsPerPremium,
|
||||
}: StateProps) => {
|
||||
const { openChat, loadMoreBoosters, closeBoostStatistics } = getActions();
|
||||
const {
|
||||
openChat, loadMoreBoosters, closeBoostStatistics, openGiveawayModal,
|
||||
} = getActions();
|
||||
const lang = useLang();
|
||||
|
||||
const isLoaded = boostStatistics?.boostStatus;
|
||||
@ -50,6 +69,7 @@ const BoostStatistics = ({
|
||||
boosts: 0,
|
||||
levelProgress: 0,
|
||||
remainingBoosts: 0,
|
||||
prepaidGiveaways: [],
|
||||
};
|
||||
}
|
||||
return getBoostProgressInfo(status);
|
||||
@ -63,6 +83,7 @@ const BoostStatistics = ({
|
||||
boosts,
|
||||
premiumSubscribers: status.premiumSubscribers!,
|
||||
remainingBoosts,
|
||||
prepaidGiveaways: status.prepaidGiveaways!,
|
||||
} satisfies ApiBoostStatistics;
|
||||
}, [status, boosts, currentLevel, remainingBoosts]);
|
||||
|
||||
@ -78,10 +99,18 @@ const BoostStatistics = ({
|
||||
closeBoostStatistics();
|
||||
});
|
||||
|
||||
const handleGiveawayClick = useLastCallback(() => {
|
||||
openGiveawayModal({ chatId });
|
||||
});
|
||||
|
||||
const handleLoadMore = useLastCallback(() => {
|
||||
loadMoreBoosters();
|
||||
});
|
||||
|
||||
const launchPrepaidGiveawayHandler = useLastCallback((prepaidGiveaway: ApiPrepaidGiveaway) => {
|
||||
openGiveawayModal({ chatId, prepaidGiveaway });
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={buildClassName(styles.root, 'custom-scroll')}>
|
||||
{!isLoaded && <Loading />}
|
||||
@ -97,6 +126,42 @@ const BoostStatistics = ({
|
||||
/>
|
||||
<StatisticsOverview className={styles.stats} statistics={statsOverview} type="boost" />
|
||||
</div>
|
||||
{statsOverview.prepaidGiveaways && (
|
||||
<div className={styles.section}>
|
||||
<h4 className={styles.sectionHeader} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{lang('BoostingPreparedGiveaways')}
|
||||
</h4>
|
||||
{statsOverview?.prepaidGiveaways?.map((prepaidGiveaway) => (
|
||||
<ListItem
|
||||
key={prepaidGiveaway.id}
|
||||
className="chat-item-clickable"
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => launchPrepaidGiveawayHandler(prepaidGiveaway)}
|
||||
>
|
||||
<div className={buildClassName(styles.status, 'status-clickable')}>
|
||||
<div>
|
||||
<img src={GIVEAWAY_IMG_LIST[prepaidGiveaway.months]} alt="Giveaway" />
|
||||
</div>
|
||||
<div className={styles.info}>
|
||||
<h3>
|
||||
{lang('BoostingTelegramPremiumCountPlural', prepaidGiveaway.quantity)}
|
||||
</h3>
|
||||
<p className={styles.month}>{lang('PrepaidGiveawayMonths', prepaidGiveaway.months)}</p>
|
||||
</div>
|
||||
<div className={styles.quantity}>
|
||||
<div className={buildClassName(styles.floatingBadge, styles.floatingBadgeButtonColor)}>
|
||||
<Icon name="boost" className={styles.floatingBadgeIcon} />
|
||||
<div className={styles.floatingBadgeValue} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{prepaidGiveaway.quantity * (giveawayBoostsPerPremium ?? GIVEAWAY_BOOST_PER_PREMIUM)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ListItem>
|
||||
))}
|
||||
<p className="text-muted hint" key="links-hint">{lang('BoostingSelectPaidGiveaway')}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.section}>
|
||||
<h4 className={styles.sectionHeader} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{lang('Boosters')}
|
||||
@ -136,6 +201,14 @@ const BoostStatistics = ({
|
||||
)}
|
||||
</div>
|
||||
<LinkField className={styles.section} link={status!.boostUrl} withShare title={lang('LinkForBoosting')} />
|
||||
{isGiveawayAvailable && (
|
||||
<div className={styles.section}>
|
||||
<ListItem icon="gift" ripple onClick={handleGiveawayClick}>
|
||||
{lang('BoostingGetBoostsViaGifts')}
|
||||
</ListItem>
|
||||
<p className="text-muted hint" key="links-hint">{lang('BoostingGetMoreBoosts')}</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
@ -146,9 +219,15 @@ export default memo(withGlobal(
|
||||
(global): StateProps => {
|
||||
const tabState = selectTabState(global);
|
||||
const boostStatistics = tabState.boostStatistics;
|
||||
const isGiveawayAvailable = selectIsGiveawayGiftsPurchaseAvailable(global);
|
||||
const chatId = boostStatistics && boostStatistics.chatId;
|
||||
const giveawayBoostsPerPremium = global.appConfig?.giveawayBoostsPerPremium;
|
||||
|
||||
return {
|
||||
boostStatistics,
|
||||
isGiveawayAvailable,
|
||||
chatId: chatId!,
|
||||
giveawayBoostsPerPremium,
|
||||
};
|
||||
},
|
||||
)(BoostStatistics));
|
||||
|
||||
@ -73,6 +73,10 @@
|
||||
color: var(--color-text-secondary);
|
||||
unicode-bidi: plaintext;
|
||||
}
|
||||
|
||||
.subLabelLink {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
input:checked ~ .Radio-main {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { ChangeEvent } from 'react';
|
||||
import type { ChangeEvent, MouseEventHandler } from 'react';
|
||||
import type { FC, TeactNode } from '../../lib/teact/teact';
|
||||
import React, { memo } from '../../lib/teact/teact';
|
||||
|
||||
@ -18,16 +18,20 @@ type OwnProps = {
|
||||
value: string;
|
||||
checked: boolean;
|
||||
disabled?: boolean;
|
||||
isLink?: boolean;
|
||||
hidden?: boolean;
|
||||
isLoading?: boolean;
|
||||
className?: string;
|
||||
subLabelClassName?: string;
|
||||
onChange: (e: ChangeEvent<HTMLInputElement>) => void;
|
||||
onSubLabelClick?: MouseEventHandler<HTMLSpanElement> | undefined;
|
||||
};
|
||||
|
||||
const Radio: FC<OwnProps> = ({
|
||||
id,
|
||||
label,
|
||||
subLabel,
|
||||
subLabelClassName,
|
||||
value,
|
||||
name,
|
||||
checked,
|
||||
@ -36,6 +40,8 @@ const Radio: FC<OwnProps> = ({
|
||||
isLoading,
|
||||
className,
|
||||
onChange,
|
||||
isLink,
|
||||
onSubLabelClick,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
const fullClassName = buildClassName(
|
||||
@ -59,7 +65,16 @@ const Radio: FC<OwnProps> = ({
|
||||
/>
|
||||
<div className="Radio-main">
|
||||
<span className="label" dir={lang.isRtl ? 'auto' : undefined}>{label}</span>
|
||||
{subLabel && <span className="subLabel" dir={lang.isRtl ? 'auto' : undefined}>{subLabel}</span>}
|
||||
{subLabel
|
||||
&& (
|
||||
<span
|
||||
className={buildClassName(subLabelClassName, 'subLabel', isLink ? 'subLabelLink' : undefined)}
|
||||
dir={lang.isRtl ? 'auto' : undefined}
|
||||
onClick={isLink ? onSubLabelClick : undefined}
|
||||
>
|
||||
{subLabel}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{isLoading && <Spinner />}
|
||||
</label>
|
||||
|
||||
10
src/components/ui/RadioGroup.module.scss
Normal file
10
src/components/ui/RadioGroup.module.scss
Normal file
@ -0,0 +1,10 @@
|
||||
.wrapper {
|
||||
position: relative;
|
||||
display: block;
|
||||
border: none;
|
||||
margin-bottom: 0;
|
||||
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
|
||||
line-height: 1.5rem;
|
||||
}
|
||||
@ -2,12 +2,14 @@ import type { ChangeEvent } from 'react';
|
||||
import type { FC, TeactNode } from '../../lib/teact/teact';
|
||||
import React, { memo, useCallback } from '../../lib/teact/teact';
|
||||
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
|
||||
import Radio from './Radio';
|
||||
|
||||
export type IRadioOption = {
|
||||
export type IRadioOption<T = string> = {
|
||||
label: TeactNode;
|
||||
subLabel?: string;
|
||||
value: string;
|
||||
value: T;
|
||||
hidden?: boolean;
|
||||
className?: string;
|
||||
};
|
||||
@ -20,6 +22,10 @@ type OwnProps = {
|
||||
disabled?: boolean;
|
||||
loadingOption?: string;
|
||||
onChange: (value: string, event: ChangeEvent<HTMLInputElement>) => void;
|
||||
onClickAction?: (value: string) => void;
|
||||
isLink?: boolean;
|
||||
subLabelClassName?: string;
|
||||
subLabel?: string | undefined;
|
||||
};
|
||||
|
||||
const RadioGroup: FC<OwnProps> = ({
|
||||
@ -30,19 +36,28 @@ const RadioGroup: FC<OwnProps> = ({
|
||||
disabled,
|
||||
loadingOption,
|
||||
onChange,
|
||||
onClickAction,
|
||||
subLabelClassName,
|
||||
isLink,
|
||||
subLabel,
|
||||
}) => {
|
||||
const handleChange = useCallback((event: ChangeEvent<HTMLInputElement>) => {
|
||||
const { value } = event.currentTarget;
|
||||
onChange(value, event);
|
||||
}, [onChange]);
|
||||
|
||||
const onSubLabelClick = useLastCallback((value: string) => () => {
|
||||
onClickAction?.(value);
|
||||
});
|
||||
|
||||
return (
|
||||
<div id={id} className="radio-group">
|
||||
{options.map((option) => (
|
||||
<Radio
|
||||
name={name}
|
||||
label={option.label}
|
||||
subLabel={option.subLabel}
|
||||
subLabel={subLabel || option.subLabel}
|
||||
subLabelClassName={subLabelClassName}
|
||||
value={option.value}
|
||||
checked={option.value === selected}
|
||||
hidden={option.hidden}
|
||||
@ -50,6 +65,8 @@ const RadioGroup: FC<OwnProps> = ({
|
||||
isLoading={loadingOption ? loadingOption === option.value : undefined}
|
||||
className={option.className}
|
||||
onChange={handleChange}
|
||||
onSubLabelClick={onSubLabelClick(option.value)}
|
||||
isLink={isLink}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
132
src/components/ui/RangeSliderWithMarks.module.scss
Normal file
132
src/components/ui/RangeSliderWithMarks.module.scss
Normal file
@ -0,0 +1,132 @@
|
||||
@use '../../styles/mixins';
|
||||
|
||||
@mixin thumb-styles() {
|
||||
border: none;
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
background: var(--color-links);
|
||||
border-radius: 50%;
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
transform: scale(1);
|
||||
transition: transform 0.30s ease-in-out;
|
||||
z-index: 2;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
|
||||
&:hover {
|
||||
transform: scale(1.5);
|
||||
}
|
||||
}
|
||||
|
||||
.dotWrapper {
|
||||
width: 90%;
|
||||
margin-left: 1rem;
|
||||
padding: 1rem 0 0;
|
||||
}
|
||||
|
||||
.sliderContainer {
|
||||
--fill-percentage: 0%;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.marksContainer {
|
||||
width: 100%;
|
||||
pointer-events: none;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.mark {
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 700;
|
||||
color: var(--color-text-secondary);
|
||||
position: relative;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
width: 0.1875rem;
|
||||
}
|
||||
|
||||
.active {
|
||||
color: var(--color-links);
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: relative;
|
||||
-webkit-appearance: none;
|
||||
width: 100%;
|
||||
height: 0.1875rem;
|
||||
outline: none;
|
||||
transition: opacity 0.2s;
|
||||
z-index: 1;
|
||||
|
||||
background: linear-gradient(to right,
|
||||
var(--color-links) 0%,
|
||||
var(--color-links) var(--fill-percentage),
|
||||
var(--color-text-secondary) var(--fill-percentage),
|
||||
var(--color-text-secondary) 100%);
|
||||
}
|
||||
|
||||
.slider::before,
|
||||
.slider::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
height: 100%;
|
||||
transition: transform 0.2s ease;
|
||||
z-index: -1;
|
||||
transform-origin: left;
|
||||
}
|
||||
|
||||
.slider::before {
|
||||
left: 0;
|
||||
background-color: var(--color-links);
|
||||
transform: scaleX(var(--fill-percentage-before));
|
||||
}
|
||||
|
||||
.slider::after {
|
||||
right: 0;
|
||||
background: var(--color-text-secondary);
|
||||
transform: scaleX(var(--fill-percentage-after));
|
||||
transform-origin: right;
|
||||
}
|
||||
|
||||
.slider::-webkit-slider-thumb {
|
||||
@include thumb-styles();
|
||||
}
|
||||
|
||||
.slider::-moz-range-thumb {
|
||||
@include thumb-styles();
|
||||
}
|
||||
|
||||
.slider::-ms-thumb {
|
||||
@include thumb-styles();
|
||||
}
|
||||
|
||||
.tickMarks {
|
||||
position: absolute;
|
||||
top: 2rem;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
pointer-events: none;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.tick {
|
||||
position: relative;
|
||||
width: 0.25rem;
|
||||
height: 0.5rem;
|
||||
background-color: var(--color-text-secondary);
|
||||
border-radius: 0.1875rem;
|
||||
}
|
||||
|
||||
.filled {
|
||||
background-color: var(--color-links);
|
||||
}
|
||||
|
||||
.tickUnfilled {
|
||||
background-color: var(--color-text-secondary);
|
||||
}
|
||||
85
src/components/ui/RangeSliderWithMarks.tsx
Normal file
85
src/components/ui/RangeSliderWithMarks.tsx
Normal file
@ -0,0 +1,85 @@
|
||||
import type { ChangeEvent } from 'react';
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useLayoutEffect, useMemo, useRef,
|
||||
} from '../../lib/teact/teact';
|
||||
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
|
||||
import styles from './RangeSliderWithMarks.module.scss';
|
||||
|
||||
export type OwnProps = {
|
||||
marks: number[];
|
||||
onChange: (value: number) => void;
|
||||
rangeCount: number;
|
||||
};
|
||||
|
||||
const RangeSliderWithMarks: FC<OwnProps> = ({ marks, onChange, rangeCount }) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const sliderRef = useRef<HTMLInputElement | null>(null);
|
||||
|
||||
const fillPercentage = useMemo(() => {
|
||||
return ((marks.indexOf(rangeCount) / (marks.length - 1)) * 100).toFixed(2);
|
||||
}, [marks, rangeCount]);
|
||||
|
||||
const rangeCountIndex = useMemo(() => marks.indexOf(rangeCount), [marks, rangeCount]);
|
||||
|
||||
const rangeValue = useMemo(() => {
|
||||
return marks.indexOf(rangeCount).toString();
|
||||
}, [marks, rangeCount]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
sliderRef.current!.style.setProperty('--fill-percentage', `${fillPercentage}%`);
|
||||
}, [fillPercentage]);
|
||||
|
||||
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
|
||||
const index = parseInt(event.target.value, 10);
|
||||
const newValue = marks[index];
|
||||
onChange(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.dotWrapper}>
|
||||
<form>
|
||||
<div className={styles.sliderContainer}>
|
||||
<div className={styles.tickMarks}>
|
||||
{marks.map((mark, index) => {
|
||||
const isFilled = index <= rangeCountIndex;
|
||||
return (
|
||||
<div
|
||||
key={mark}
|
||||
className={buildClassName(
|
||||
styles.tick,
|
||||
isFilled ? styles.filled : styles.tickUnfilled,
|
||||
)}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className={styles.marksContainer}>
|
||||
{marks.map((mark) => (
|
||||
<div
|
||||
key={mark}
|
||||
className={buildClassName(styles.mark, rangeCount === mark && styles.active)}
|
||||
>
|
||||
{mark}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<input
|
||||
ref={sliderRef}
|
||||
type="range"
|
||||
className={styles.slider}
|
||||
min="0"
|
||||
max={marks.length - 1}
|
||||
value={rangeValue}
|
||||
onChange={handleChange}
|
||||
step="1"
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(RangeSliderWithMarks);
|
||||
@ -312,6 +312,10 @@ export const GENERAL_TOPIC_ID = 1;
|
||||
export const STORY_EXPIRE_PERIOD = 86400; // 1 day
|
||||
export const STORY_VIEWERS_EXPIRE_PERIOD = 86400; // 1 day
|
||||
export const FRESH_AUTH_PERIOD = 86400; // 1 day
|
||||
export const GIVEAWAY_BOOST_PER_PREMIUM = 4;
|
||||
export const GIVEAWAY_MAX_ADDITIONAL_CHANNELS = 10;
|
||||
export const GIVEAWAY_MAX_ADDITIONAL_USERS = 10;
|
||||
export const GIVEAWAY_MAX_ADDITIONAL_COUNTRIES = 10;
|
||||
|
||||
export const LIGHT_THEME_BG_COLOR = '#99BA92';
|
||||
export const DARK_THEME_BG_COLOR = '#0F0F0F';
|
||||
|
||||
@ -104,6 +104,7 @@ addActionHandler('clickBotInlineButton', (global, actions, payload): ActionRetur
|
||||
return;
|
||||
}
|
||||
actions.openInvoice({
|
||||
type: 'message',
|
||||
chatId: chat.id,
|
||||
messageId,
|
||||
tabId,
|
||||
|
||||
@ -1403,11 +1403,13 @@ addActionHandler('openTelegramLink', (global, actions, payload): ActionReturnTyp
|
||||
});
|
||||
} else if (part1.startsWith('$')) {
|
||||
openInvoice({
|
||||
type: 'slug',
|
||||
slug: part1.substring(1),
|
||||
tabId,
|
||||
});
|
||||
} else if (part1 === 'invoice') {
|
||||
openInvoice({
|
||||
type: 'slug',
|
||||
slug: part2,
|
||||
tabId,
|
||||
});
|
||||
|
||||
@ -7,9 +7,11 @@ import { DEBUG_PAYMENT_SMART_GLOCAL } from '../../../config';
|
||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||
import { buildCollectionByKey, unique } from '../../../util/iteratees';
|
||||
import * as langProvider from '../../../util/langProvider';
|
||||
import { getStripeError } from '../../../util/payments/stripe';
|
||||
import { buildQueryString } from '../../../util/requestQuery';
|
||||
import { callApi } from '../../../api/gramjs';
|
||||
import { getStripeError, isChatChannel, isChatSuperGroup } from '../../helpers';
|
||||
import { isChatChannel, isChatSuperGroup } from '../../helpers';
|
||||
import { getRequestInputInvoice } from '../../helpers/payments';
|
||||
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
||||
import {
|
||||
addChats,
|
||||
@ -48,38 +50,24 @@ addActionHandler('validateRequestedInfo', (global, actions, payload): ActionRetu
|
||||
return;
|
||||
}
|
||||
|
||||
if ('slug' in inputInvoice) {
|
||||
void validateRequestedInfo(global, inputInvoice, requestInfo, saveInfo, tabId);
|
||||
} else {
|
||||
const chat = selectChat(global, inputInvoice.chatId);
|
||||
if (!chat) {
|
||||
return;
|
||||
}
|
||||
|
||||
void validateRequestedInfo(global, {
|
||||
chat,
|
||||
messageId: inputInvoice.messageId,
|
||||
}, requestInfo, saveInfo, tabId);
|
||||
const requestInputInvoice = getRequestInputInvoice(global, inputInvoice);
|
||||
if (!requestInputInvoice) {
|
||||
return;
|
||||
}
|
||||
|
||||
validateRequestedInfo(global, requestInputInvoice, requestInfo, saveInfo, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('openInvoice', async (global, actions, payload): Promise<void> => {
|
||||
const { tabId = getCurrentTabId() } = payload;
|
||||
let invoice: ApiInvoice | undefined;
|
||||
if ('slug' in payload) {
|
||||
invoice = await getPaymentForm(global, { slug: payload.slug }, tabId);
|
||||
} else {
|
||||
const chat = selectChat(global, payload.chatId);
|
||||
if (!chat) {
|
||||
return;
|
||||
}
|
||||
const { tabId = getCurrentTabId(), ...inputInvoice } = payload;
|
||||
|
||||
invoice = await getPaymentForm(global, {
|
||||
chat,
|
||||
messageId: payload.messageId,
|
||||
}, tabId);
|
||||
const requestInputInvoice = getRequestInputInvoice(global, inputInvoice);
|
||||
if (!requestInputInvoice) {
|
||||
return;
|
||||
}
|
||||
|
||||
const invoice = await getPaymentForm(global, requestInputInvoice, tabId);
|
||||
|
||||
if (!invoice) {
|
||||
return;
|
||||
}
|
||||
@ -200,21 +188,9 @@ addActionHandler('sendPaymentForm', async (global, actions, payload): Promise<vo
|
||||
return;
|
||||
}
|
||||
|
||||
let requestInputInvoice;
|
||||
if ('slug' in inputInvoice) {
|
||||
requestInputInvoice = {
|
||||
slug: inputInvoice.slug,
|
||||
};
|
||||
} else {
|
||||
const chat = selectChat(global, inputInvoice.chatId);
|
||||
if (!chat) {
|
||||
return;
|
||||
}
|
||||
|
||||
requestInputInvoice = {
|
||||
chat,
|
||||
messageId: inputInvoice.messageId,
|
||||
};
|
||||
const requestInputInvoice = getRequestInputInvoice(global, inputInvoice);
|
||||
if (!requestInputInvoice) {
|
||||
return;
|
||||
}
|
||||
|
||||
global = updatePayment(global, { status: 'pending' }, tabId);
|
||||
@ -406,6 +382,46 @@ addActionHandler('openPremiumModal', async (global, actions, payload): Promise<v
|
||||
actions.closeReactionPicker({ tabId });
|
||||
});
|
||||
|
||||
addActionHandler('openGiveawayModal', async (global, actions, payload): Promise<void> => {
|
||||
const {
|
||||
chatId, prepaidGiveaway,
|
||||
tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) return;
|
||||
|
||||
const result = await callApi('getPremiumGiftCodeOptions', {
|
||||
chat,
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
global = getGlobal();
|
||||
|
||||
const isOpen = Boolean(chatId);
|
||||
|
||||
global = updateTabState(global, {
|
||||
giveawayModal: {
|
||||
chatId,
|
||||
gifts: result,
|
||||
isOpen,
|
||||
prepaidGiveaway,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('closeGiveawayModal', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
|
||||
return updateTabState(global, {
|
||||
giveawayModal: undefined,
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('openGiftPremiumModal', async (global, actions, payload): Promise<void> => {
|
||||
const { forUserId, tabId = getCurrentTabId() } = payload || {};
|
||||
const result = await callApi('fetchPremiumPromo');
|
||||
@ -484,7 +500,7 @@ addActionHandler('openBoostModal', async (global, actions, payload): Promise<voi
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
|
||||
const result = await callApi('fetchBoostsStatus', {
|
||||
const result = await callApi('fetchBoostStatus', {
|
||||
chat,
|
||||
});
|
||||
|
||||
@ -535,8 +551,8 @@ addActionHandler('openBoostStatistics', async (global, actions, payload): Promis
|
||||
setGlobal(global);
|
||||
|
||||
const [boostsListResult, boostStatusResult] = await Promise.all([
|
||||
callApi('fetchBoostsList', { chat }),
|
||||
callApi('fetchBoostsStatus', { chat }),
|
||||
callApi('fetchBoostList', { chat }),
|
||||
callApi('fetchBoostStatus', { chat }),
|
||||
]);
|
||||
|
||||
global = getGlobal();
|
||||
@ -578,7 +594,7 @@ addActionHandler('loadMoreBoosters', async (global, actions, payload): Promise<v
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
|
||||
const result = await callApi('fetchBoostsList', {
|
||||
const result = await callApi('fetchBoostList', {
|
||||
chat,
|
||||
offset: tabState.boostStatistics.nextOffset,
|
||||
});
|
||||
@ -754,3 +770,36 @@ addActionHandler('applyGiftCode', async (global, actions, payload): Promise<void
|
||||
actions.requestConfetti({ tabId });
|
||||
actions.closeGiftCodeModal({ tabId });
|
||||
});
|
||||
|
||||
addActionHandler('launchPrepaidGiveaway', async (global, actions, payload): Promise<void> => {
|
||||
const {
|
||||
chatId, giveawayId, paymentPurpose, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) return;
|
||||
|
||||
const additionalChannels = paymentPurpose?.additionalChannelIds?.map((id) => selectChat(global, id)).filter(Boolean);
|
||||
|
||||
const result = await callApi('launchPrepaidGiveaway', {
|
||||
chat,
|
||||
giveawayId,
|
||||
paymentPurpose: {
|
||||
type: 'giveaway',
|
||||
chat,
|
||||
areWinnersVisible: paymentPurpose?.areWinnersVisible,
|
||||
additionalChannels,
|
||||
countries: paymentPurpose?.countries,
|
||||
prizeDescription: paymentPurpose.prizeDescription,
|
||||
untilDate: paymentPurpose.untilDate,
|
||||
currency: paymentPurpose.currency,
|
||||
amount: paymentPurpose.amount,
|
||||
},
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
actions.openBoostStatistics({ chatId, tabId });
|
||||
});
|
||||
|
||||
@ -4,7 +4,6 @@ export * from './messages';
|
||||
export * from './messageSummary';
|
||||
export * from './messageMedia';
|
||||
export * from './localSearch';
|
||||
export * from './payments';
|
||||
export * from './reactions';
|
||||
export * from './bots';
|
||||
export * from './media';
|
||||
|
||||
@ -1,51 +1,73 @@
|
||||
import type { ApiFieldError } from '../../api/types';
|
||||
import type { ApiInputInvoice, ApiRequestInputInvoice } from '../../api/types';
|
||||
import type { GlobalState } from '../types';
|
||||
|
||||
const STRIPE_ERRORS: Record<string, ApiFieldError> = {
|
||||
missing_payment_information: {
|
||||
field: 'cardNumber',
|
||||
message: 'Incorrect card number',
|
||||
},
|
||||
invalid_number: {
|
||||
field: 'cardNumber',
|
||||
message: 'Incorrect card number',
|
||||
},
|
||||
number: {
|
||||
field: 'cardNumber',
|
||||
message: 'Incorrect card number',
|
||||
},
|
||||
exp_year: {
|
||||
field: 'expiry',
|
||||
message: 'Incorrect year',
|
||||
},
|
||||
exp_month: {
|
||||
field: 'expiry',
|
||||
message: 'Incorrect month',
|
||||
},
|
||||
invalid_expiry_year: {
|
||||
field: 'expiry',
|
||||
message: 'Incorrect year',
|
||||
},
|
||||
invalid_expiry_month: {
|
||||
field: 'expiry',
|
||||
message: 'Incorrect month',
|
||||
},
|
||||
cvc: {
|
||||
field: 'cvv',
|
||||
message: 'Incorrect CVV',
|
||||
},
|
||||
invalid_cvc: {
|
||||
field: 'cvv',
|
||||
message: 'Incorrect CVV',
|
||||
},
|
||||
};
|
||||
import { selectChat, selectUser } from '../selectors';
|
||||
|
||||
export function getStripeError(error: {
|
||||
code: string;
|
||||
message: string;
|
||||
param?: string;
|
||||
}) {
|
||||
const { message: description, code, param } = error;
|
||||
const { field, message } = param ? STRIPE_ERRORS[param] : STRIPE_ERRORS[code];
|
||||
export function getRequestInputInvoice<T extends GlobalState>(
|
||||
global: T, inputInvoice: ApiInputInvoice,
|
||||
): ApiRequestInputInvoice | undefined {
|
||||
if (inputInvoice.type === 'slug') return inputInvoice;
|
||||
|
||||
return { field, message, description };
|
||||
if (inputInvoice.type === 'message') {
|
||||
const chat = selectChat(global, inputInvoice.chatId);
|
||||
if (!chat) {
|
||||
return undefined;
|
||||
}
|
||||
return {
|
||||
type: 'message',
|
||||
chat,
|
||||
messageId: inputInvoice.messageId,
|
||||
};
|
||||
}
|
||||
|
||||
if (inputInvoice.type === 'giftcode') {
|
||||
const {
|
||||
userIds, boostChannelId, amount, currency, option,
|
||||
} = inputInvoice;
|
||||
const users = userIds.map((id) => selectUser(global, id)).filter(Boolean);
|
||||
const boostChannel = boostChannelId ? selectChat(global, boostChannelId) : undefined;
|
||||
|
||||
return {
|
||||
type: 'giveaway',
|
||||
option,
|
||||
purpose: {
|
||||
type: 'giftcode',
|
||||
amount,
|
||||
currency,
|
||||
users,
|
||||
boostChannel,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
if (inputInvoice.type === 'giveaway') {
|
||||
const {
|
||||
chatId, additionalChannelIds, amount, currency, option, untilDate, areWinnersVisible, countries,
|
||||
isOnlyForNewSubscribers, prizeDescription,
|
||||
} = inputInvoice;
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) {
|
||||
return undefined;
|
||||
}
|
||||
const additionalChannels = additionalChannelIds?.map((id) => selectChat(global, id)).filter(Boolean);
|
||||
|
||||
return {
|
||||
type: 'giveaway',
|
||||
option,
|
||||
purpose: {
|
||||
type: 'giveaway',
|
||||
amount,
|
||||
currency,
|
||||
chat,
|
||||
additionalChannels,
|
||||
untilDate,
|
||||
areWinnersVisible,
|
||||
countries,
|
||||
isOnlyForNewSubscribers,
|
||||
prizeDescription,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { GlobalState, TabArgs } from '../types';
|
||||
|
||||
import { getCurrentTabId } from '../../util/establishMultitabRole';
|
||||
import { buildChatThreadKey } from '../helpers';
|
||||
import { buildChatThreadKey } from '../helpers/localSearch';
|
||||
import { selectCurrentMessageList } from './messages';
|
||||
import { selectTabState } from './tabs';
|
||||
|
||||
|
||||
@ -4,7 +4,7 @@ import type { GlobalState, TabArgs } from '../types';
|
||||
import { NewChatMembersProgress, RightColumnContent } from '../../types';
|
||||
|
||||
import { getCurrentTabId } from '../../util/establishMultitabRole';
|
||||
import { getMessageVideo, getMessageWebPageVideo } from '../helpers';
|
||||
import { getMessageVideo, getMessageWebPageVideo } from '../helpers/messageMedia';
|
||||
import { selectCurrentTextSearch } from './localSearch';
|
||||
import { selectCurrentManagement } from './management';
|
||||
import { selectIsStatisticsShown } from './statistics';
|
||||
|
||||
@ -31,6 +31,10 @@ export function selectIsPremiumPurchaseBlocked<T extends GlobalState>(global: T)
|
||||
return global.appConfig?.isPremiumPurchaseBlocked ?? true;
|
||||
}
|
||||
|
||||
export function selectIsGiveawayGiftsPurchaseAvailable<T extends GlobalState>(global: T) {
|
||||
return global.appConfig?.isGiveawayGiftsPurchaseAvailable ?? true;
|
||||
}
|
||||
|
||||
// Slow, not to be used in `withGlobal`
|
||||
export function selectUserByPhoneNumber<T extends GlobalState>(global: T, phoneNumber: string) {
|
||||
const phoneNumberCleaned = phoneNumber.replace(/[^0-9]/g, '');
|
||||
|
||||
@ -44,7 +44,9 @@ import type {
|
||||
ApiPhoneCall,
|
||||
ApiPhoto,
|
||||
ApiPostStatistics,
|
||||
ApiPremiumGiftCodeOption,
|
||||
ApiPremiumPromo,
|
||||
ApiPrepaidGiveaway,
|
||||
ApiQuickReply,
|
||||
ApiReaction,
|
||||
ApiReactionKey,
|
||||
@ -625,6 +627,15 @@ export type TabState = {
|
||||
isSuccess?: boolean;
|
||||
};
|
||||
|
||||
giveawayModal?: {
|
||||
chatId: string;
|
||||
isOpen?: boolean;
|
||||
gifts?: ApiPremiumGiftCodeOption[];
|
||||
selectedMemberIds?: string[];
|
||||
selectedChannelIds?: string[];
|
||||
prepaidGiveaway?: ApiPrepaidGiveaway;
|
||||
};
|
||||
|
||||
giftPremiumModal?: {
|
||||
isOpen?: boolean;
|
||||
forUserId?: string;
|
||||
@ -1989,6 +2000,20 @@ export interface ActionPayloads {
|
||||
} & WithTabId;
|
||||
closeGiftCodeModal: WithTabId | undefined;
|
||||
|
||||
launchPrepaidGiveaway: {
|
||||
chatId: string;
|
||||
giveawayId: string;
|
||||
paymentPurpose: {
|
||||
additionalChannelIds?: string[];
|
||||
areWinnersVisible?: boolean;
|
||||
countries?: string[];
|
||||
prizeDescription?: string;
|
||||
untilDate: number;
|
||||
currency: string;
|
||||
amount: number;
|
||||
};
|
||||
} & WithTabId;
|
||||
|
||||
checkChatlistInvite: {
|
||||
slug: string;
|
||||
} & WithTabId;
|
||||
@ -2914,6 +2939,13 @@ export interface ActionPayloads {
|
||||
} & WithTabId) | undefined;
|
||||
closePremiumModal: WithTabId | undefined;
|
||||
|
||||
openGiveawayModal: ({
|
||||
chatId: string;
|
||||
gifts?: number[] | undefined;
|
||||
prepaidGiveaway?: ApiPrepaidGiveaway | undefined;
|
||||
} & WithTabId);
|
||||
closeGiveawayModal: WithTabId | undefined;
|
||||
|
||||
transcribeAudio: {
|
||||
chatId: string;
|
||||
messageId: number;
|
||||
|
||||
@ -1525,9 +1525,11 @@ payments.getPaymentReceipt#2478d1cc peer:InputPeer msg_id:int = payments.Payment
|
||||
payments.validateRequestedInfo#b6c8f12b flags:# save:flags.0?true invoice:InputInvoice info:PaymentRequestedInfo = payments.ValidatedRequestedInfo;
|
||||
payments.sendPaymentForm#2d03522f flags:# form_id:long invoice:InputInvoice requested_info_id:flags.0?string shipping_option_id:flags.1?string credentials:InputPaymentCredentials tip_amount:flags.2?long = payments.PaymentResult;
|
||||
payments.getSavedInfo#227d824b = payments.SavedInfo;
|
||||
payments.getPremiumGiftCodeOptions#2757ba54 flags:# boost_peer:flags.0?InputPeer = Vector<PremiumGiftCodeOption>;
|
||||
payments.checkGiftCode#8e51b4c1 slug:string = payments.CheckedGiftCode;
|
||||
payments.applyGiftCode#f6e26854 slug:string = Updates;
|
||||
payments.getGiveawayInfo#f4239425 peer:InputPeer msg_id:int = payments.GiveawayInfo;
|
||||
payments.launchPrepaidGiveaway#5ff58f20 peer:InputPeer giveaway_id:long purpose:InputStorePaymentPurpose = Updates;
|
||||
phone.requestCall#42ff96ed flags:# video:flags.0?true user_id:InputUser random_id:int g_a_hash:bytes protocol:PhoneCallProtocol = phone.PhoneCall;
|
||||
phone.acceptCall#3bd2b4a0 peer:InputPhoneCall g_b:bytes protocol:PhoneCallProtocol = phone.PhoneCall;
|
||||
phone.confirmCall#2efe1722 peer:InputPhoneCall g_a:bytes key_fingerprint:long protocol:PhoneCallProtocol = phone.PhoneCall;
|
||||
|
||||
@ -343,5 +343,7 @@
|
||||
"premium.getBoostsList",
|
||||
"payments.checkGiftCode",
|
||||
"payments.applyGiftCode",
|
||||
"payments.getGiveawayInfo"
|
||||
"payments.getGiveawayInfo",
|
||||
"payments.getPremiumGiftCodeOptions",
|
||||
"payments.launchPrepaidGiveaway"
|
||||
]
|
||||
|
||||
@ -101,8 +101,8 @@ $color-message-story-mention-to: #74bcff;
|
||||
--color-primary-shade: #{color.mix($color-primary, $color-black, 92%)};
|
||||
--color-primary-shade-darker: #{color.mix($color-primary, $color-black, 84%)};
|
||||
--color-primary-shade-rgb: #{toRGB(color.mix($color-primary, $color-black, 92%))};
|
||||
--color-primary-opacity: rgba(var(--color-primary), 0.25);
|
||||
--color-primary-opacity-hover: rgba(var(--color-primary), 0.15);
|
||||
--color-primary-opacity: rgba(var(--color-primary), 0.15);
|
||||
--color-primary-opacity-hover: rgba(var(--color-primary), 0.25);
|
||||
--color-primary-tint: rgba(var(--color-primary), 0.1);
|
||||
--color-green: #{$color-green};
|
||||
--color-green-darker: #{color.mix($color-green, $color-black, 84%)};
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
{
|
||||
"--color-primary": ["#3390EC", "#8774E1"],
|
||||
"--color-primary-opacity": ["#50A2E940", "#8378DB80"],
|
||||
"--color-primary-opacity-hover": ["#50A2E926", "#8378DBA0"],
|
||||
"--color-primary-opacity": ["#50A2E91E", "#8378DB1E"],
|
||||
"--color-primary-opacity-hover": ["#50A2E940", "#8378DB40"],
|
||||
"--color-primary-tint": ["#3390ec1a", "#8774e11a"],
|
||||
"--color-primary-shade": ["#4a95d6", "#7b71c6"],
|
||||
"--color-background": ["#FFFFFF", "#212121"],
|
||||
|
||||
@ -150,7 +150,7 @@ export const processDeepLink = (url: string): boolean => {
|
||||
|
||||
case 'invoice': {
|
||||
const { slug } = params;
|
||||
openInvoice({ slug });
|
||||
openInvoice({ type: 'slug', slug });
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
51
src/util/payments/stripe.ts
Normal file
51
src/util/payments/stripe.ts
Normal file
@ -0,0 +1,51 @@
|
||||
import type { ApiFieldError } from '../../api/types';
|
||||
|
||||
const STRIPE_ERRORS: Record<string, ApiFieldError> = {
|
||||
missing_payment_information: {
|
||||
field: 'cardNumber',
|
||||
message: 'Incorrect card number',
|
||||
},
|
||||
invalid_number: {
|
||||
field: 'cardNumber',
|
||||
message: 'Incorrect card number',
|
||||
},
|
||||
number: {
|
||||
field: 'cardNumber',
|
||||
message: 'Incorrect card number',
|
||||
},
|
||||
exp_year: {
|
||||
field: 'expiry',
|
||||
message: 'Incorrect year',
|
||||
},
|
||||
exp_month: {
|
||||
field: 'expiry',
|
||||
message: 'Incorrect month',
|
||||
},
|
||||
invalid_expiry_year: {
|
||||
field: 'expiry',
|
||||
message: 'Incorrect year',
|
||||
},
|
||||
invalid_expiry_month: {
|
||||
field: 'expiry',
|
||||
message: 'Incorrect month',
|
||||
},
|
||||
cvc: {
|
||||
field: 'cvv',
|
||||
message: 'Incorrect CVV',
|
||||
},
|
||||
invalid_cvc: {
|
||||
field: 'cvv',
|
||||
message: 'Incorrect CVV',
|
||||
},
|
||||
};
|
||||
|
||||
export function getStripeError(error: {
|
||||
code: string;
|
||||
message: string;
|
||||
param?: string;
|
||||
}) {
|
||||
const { message: description, code, param } = error;
|
||||
const { field, message } = param ? STRIPE_ERRORS[param] : STRIPE_ERRORS[code];
|
||||
|
||||
return { field, message, description };
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user