Introduce giveaway support (#3960)
This commit is contained in:
parent
1eea365bb2
commit
e050f427ee
@ -6,6 +6,7 @@ import type {
|
||||
ApiDocument,
|
||||
ApiFormattedText,
|
||||
ApiGame,
|
||||
ApiGiveaway,
|
||||
ApiInvoice,
|
||||
ApiLocation,
|
||||
ApiMessageExtendedMediaPreview,
|
||||
@ -49,7 +50,7 @@ export function buildMessageContent(
|
||||
const hasUnsupportedMedia = mtpMessage.media instanceof GramJs.MessageMediaUnsupported;
|
||||
|
||||
if (mtpMessage.message && !hasUnsupportedMedia
|
||||
&& !content.sticker && !content.poll && !content.contact && !(content.video?.isRound)) {
|
||||
&& !content.sticker && !content.poll && !content.contact && !content.video?.isRound) {
|
||||
content = {
|
||||
...content,
|
||||
text: buildMessageTextContent(mtpMessage.message, mtpMessage.entities),
|
||||
@ -118,6 +119,9 @@ export function buildMessageMediaContent(media: GramJs.TypeMessageMedia): MediaC
|
||||
const storyData = buildMessageStoryData(media);
|
||||
if (storyData) return { storyData };
|
||||
|
||||
const giveaway = buildGiweawayFromMedia(media);
|
||||
if (giveaway) return { giveaway };
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -466,6 +470,31 @@ function buildGame(media: GramJs.MessageMediaGame): ApiGame | undefined {
|
||||
};
|
||||
}
|
||||
|
||||
function buildGiweawayFromMedia(media: GramJs.TypeMessageMedia): ApiGiveaway | undefined {
|
||||
if (!(media instanceof GramJs.MessageMediaGiveaway)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return buildGiveaway(media);
|
||||
}
|
||||
|
||||
function buildGiveaway(media: GramJs.MessageMediaGiveaway): ApiGiveaway | undefined {
|
||||
const {
|
||||
channels, months, quantity, untilDate, countriesIso2, onlyNewSubscribers,
|
||||
} = media;
|
||||
|
||||
const channelIds = channels.map((channel) => buildApiPeerId(channel, 'channel'));
|
||||
|
||||
return {
|
||||
channelIds,
|
||||
months,
|
||||
quantity,
|
||||
untilDate,
|
||||
countries: countriesIso2,
|
||||
isOnlyForNewSubscribers: onlyNewSubscribers,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildMessageStoryData(media: GramJs.TypeMessageMedia): ApiMessageStoryData | undefined {
|
||||
if (!(media instanceof GramJs.MessageMediaStory)) {
|
||||
return undefined;
|
||||
|
||||
@ -336,6 +336,8 @@ function buildAction(
|
||||
let months: number | undefined;
|
||||
let topicEmojiIconId: string | undefined;
|
||||
let isTopicAction: boolean | undefined;
|
||||
let slug: string | undefined;
|
||||
let isGiveaway: boolean | undefined;
|
||||
|
||||
const targetUserIds = 'users' in action
|
||||
? action.users && action.users.map((id) => buildApiPeerId(id, 'user'))
|
||||
@ -523,6 +525,17 @@ function buildAction(
|
||||
translationValues.push('%target_user%');
|
||||
|
||||
if (targetPeerId) targetUserIds.push(targetPeerId);
|
||||
} else if (action instanceof GramJs.MessageActionGiveawayLaunch) {
|
||||
text = 'BoostingGiveawayJustStarted';
|
||||
translationValues.push('%action_origin%');
|
||||
} else if (action instanceof GramJs.MessageActionGiftCode) {
|
||||
text = 'BoostingReceivedGiftNoName';
|
||||
slug = action.slug;
|
||||
months = action.months;
|
||||
isGiveaway = Boolean(action.viaGiveaway);
|
||||
if (action.boostPeer) {
|
||||
targetChatId = getApiChatIdFromMtpPeer(action.boostPeer);
|
||||
}
|
||||
} else {
|
||||
text = 'ChatList.UnsupportedMessage';
|
||||
}
|
||||
@ -541,6 +554,8 @@ function buildAction(
|
||||
amount,
|
||||
currency,
|
||||
giftCryptoInfo,
|
||||
isGiveaway,
|
||||
slug,
|
||||
translationValues,
|
||||
call,
|
||||
phoneCall,
|
||||
|
||||
@ -1,6 +1,10 @@
|
||||
import type { Api as GramJs } from '../../../lib/gramjs';
|
||||
import { Api as GramJs } from '../../../lib/gramjs';
|
||||
|
||||
import type {
|
||||
ApiInvoice, ApiLabeledPrice, ApiPaymentCredentials,
|
||||
ApiBoostsStatus,
|
||||
ApiCheckedGiftCode,
|
||||
ApiGiveawayInfo,
|
||||
ApiInvoice, ApiLabeledPrice, ApiMyBoost, ApiPaymentCredentials,
|
||||
ApiPaymentForm, ApiPaymentSavedInfo, ApiPremiumPromo, ApiPremiumSubscriptionOption,
|
||||
ApiReceipt,
|
||||
} from '../../types';
|
||||
@ -8,6 +12,8 @@ import type {
|
||||
import { buildApiMessageEntity } from './common';
|
||||
import { omitVirtualClassFields } from './helpers';
|
||||
import { buildApiDocument, buildApiWebDocument } from './messageContent';
|
||||
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
|
||||
import { buildStatisticsPercentage } from './statistics';
|
||||
|
||||
export function buildShippingOptions(shippingOptions: GramJs.ShippingOption[] | undefined) {
|
||||
if (!shippingOptions) {
|
||||
@ -197,3 +203,92 @@ function buildApiPremiumSubscriptionOption(option: GramJs.PremiumSubscriptionOpt
|
||||
export function buildApiPaymentCredentials(credentials: GramJs.PaymentSavedCredentialsCard[]): ApiPaymentCredentials[] {
|
||||
return credentials.map(({ id, title }) => ({ id, title }));
|
||||
}
|
||||
|
||||
export function buildApiBoostsStatus(boostStatus: GramJs.premium.BoostsStatus): ApiBoostsStatus {
|
||||
const {
|
||||
level, boostUrl, boosts, myBoost, currentLevelBoosts, nextLevelBoosts, premiumAudience,
|
||||
} = boostStatus;
|
||||
return {
|
||||
level,
|
||||
currentLevelBoosts,
|
||||
boosts,
|
||||
hasMyBoost: Boolean(myBoost),
|
||||
boostUrl,
|
||||
nextLevelBoosts,
|
||||
...(premiumAudience && { premiumSubscribers: buildStatisticsPercentage(premiumAudience) }),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildApiMyBoost(myBoost: GramJs.MyBoost): ApiMyBoost {
|
||||
const {
|
||||
date, expires, slot, cooldownUntilDate, peer,
|
||||
} = myBoost;
|
||||
|
||||
return {
|
||||
date,
|
||||
expires,
|
||||
slot,
|
||||
cooldownUntil: cooldownUntilDate,
|
||||
chatId: peer && getApiChatIdFromMtpPeer(peer),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildApiGiveawayInfo(info: GramJs.payments.TypeGiveawayInfo): ApiGiveawayInfo | undefined {
|
||||
if (info instanceof GramJs.payments.GiveawayInfo) {
|
||||
const {
|
||||
startDate,
|
||||
adminDisallowedChatId,
|
||||
disallowedCountry,
|
||||
joinedTooEarlyDate,
|
||||
participating,
|
||||
preparingResults,
|
||||
} = info;
|
||||
|
||||
return {
|
||||
type: 'active',
|
||||
startDate,
|
||||
isParticipating: participating,
|
||||
adminDisallowedChatId: adminDisallowedChatId?.toString(),
|
||||
disallowedCountry,
|
||||
joinedTooEarlyDate,
|
||||
isPreparingResults: preparingResults,
|
||||
};
|
||||
} else {
|
||||
const {
|
||||
activatedCount,
|
||||
finishDate,
|
||||
giftCodeSlug,
|
||||
winner,
|
||||
refunded,
|
||||
startDate,
|
||||
winnersCount,
|
||||
} = info;
|
||||
|
||||
return {
|
||||
type: 'results',
|
||||
startDate,
|
||||
activatedCount,
|
||||
finishDate,
|
||||
winnersCount,
|
||||
giftCodeSlug,
|
||||
isRefunded: refunded,
|
||||
isWinner: winner,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export function buildApiCheckedGiftCode(giftcode: GramJs.payments.TypeCheckedGiftCode): ApiCheckedGiftCode {
|
||||
const {
|
||||
date, fromId, months, giveawayMsgId, toId, usedDate, viaGiveaway,
|
||||
} = giftcode;
|
||||
|
||||
return {
|
||||
date,
|
||||
months,
|
||||
toId: toId && buildApiPeerId(toId, 'user'),
|
||||
fromId: fromId && getApiChatIdFromMtpPeer(fromId),
|
||||
usedAt: usedDate,
|
||||
isFromGiveaway: viaGiveaway,
|
||||
giveawayMessageId: giveawayMsgId,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
import { Api as GramJs } from '../../../lib/gramjs';
|
||||
|
||||
import type {
|
||||
ApiBoostsStatus,
|
||||
ApiMediaArea,
|
||||
ApiMediaAreaCoordinates,
|
||||
ApiMyBoost,
|
||||
ApiStealthMode,
|
||||
ApiStoryView,
|
||||
ApiTypeStory,
|
||||
@ -16,7 +14,6 @@ import { buildPrivacyRules } from './common';
|
||||
import { buildGeoPoint, buildMessageMediaContent, buildMessageTextContent } from './messageContent';
|
||||
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
|
||||
import { buildApiReaction, buildReactionCount } from './reactions';
|
||||
import { buildStatisticsPercentage } from './statistics';
|
||||
|
||||
export function buildApiStory(peerId: string, story: GramJs.TypeStoryItem): ApiTypeStory {
|
||||
if (story instanceof GramJs.StoryItemDeleted) {
|
||||
@ -169,32 +166,3 @@ export function buildApiPeerStories(peerStories: GramJs.PeerStories) {
|
||||
|
||||
return buildCollectionByCallback(peerStories.stories, (story) => [story.id, buildApiStory(peerId, story)]);
|
||||
}
|
||||
|
||||
export function buildApiBoostsStatus(boostStatus: GramJs.premium.BoostsStatus): ApiBoostsStatus {
|
||||
const {
|
||||
level, boostUrl, boosts, myBoost, currentLevelBoosts, nextLevelBoosts, premiumAudience,
|
||||
} = boostStatus;
|
||||
return {
|
||||
level,
|
||||
currentLevelBoosts,
|
||||
boosts,
|
||||
hasMyBoost: Boolean(myBoost),
|
||||
boostUrl,
|
||||
nextLevelBoosts,
|
||||
...(premiumAudience && { premiumSubscribers: buildStatisticsPercentage(premiumAudience) }),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildApiMyBoost(myBoost: GramJs.MyBoost): ApiMyBoost {
|
||||
const {
|
||||
date, expires, slot, cooldownUntilDate, peer,
|
||||
} = myBoost;
|
||||
|
||||
return {
|
||||
date,
|
||||
expires,
|
||||
slot,
|
||||
cooldownUntil: cooldownUntilDate,
|
||||
chatId: peer && getApiChatIdFromMtpPeer(peer),
|
||||
};
|
||||
}
|
||||
|
||||
@ -80,10 +80,6 @@ export {
|
||||
allowBotSendMessages, fetchBotCanSendMessage, invokeWebViewCustomMethod,
|
||||
} from './bots';
|
||||
|
||||
export {
|
||||
validateRequestedInfo, sendPaymentForm, getPaymentForm, getReceipt, fetchPremiumPromo, fetchTemporaryPaymentPassword,
|
||||
} from './payments';
|
||||
|
||||
export {
|
||||
getGroupCall, joinGroupCall, discardGroupCall, createGroupCall,
|
||||
editGroupCallTitle, editGroupCallParticipant, exportGroupCallInvite, fetchGroupCallParticipants,
|
||||
@ -111,3 +107,8 @@ export {
|
||||
} from '../localDb';
|
||||
|
||||
export * from './stories';
|
||||
|
||||
export {
|
||||
validateRequestedInfo, sendPaymentForm, getPaymentForm, getReceipt, fetchPremiumPromo, fetchTemporaryPaymentPassword,
|
||||
applyBoost, fetchBoostsList, fetchBoostsStatus, fetchGiveawayInfo, fetchMyBoosts, applyGiftCode, checkGiftCode,
|
||||
} from './payments';
|
||||
|
||||
@ -2,12 +2,18 @@ import BigInt from 'big-integer';
|
||||
import { Api as GramJs } from '../../../lib/gramjs';
|
||||
|
||||
import type {
|
||||
ApiChat, ApiRequestInputInvoice,
|
||||
ApiChat, ApiPeer, ApiRequestInputInvoice,
|
||||
OnApiUpdate,
|
||||
} from '../../types';
|
||||
|
||||
import { buildCollectionByCallback } from '../../../util/iteratees';
|
||||
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
||||
import {
|
||||
buildApiBoostsStatus,
|
||||
buildApiCheckedGiftCode,
|
||||
buildApiGiveawayInfo,
|
||||
buildApiInvoiceFromForm,
|
||||
buildApiMyBoost,
|
||||
buildApiPaymentForm,
|
||||
buildApiPremiumPromo,
|
||||
buildApiReceipt,
|
||||
@ -186,3 +192,145 @@ export async function fetchTemporaryPaymentPassword(password: string) {
|
||||
validUntil: result.validUntil,
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchMyBoosts() {
|
||||
const result = await invokeRequest(new GramJs.premium.GetMyBoosts());
|
||||
|
||||
if (!result) return undefined;
|
||||
|
||||
addEntitiesToLocalDb(result.users);
|
||||
addEntitiesToLocalDb(result.chats);
|
||||
|
||||
const users = result.users.map(buildApiUser).filter(Boolean);
|
||||
const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean);
|
||||
const boosts = result.myBoosts.map(buildApiMyBoost);
|
||||
|
||||
return {
|
||||
users,
|
||||
chats,
|
||||
boosts,
|
||||
};
|
||||
}
|
||||
|
||||
export function applyBoost({
|
||||
chat,
|
||||
slots,
|
||||
} : {
|
||||
chat: ApiChat;
|
||||
slots: number[];
|
||||
}) {
|
||||
return invokeRequest(new GramJs.premium.ApplyBoost({
|
||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||
slots,
|
||||
}), {
|
||||
shouldReturnTrue: true,
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchBoostsStatus({
|
||||
chat,
|
||||
}: {
|
||||
chat: ApiChat;
|
||||
}) {
|
||||
const result = await invokeRequest(new GramJs.premium.GetBoostsStatus({
|
||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||
}));
|
||||
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return buildApiBoostsStatus(result);
|
||||
}
|
||||
|
||||
export async function fetchBoostsList({
|
||||
chat,
|
||||
offset = '',
|
||||
limit,
|
||||
}: {
|
||||
chat: ApiChat;
|
||||
offset?: string;
|
||||
limit?: number;
|
||||
}) {
|
||||
const result = await invokeRequest(new GramJs.premium.GetBoostsList({
|
||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||
offset,
|
||||
limit,
|
||||
}));
|
||||
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
addEntitiesToLocalDb(result.users);
|
||||
|
||||
const users = result.users.map(buildApiUser).filter(Boolean);
|
||||
|
||||
const userBoosts = result.boosts.filter((boost) => boost.userId);
|
||||
const boosterIds = userBoosts.map((boost) => boost.userId!.toString());
|
||||
const boosters = buildCollectionByCallback(userBoosts, (boost) => (
|
||||
[boost.userId!.toString(), boost.expires]
|
||||
));
|
||||
|
||||
return {
|
||||
count: result.count,
|
||||
users,
|
||||
boosters,
|
||||
boosterIds,
|
||||
nextOffset: result.nextOffset,
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchGiveawayInfo({
|
||||
peer,
|
||||
messageId,
|
||||
}: {
|
||||
peer: ApiPeer;
|
||||
messageId: number;
|
||||
}) {
|
||||
const result = await invokeRequest(new GramJs.payments.GetGiveawayInfo({
|
||||
peer: buildInputPeer(peer.id, peer.accessHash),
|
||||
msgId: messageId,
|
||||
}));
|
||||
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return buildApiGiveawayInfo(result);
|
||||
}
|
||||
|
||||
export async function checkGiftCode({
|
||||
slug,
|
||||
}: {
|
||||
slug: string;
|
||||
}) {
|
||||
const result = await invokeRequest(new GramJs.payments.CheckGiftCode({
|
||||
slug,
|
||||
}));
|
||||
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
addEntitiesToLocalDb(result.users);
|
||||
addEntitiesToLocalDb(result.chats);
|
||||
|
||||
return {
|
||||
code: buildApiCheckedGiftCode(result),
|
||||
users: result.users.map(buildApiUser).filter(Boolean),
|
||||
chats: result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean),
|
||||
};
|
||||
}
|
||||
|
||||
export function applyGiftCode({
|
||||
slug,
|
||||
}: {
|
||||
slug: string;
|
||||
}) {
|
||||
return invokeRequest(new GramJs.payments.ApplyGiftCode({
|
||||
slug,
|
||||
}), {
|
||||
shouldReturnTrue: true,
|
||||
});
|
||||
}
|
||||
|
||||
@ -17,8 +17,6 @@ import { buildCollectionByCallback } from '../../../util/iteratees';
|
||||
import { buildApiChatFromPreview } from '../apiBuilders/chats';
|
||||
import { getApiChatIdFromMtpPeer } from '../apiBuilders/peers';
|
||||
import {
|
||||
buildApiBoostsStatus,
|
||||
buildApiMyBoost,
|
||||
buildApiPeerStories,
|
||||
buildApiStealthMode,
|
||||
buildApiStory,
|
||||
@ -428,91 +426,3 @@ export function activateStealthMode({
|
||||
shouldReturnTrue: true,
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchMyBoosts() {
|
||||
const result = await invokeRequest(new GramJs.premium.GetMyBoosts());
|
||||
|
||||
if (!result) return undefined;
|
||||
|
||||
addEntitiesToLocalDb(result.users);
|
||||
addEntitiesToLocalDb(result.chats);
|
||||
|
||||
const users = result.users.map(buildApiUser).filter(Boolean);
|
||||
const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean);
|
||||
const boosts = result.myBoosts.map(buildApiMyBoost);
|
||||
|
||||
return {
|
||||
users,
|
||||
chats,
|
||||
boosts,
|
||||
};
|
||||
}
|
||||
|
||||
export function applyBoost({
|
||||
chat,
|
||||
slots,
|
||||
} : {
|
||||
chat: ApiChat;
|
||||
slots: number[];
|
||||
}) {
|
||||
return invokeRequest(new GramJs.premium.ApplyBoost({
|
||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||
slots,
|
||||
}), {
|
||||
shouldReturnTrue: true,
|
||||
});
|
||||
}
|
||||
|
||||
export async function fetchBoostsStatus({
|
||||
chat,
|
||||
}: {
|
||||
chat: ApiChat;
|
||||
}) {
|
||||
const result = await invokeRequest(new GramJs.premium.GetBoostsStatus({
|
||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||
}));
|
||||
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return buildApiBoostsStatus(result);
|
||||
}
|
||||
|
||||
export async function fetchBoostersList({
|
||||
chat,
|
||||
offset = '',
|
||||
limit,
|
||||
}: {
|
||||
chat: ApiChat;
|
||||
offset?: string;
|
||||
limit?: number;
|
||||
}) {
|
||||
const result = await invokeRequest(new GramJs.premium.GetBoostsList({
|
||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||
offset,
|
||||
limit,
|
||||
}));
|
||||
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
addEntitiesToLocalDb(result.users);
|
||||
|
||||
const users = result.users.map(buildApiUser).filter(Boolean);
|
||||
|
||||
const userBoosts = result.boosts.filter((boost) => boost.userId);
|
||||
const boosterIds = userBoosts.map((boost) => boost.userId!.toString());
|
||||
const boosters = buildCollectionByCallback(userBoosts, (boost) => (
|
||||
[boost.userId!.toString(), boost.expires]
|
||||
));
|
||||
|
||||
return {
|
||||
count: result.count,
|
||||
users,
|
||||
boosters,
|
||||
boosterIds,
|
||||
nextOffset: result.nextOffset,
|
||||
};
|
||||
}
|
||||
|
||||
@ -253,6 +253,15 @@ export type ApiGame = {
|
||||
document?: ApiDocument;
|
||||
};
|
||||
|
||||
export type ApiGiveaway = {
|
||||
quantity: number;
|
||||
months: number;
|
||||
untilDate: number;
|
||||
isOnlyForNewSubscribers?: true;
|
||||
countries?: string[];
|
||||
channelIds: string[];
|
||||
};
|
||||
|
||||
export type ApiNewPoll = {
|
||||
summary: ApiPoll['summary'];
|
||||
quiz?: {
|
||||
@ -281,6 +290,8 @@ export interface ApiAction {
|
||||
months?: number;
|
||||
topicEmojiIconId?: string;
|
||||
isTopicAction?: boolean;
|
||||
slug?: string;
|
||||
isGiveaway?: boolean;
|
||||
}
|
||||
|
||||
export interface ApiWebPage {
|
||||
@ -432,6 +443,7 @@ export type MediaContent = {
|
||||
location?: ApiLocation;
|
||||
game?: ApiGame;
|
||||
storyData?: ApiMessageStoryData;
|
||||
giveaway?: ApiGiveaway;
|
||||
};
|
||||
|
||||
export interface ApiMessage {
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { ApiInvoiceContainer } from '../../types';
|
||||
import type { ApiWebDocument } from './bots';
|
||||
import type { ApiDocument, ApiMessageEntity, ApiPaymentCredentials } from './messages';
|
||||
import type { StatisticsOverviewPercentage } from './statistics';
|
||||
|
||||
export interface ApiShippingAddress {
|
||||
streetLine1: string;
|
||||
@ -77,3 +78,54 @@ export interface ApiPremiumSubscriptionOption {
|
||||
amount: string;
|
||||
botUrl: string;
|
||||
}
|
||||
|
||||
export type ApiBoostsStatus = {
|
||||
level: number;
|
||||
currentLevelBoosts: number;
|
||||
boosts: number;
|
||||
nextLevelBoosts?: number;
|
||||
hasMyBoost?: boolean;
|
||||
boostUrl: string;
|
||||
premiumSubscribers?: StatisticsOverviewPercentage;
|
||||
};
|
||||
|
||||
export type ApiMyBoost = {
|
||||
slot: number;
|
||||
chatId?: string;
|
||||
date: number;
|
||||
expires: number;
|
||||
cooldownUntil?: number;
|
||||
};
|
||||
|
||||
export type ApiGiveawayInfoActive = {
|
||||
type: 'active';
|
||||
isParticipating?: true;
|
||||
isPreparingResults?: true;
|
||||
startDate: number;
|
||||
joinedTooEarlyDate?: number;
|
||||
adminDisallowedChatId?: string;
|
||||
disallowedCountry?: string;
|
||||
};
|
||||
|
||||
export type ApiGiveawayInfoResults = {
|
||||
type: 'results';
|
||||
isWinner?: true;
|
||||
isRefunded?: true;
|
||||
startDate: number;
|
||||
finishDate: number;
|
||||
giftCodeSlug?: string;
|
||||
winnersCount: number;
|
||||
activatedCount: number;
|
||||
};
|
||||
|
||||
export type ApiGiveawayInfo = ApiGiveawayInfoActive | ApiGiveawayInfoResults;
|
||||
|
||||
export type ApiCheckedGiftCode = {
|
||||
isFromGiveaway?: true;
|
||||
fromId?: string;
|
||||
giveawayMessageId?: number;
|
||||
toId?: string;
|
||||
date: number;
|
||||
months: number;
|
||||
usedAt?: number;
|
||||
};
|
||||
|
||||
@ -2,7 +2,6 @@ import type { ApiPrivacySettings } from '../../types';
|
||||
import type {
|
||||
ApiGeoPoint, ApiReaction, ApiReactionCount, MediaContent,
|
||||
} from './messages';
|
||||
import type { StatisticsOverviewPercentage } from './statistics';
|
||||
|
||||
export interface ApiStory {
|
||||
'@type'?: 'story';
|
||||
@ -109,21 +108,3 @@ export type ApiMediaAreaSuggestedReaction = {
|
||||
};
|
||||
|
||||
export type ApiMediaArea = ApiMediaAreaVenue | ApiMediaAreaGeoPoint | ApiMediaAreaSuggestedReaction;
|
||||
|
||||
export type ApiBoostsStatus = {
|
||||
level: number;
|
||||
currentLevelBoosts: number;
|
||||
boosts: number;
|
||||
nextLevelBoosts?: number;
|
||||
hasMyBoost?: boolean;
|
||||
boostUrl: string;
|
||||
premiumSubscribers?: StatisticsOverviewPercentage;
|
||||
};
|
||||
|
||||
export type ApiMyBoost = {
|
||||
slot: number;
|
||||
chatId?: string;
|
||||
date: number;
|
||||
expires: number;
|
||||
cooldownUntil?: number;
|
||||
};
|
||||
|
||||
@ -20,7 +20,7 @@ export { default as GiftPremiumModal } from '../components/main/premium/GiftPrem
|
||||
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';
|
||||
|
||||
export { default as GiftCodeModal } from '../components/modals/giftcode/GiftCodeModal';
|
||||
export { default as ChatlistModal } from '../components/modals/chatlist/ChatlistModal';
|
||||
|
||||
export { default as AboutAdsModal } from '../components/common/AboutAdsModal';
|
||||
|
||||
@ -9,7 +9,7 @@
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.moreMenu {
|
||||
.moreMenu, .copy {
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
top: 50%;
|
||||
@ -12,22 +12,25 @@ import useLastCallback from '../../hooks/useLastCallback';
|
||||
import Button from '../ui/Button';
|
||||
import DropdownMenu from '../ui/DropdownMenu';
|
||||
import MenuItem from '../ui/MenuItem';
|
||||
import Icon from './Icon';
|
||||
|
||||
import styles from './InviteLink.module.scss';
|
||||
import styles from './LinkField.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
title?: string;
|
||||
inviteLink: string;
|
||||
link: string;
|
||||
isDisabled?: boolean;
|
||||
className?: string;
|
||||
withShare?: boolean;
|
||||
onRevoke?: VoidFunction;
|
||||
};
|
||||
|
||||
const InviteLink: FC<OwnProps> = ({
|
||||
title,
|
||||
inviteLink,
|
||||
link,
|
||||
isDisabled,
|
||||
className,
|
||||
withShare,
|
||||
onRevoke,
|
||||
}) => {
|
||||
const lang = useLang();
|
||||
@ -35,20 +38,22 @@ const InviteLink: FC<OwnProps> = ({
|
||||
|
||||
const { isMobile } = useAppLayout();
|
||||
|
||||
const copyLink = useLastCallback((link: string) => {
|
||||
const isOnlyCopy = !onRevoke;
|
||||
|
||||
const copyLink = useLastCallback(() => {
|
||||
copyTextToClipboard(link);
|
||||
showNotification({
|
||||
message: lang('LinkCopied'),
|
||||
});
|
||||
});
|
||||
|
||||
const handleCopyPrimaryClicked = useLastCallback(() => {
|
||||
const handleCopyClick = useLastCallback(() => {
|
||||
if (isDisabled) return;
|
||||
copyLink(inviteLink);
|
||||
copyLink();
|
||||
});
|
||||
|
||||
const handleShare = useLastCallback(() => {
|
||||
openChatWithDraft({ text: inviteLink });
|
||||
openChatWithDraft({ text: link });
|
||||
});
|
||||
|
||||
const PrimaryLinkMenuButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => {
|
||||
@ -75,28 +80,43 @@ const InviteLink: FC<OwnProps> = ({
|
||||
<div className={styles.primaryLink}>
|
||||
<input
|
||||
className={buildClassName('form-control', styles.input)}
|
||||
value={inviteLink}
|
||||
value={link}
|
||||
readOnly
|
||||
onClick={handleCopyPrimaryClicked}
|
||||
onClick={handleCopyClick}
|
||||
/>
|
||||
<DropdownMenu
|
||||
className={styles.moreMenu}
|
||||
trigger={PrimaryLinkMenuButton}
|
||||
positionX="right"
|
||||
>
|
||||
<MenuItem icon="copy" onClick={handleCopyPrimaryClicked} disabled={isDisabled}>{lang('Copy')}</MenuItem>
|
||||
{onRevoke && (
|
||||
<MenuItem icon="delete" onClick={onRevoke} destructive>{lang('RevokeButton')}</MenuItem>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
{isOnlyCopy ? (
|
||||
<Button
|
||||
color="translucent"
|
||||
className={styles.copy}
|
||||
size="smaller"
|
||||
round
|
||||
disabled={isDisabled}
|
||||
onClick={handleCopyClick}
|
||||
>
|
||||
<Icon name="copy" />
|
||||
</Button>
|
||||
) : (
|
||||
<DropdownMenu
|
||||
className={styles.moreMenu}
|
||||
trigger={PrimaryLinkMenuButton}
|
||||
positionX="right"
|
||||
>
|
||||
<MenuItem icon="copy" onClick={handleCopyClick} disabled={isDisabled}>{lang('Copy')}</MenuItem>
|
||||
{onRevoke && (
|
||||
<MenuItem icon="delete" onClick={onRevoke} destructive>{lang('RevokeButton')}</MenuItem>
|
||||
)}
|
||||
</DropdownMenu>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
size="smaller"
|
||||
disabled={isDisabled}
|
||||
onClick={handleShare}
|
||||
>
|
||||
{lang('FolderLinkScreen.LinkActionShare')}
|
||||
</Button>
|
||||
{withShare && (
|
||||
<Button
|
||||
size="smaller"
|
||||
disabled={isDisabled}
|
||||
onClick={handleShare}
|
||||
>
|
||||
{lang('FolderLinkScreen.LinkActionShare')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@ -51,6 +51,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.fluid {
|
||||
max-width: unset;
|
||||
}
|
||||
|
||||
.SearchInput & {
|
||||
flex: 1 0 auto;
|
||||
position: relative;
|
||||
|
||||
@ -25,6 +25,7 @@ type OwnProps = {
|
||||
forceShowSelf?: boolean;
|
||||
clickArg?: any;
|
||||
className?: string;
|
||||
fluid?: boolean;
|
||||
onClick: (arg: any) => void;
|
||||
};
|
||||
|
||||
@ -43,6 +44,7 @@ const PickerSelectedItem: FC<OwnProps & StateProps> = ({
|
||||
chat,
|
||||
user,
|
||||
className,
|
||||
fluid,
|
||||
isSavedMessages,
|
||||
onClick,
|
||||
}) => {
|
||||
@ -81,6 +83,7 @@ const PickerSelectedItem: FC<OwnProps & StateProps> = ({
|
||||
chat?.isForum && 'forum-avatar',
|
||||
isMinimized && 'minimized',
|
||||
canClose && 'closeable',
|
||||
fluid && 'fluid',
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@ -78,7 +78,7 @@ const SettingsExperimental: FC<OwnProps & StateProps> = ({
|
||||
<div className="settings-item">
|
||||
<ListItem
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => requestConfetti()}
|
||||
onClick={() => requestConfetti({})}
|
||||
icon="animations"
|
||||
>
|
||||
<div className="title">Launch some confetti!</div>
|
||||
|
||||
@ -21,7 +21,7 @@ import useLang from '../../../../hooks/useLang';
|
||||
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||
|
||||
import AnimatedIcon from '../../../common/AnimatedIcon';
|
||||
import InviteLink from '../../../common/InviteLink';
|
||||
import LinkField from '../../../common/LinkField';
|
||||
import Picker from '../../../common/Picker';
|
||||
import FloatingActionButton from '../../../ui/FloatingActionButton';
|
||||
import Spinner from '../../../ui/Spinner';
|
||||
@ -159,9 +159,10 @@ const SettingsShareChatlist: FC<OwnProps & StateProps> = ({
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<InviteLink
|
||||
<LinkField
|
||||
className="settings-item"
|
||||
inviteLink={!url ? lang('Loading') : url}
|
||||
link={!url ? lang('Loading') : url}
|
||||
withShare
|
||||
onRevoke={handleRevoke}
|
||||
isDisabled={!chatsCount || isTouched}
|
||||
/>
|
||||
|
||||
@ -78,6 +78,7 @@ import MiddleColumn from '../middle/MiddleColumn';
|
||||
import AttachBotInstallModal from '../modals/attachBotInstall/AttachBotInstallModal.async';
|
||||
import BoostModal from '../modals/boost/BoostModal.async';
|
||||
import ChatlistModal from '../modals/chatlist/ChatlistModal.async';
|
||||
import GiftCodeModal from '../modals/giftcode/GiftCodeModal.async';
|
||||
import MapModal from '../modals/map/MapModal.async';
|
||||
import UrlAuthModal from '../modals/urlAuth/UrlAuthModal.async';
|
||||
import WebAppModal from '../modals/webApp/WebAppModal.async';
|
||||
@ -110,6 +111,7 @@ export interface OwnProps {
|
||||
type StateProps = {
|
||||
isMasterTab?: boolean;
|
||||
chat?: ApiChat;
|
||||
currentUserId?: string;
|
||||
isLeftColumnOpen: boolean;
|
||||
isMiddleColumnOpen: boolean;
|
||||
isRightColumnOpen: boolean;
|
||||
@ -155,6 +157,7 @@ type StateProps = {
|
||||
isCurrentUserPremium?: boolean;
|
||||
chatlistModal?: TabState['chatlistModal'];
|
||||
boostModal?: TabState['boostModal'];
|
||||
giftCodeModal?: TabState['giftCodeModal'];
|
||||
noRightColumnAnimation?: boolean;
|
||||
withInterfaceAnimations?: boolean;
|
||||
isSynced?: boolean;
|
||||
@ -174,6 +177,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
isMediaViewerOpen,
|
||||
isStoryViewerOpen,
|
||||
isForwardModalOpen,
|
||||
currentUserId,
|
||||
hasNotifications,
|
||||
hasDialogs,
|
||||
audioMessage,
|
||||
@ -214,6 +218,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
deleteFolderDialog,
|
||||
isMasterTab,
|
||||
chatlistModal,
|
||||
giftCodeModal,
|
||||
boostModal,
|
||||
noRightColumnAnimation,
|
||||
isSynced,
|
||||
@ -558,6 +563,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
isByPhoneNumber={newContactByPhoneNumber}
|
||||
/>
|
||||
<BoostModal info={boostModal} />
|
||||
<GiftCodeModal modal={giftCodeModal} currentUserId={currentUserId} />
|
||||
<ChatlistModal info={chatlistModal} />
|
||||
<GameModal openedGame={openedGame} gameTitle={gameTitle} />
|
||||
<WebAppModal webApp={webApp} />
|
||||
@ -592,6 +598,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
language, wasTimeFormatSetManually,
|
||||
},
|
||||
},
|
||||
currentUserId,
|
||||
} = global;
|
||||
|
||||
const {
|
||||
@ -621,6 +628,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
deleteFolderDialogModal,
|
||||
chatlistModal,
|
||||
boostModal,
|
||||
giftCodeModal,
|
||||
} = selectTabState(global);
|
||||
|
||||
const { chatId: audioChatId, messageId: audioMessageId } = audioPlayer;
|
||||
@ -637,6 +645,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
const deleteFolderDialog = deleteFolderDialogModal ? selectChatFolder(global, deleteFolderDialogModal) : undefined;
|
||||
|
||||
return {
|
||||
currentUserId,
|
||||
isLeftColumnOpen: isLeftColumnShown,
|
||||
isMiddleColumnOpen: Boolean(chatId),
|
||||
isRightColumnOpen: selectIsRightColumnShown(global, isMobile),
|
||||
@ -684,6 +693,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
requestedDraft,
|
||||
chatlistModal,
|
||||
boostModal,
|
||||
giftCodeModal,
|
||||
noRightColumnAnimation,
|
||||
isSynced: global.isSynced,
|
||||
};
|
||||
|
||||
@ -12,12 +12,13 @@ import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
import type { FocusDirection } from '../../types';
|
||||
import type { PinnedIntersectionChangedCallback } from './hooks/usePinnedMessage';
|
||||
|
||||
import { getMessageHtmlId, isChatChannel } from '../../global/helpers';
|
||||
import { getChatTitle, getMessageHtmlId, isChatChannel } from '../../global/helpers';
|
||||
import { getMessageReplyInfo } from '../../global/helpers/replies';
|
||||
import {
|
||||
selectCanPlayAnimatedEmojis,
|
||||
selectChat,
|
||||
selectChatMessage,
|
||||
selectGiftStickerForDuration,
|
||||
selectIsMessageFocused,
|
||||
selectTabState,
|
||||
selectTopicFromMessage,
|
||||
@ -25,6 +26,7 @@ import {
|
||||
} from '../../global/selectors';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { renderActionMessageText } from '../common/helpers/renderActionMessageText';
|
||||
import renderText from '../common/helpers/renderText';
|
||||
import { preventMessageInputBlur } from './helpers/preventMessageInputBlur';
|
||||
|
||||
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
||||
@ -61,6 +63,7 @@ type StateProps = {
|
||||
targetUserIds?: string[];
|
||||
targetMessage?: ApiMessage;
|
||||
targetChatId?: string;
|
||||
targetChat?: ApiChat;
|
||||
isFocused: boolean;
|
||||
topic?: ApiTopic;
|
||||
focusDirection?: FocusDirection;
|
||||
@ -82,6 +85,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||
targetUserIds,
|
||||
targetMessage,
|
||||
targetChatId,
|
||||
targetChat,
|
||||
isFocused,
|
||||
focusDirection,
|
||||
noFocusHighlight,
|
||||
@ -95,7 +99,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||
observeIntersectionForPlaying,
|
||||
onPinnedIntersectionChange,
|
||||
}) => {
|
||||
const { openPremiumModal, requestConfetti } = getActions();
|
||||
const { openPremiumModal, requestConfetti, checkGiftCode } = getActions();
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
@ -121,6 +125,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||
const noAppearanceAnimation = appearanceOrder <= 0;
|
||||
const [isShown, markShown] = useFlag(noAppearanceAnimation);
|
||||
const isGift = Boolean(message.content.action?.text.startsWith('ActionGift'));
|
||||
const isGiftCode = Boolean(message.content.action?.text.startsWith('BoostingReceivedGift'));
|
||||
const isSuggestedAvatar = message.content.action?.type === 'suggestProfilePhoto' && message.content.action!.photo;
|
||||
|
||||
useEffect(() => {
|
||||
@ -141,7 +146,7 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||
useEffect(() => {
|
||||
if (isVisible && shouldShowConfettiRef.current) {
|
||||
shouldShowConfettiRef.current = false;
|
||||
requestConfetti();
|
||||
requestConfetti({});
|
||||
}
|
||||
}, [isVisible, requestConfetti]);
|
||||
|
||||
@ -195,6 +200,12 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||
});
|
||||
};
|
||||
|
||||
const handleGiftCodeClick = () => {
|
||||
const slug = message.content.action?.slug;
|
||||
if (!slug) return;
|
||||
checkGiftCode({ slug });
|
||||
};
|
||||
|
||||
// TODO Refactoring for action rendering
|
||||
const shouldSkipRender = isInsideTopic && message.content.action?.text === 'TopicWasCreatedAction';
|
||||
if (shouldSkipRender) {
|
||||
@ -223,13 +234,47 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
function renderGiftCode() {
|
||||
const isFromGiveaway = message.content.action?.isGiveaway;
|
||||
return (
|
||||
<span
|
||||
className="action-message-gift action-message-gift-code"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
onClick={handleGiftCodeClick}
|
||||
>
|
||||
<AnimatedIconFromSticker
|
||||
key={message.id}
|
||||
sticker={premiumGiftSticker}
|
||||
play={canPlayAnimatedEmojis}
|
||||
noLoop
|
||||
nonInteractive
|
||||
/>
|
||||
<strong>{lang('BoostingUnclaimedPrize')}</strong>
|
||||
<span className="action-message-subtitle">
|
||||
{renderText(lang(isFromGiveaway ? 'BoostingReceivedGiftFrom' : 'BoostingYouHaveUnclaimedPrize',
|
||||
getChatTitle(lang, targetChat!)),
|
||||
['simple_markdown'])}
|
||||
</span>
|
||||
<span className="action-message-subtitle">
|
||||
{renderText(lang(
|
||||
'BoostingUnclaimedPrizeDuration',
|
||||
lang('Months', message.content.action?.months, 'i'),
|
||||
), ['simple_markdown'])}
|
||||
</span>
|
||||
|
||||
<span className="action-message-button">{lang('BoostingReceivedGiftOpenBtn')}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const className = buildClassName(
|
||||
'ActionMessage message-list-item',
|
||||
isFocused && !noFocusHighlight && 'focused',
|
||||
(isGift || isSuggestedAvatar) && 'centered-action',
|
||||
isContextMenuShown && 'has-menu-open',
|
||||
isLastInList && 'last-in-list',
|
||||
!isGift && !isSuggestedAvatar && 'in-one-row',
|
||||
!isGift && !isSuggestedAvatar && !isGiftCode && 'in-one-row',
|
||||
transitionClassNames,
|
||||
);
|
||||
|
||||
@ -243,8 +288,9 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||
onMouseDown={handleMouseDown}
|
||||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
{!isSuggestedAvatar && <span className="action-message-content">{renderContent()}</span>}
|
||||
{!isSuggestedAvatar && !isGiftCode && <span className="action-message-content">{renderContent()}</span>}
|
||||
{isGift && renderGift()}
|
||||
{isGiftCode && renderGiftCode()}
|
||||
{isSuggestedAvatar && (
|
||||
<ActionMessageSuggestedAvatar
|
||||
message={message}
|
||||
@ -288,12 +334,17 @@ export default memo(withGlobal<OwnProps>(
|
||||
const isChat = chat && (isChatChannel(chat) || userId === chatId);
|
||||
const senderUser = !isChat && userId ? selectUser(global, userId) : undefined;
|
||||
const senderChat = isChat ? chat : undefined;
|
||||
const premiumGiftSticker = global.premiumGifts?.stickers?.[0];
|
||||
|
||||
const targetChat = targetChatId ? selectChat(global, targetChatId) : undefined;
|
||||
|
||||
const giftDuration = content.action?.months;
|
||||
const premiumGiftSticker = selectGiftStickerForDuration(global, giftDuration);
|
||||
const topic = selectTopicFromMessage(global, message);
|
||||
|
||||
return {
|
||||
senderUser,
|
||||
senderChat,
|
||||
targetChat,
|
||||
targetChatId,
|
||||
targetUserIds,
|
||||
targetMessage,
|
||||
|
||||
@ -278,6 +278,17 @@
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.action-message-gift-code {
|
||||
width: 20rem;
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.action-message-subtitle {
|
||||
margin-top: 1rem;
|
||||
font-weight: normal;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.action-message-suggested-avatar {
|
||||
max-width: 16rem;
|
||||
display: flex !important;
|
||||
|
||||
@ -119,7 +119,8 @@
|
||||
.audio &,
|
||||
.voice &,
|
||||
.poll &,
|
||||
.text & {
|
||||
.text &,
|
||||
.giveaway & {
|
||||
border-top: 1px solid var(--color-borders);
|
||||
}
|
||||
|
||||
@ -136,6 +137,7 @@
|
||||
.message-content.audio &,
|
||||
.message-content.voice &,
|
||||
.message-content.poll &,
|
||||
.message-content.giveaway &,
|
||||
.message-content.has-solid-background.text &,
|
||||
.message-content.has-solid-background.is-forwarded & {
|
||||
width: calc(100% + 1rem);
|
||||
|
||||
64
src/components/middle/message/Giveaway.module.scss
Normal file
64
src/components/middle/message/Giveaway.module.scss
Normal file
@ -0,0 +1,64 @@
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.title {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.gift {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.count {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
|
||||
background-color: var(--color-primary);
|
||||
color: white;
|
||||
border-radius: 1rem;
|
||||
padding: 0.0625rem 0.5rem;
|
||||
border: 1px solid var(--background-color);
|
||||
line-height: 1.25;
|
||||
|
||||
:global(.theme-dark .own) & {
|
||||
background-color: var(--color-text);
|
||||
color: var(--background-color);
|
||||
}
|
||||
}
|
||||
|
||||
.section {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.description {
|
||||
line-height: 1.25;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.channels {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
margin-block: 0.25rem;
|
||||
}
|
||||
|
||||
.channel {
|
||||
background-color: var(--accent-background-color);
|
||||
color: var(--accent-color);
|
||||
margin: unset;
|
||||
|
||||
&:hover, &:active, &:focus {
|
||||
background-color: var(--accent-background-active-color);
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
258
src/components/middle/message/Giveaway.tsx
Normal file
258
src/components/middle/message/Giveaway.tsx
Normal file
@ -0,0 +1,258 @@
|
||||
import React, {
|
||||
memo, useMemo, useRef, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, getGlobal, withGlobal } from '../../../global';
|
||||
|
||||
import type {
|
||||
ApiChat, ApiGiveawayInfo, ApiMessage, ApiPeer, ApiSticker,
|
||||
} from '../../../api/types';
|
||||
|
||||
import { getChatTitle, getUserFullName, isApiPeerChat } from '../../../global/helpers';
|
||||
import {
|
||||
selectCanPlayAnimatedEmojis,
|
||||
selectChat,
|
||||
selectForwardedSender,
|
||||
selectGiftStickerForDuration,
|
||||
} from '../../../global/selectors';
|
||||
import { formatDateAtTime, formatDateTimeToString } from '../../../util/dateFormat';
|
||||
import { isoToEmoji } from '../../../util/emoji';
|
||||
import { getServerTime } from '../../../util/serverTime';
|
||||
import { callApi } from '../../../api/gramjs';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import AnimatedIconFromSticker from '../../common/AnimatedIconFromSticker';
|
||||
import PickerSelectedItem from '../../common/PickerSelectedItem';
|
||||
import Button from '../../ui/Button';
|
||||
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||
|
||||
import styles from './Giveaway.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
message: ApiMessage;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
chat: ApiChat;
|
||||
sender?: ApiPeer;
|
||||
giftSticker?: ApiSticker;
|
||||
canPlayAnimatedEmojis?: boolean;
|
||||
};
|
||||
|
||||
const NBSP = '\u00A0';
|
||||
const GIFT_STICKER_SIZE = 175;
|
||||
|
||||
const Giveaway = ({
|
||||
chat,
|
||||
sender,
|
||||
message,
|
||||
canPlayAnimatedEmojis,
|
||||
giftSticker,
|
||||
}: OwnProps & StateProps) => {
|
||||
const { openChat } = getActions();
|
||||
|
||||
const isLoadingInfo = useRef(false);
|
||||
const [giveawayInfo, setGiveawayInfo] = useState<ApiGiveawayInfo | undefined>();
|
||||
|
||||
const lang = useLang();
|
||||
const {
|
||||
months, quantity, channelIds, untilDate, countries,
|
||||
} = message.content.giveaway!;
|
||||
|
||||
const hasEnded = getServerTime() > untilDate;
|
||||
|
||||
const countryList = useMemo(() => {
|
||||
const translatedNames = new Intl.DisplayNames([lang.code!, 'en'].filter(Boolean), { type: 'region' });
|
||||
return countries?.map((countryCode) => (
|
||||
`${isoToEmoji(countryCode)}${NBSP}${translatedNames.of(countryCode)}`
|
||||
)).join(', ');
|
||||
}, [countries, lang.code]);
|
||||
|
||||
const handleChannelClick = useLastCallback((channelId: string) => {
|
||||
openChat({ id: channelId });
|
||||
});
|
||||
|
||||
const handleShowInfoClick = useLastCallback(async () => {
|
||||
if (isLoadingInfo.current) return;
|
||||
|
||||
isLoadingInfo.current = true;
|
||||
const result = await callApi('fetchGiveawayInfo', {
|
||||
peer: chat,
|
||||
messageId: message.id,
|
||||
});
|
||||
setGiveawayInfo(result);
|
||||
isLoadingInfo.current = false;
|
||||
});
|
||||
|
||||
const handleCloseInfo = useLastCallback(() => {
|
||||
setGiveawayInfo(undefined);
|
||||
});
|
||||
|
||||
const giveawayInfoTitle = useMemo(() => {
|
||||
if (!giveawayInfo) return undefined;
|
||||
return lang(giveawayInfo.type === 'results' ? 'BoostingGiveawayEnd' : 'BoostingGiveAwayAbout');
|
||||
}, [giveawayInfo, lang]);
|
||||
|
||||
function renderGiveawayInfo() {
|
||||
if (!sender || !giveawayInfo) return undefined;
|
||||
const isResults = giveawayInfo.type === 'results';
|
||||
|
||||
const chatTitle = isApiPeerChat(sender) ? getChatTitle(lang, sender) : getUserFullName(sender);
|
||||
const duration = lang('Chat.Giveaway.Info.Months', months);
|
||||
const endDate = formatDateAtTime(lang, untilDate * 1000);
|
||||
const otherChannelsCount = channelIds.length ? channelIds.length - 1 : 0;
|
||||
const otherChannelsString = lang('Chat.Giveaway.Info.OtherChannels', otherChannelsCount);
|
||||
const isSeveral = otherChannelsCount > 0;
|
||||
|
||||
const firstKey = isResults ? 'BoostingGiveawayHowItWorksTextEnd' : 'BoostingGiveawayHowItWorksText';
|
||||
const firstParagraph = lang(firstKey, [chatTitle, quantity, duration], undefined, quantity);
|
||||
|
||||
let secondKey = '';
|
||||
if (isResults) {
|
||||
secondKey = isSeveral ? 'BoostingGiveawayHowItWorksSubTextSeveralEnd' : 'BoostingGiveawayHowItWorksSubTextEnd';
|
||||
} else {
|
||||
secondKey = isSeveral ? 'BoostingGiveawayHowItWorksSubTextSeveral' : 'BoostingGiveawayHowItWorksSubText';
|
||||
}
|
||||
let secondParagraph = lang(secondKey, [endDate, quantity, chatTitle, otherChannelsCount], undefined, quantity);
|
||||
if (isResults && giveawayInfo.activatedCount) {
|
||||
secondParagraph += ` ${lang('BoostingGiveawayUsedLinksPlural', giveawayInfo.activatedCount)}`;
|
||||
}
|
||||
|
||||
let lastParagraph = '';
|
||||
if (isResults && giveawayInfo.isRefunded) {
|
||||
lastParagraph = lang('BoostingGiveawayCanceledByPayment');
|
||||
} else if (isResults) {
|
||||
lastParagraph = lang(giveawayInfo.isWinner ? 'BoostingGiveawayYouWon' : 'BoostingGiveawayYouNotWon');
|
||||
} else if (giveawayInfo.disallowedCountry) {
|
||||
lastParagraph = lang('BoostingGiveawayNotEligibleCountry');
|
||||
} else if (giveawayInfo.adminDisallowedChatId) {
|
||||
// Since rerenders are not expected, we can use the global state directly
|
||||
const chatsById = getGlobal().chats.byId;
|
||||
const disallowedChat = chatsById[giveawayInfo.adminDisallowedChatId];
|
||||
const disallowedChatTitle = disallowedChat && getChatTitle(lang, disallowedChat);
|
||||
lastParagraph = lang('BoostingGiveawayNotEligibleAdmin', disallowedChatTitle);
|
||||
} else if (giveawayInfo.joinedTooEarlyDate) {
|
||||
const joinedTooEarlyDate = formatDateAtTime(lang, giveawayInfo.joinedTooEarlyDate * 1000);
|
||||
lastParagraph = lang('BoostingGiveawayNotEligible', joinedTooEarlyDate);
|
||||
} else if (giveawayInfo.isParticipating) {
|
||||
lastParagraph = isSeveral
|
||||
? lang('Chat.Giveaway.Info.ParticipatingMany', [chatTitle, otherChannelsCount])
|
||||
: lang('Chat.Giveaway.Info.Participating', chatTitle);
|
||||
} else {
|
||||
lastParagraph = isSeveral
|
||||
? lang('Chat.Giveaway.Info.NotQualifiedMany', [chatTitle, otherChannelsString, endDate])
|
||||
: lang('Chat.Giveaway.Info.NotQualified', [chatTitle, endDate]);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<p>
|
||||
{renderText(firstParagraph, ['simple_markdown'])}
|
||||
</p>
|
||||
<p>
|
||||
{renderText(secondParagraph, ['simple_markdown'])}
|
||||
</p>
|
||||
<p>
|
||||
{renderText(lastParagraph, ['simple_markdown'])}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
<div className={styles.gift}>
|
||||
<AnimatedIconFromSticker
|
||||
key={message.id}
|
||||
sticker={giftSticker}
|
||||
play={canPlayAnimatedEmojis && hasEnded}
|
||||
noLoop
|
||||
nonInteractive
|
||||
size={GIFT_STICKER_SIZE}
|
||||
/>
|
||||
<span className={styles.count}>
|
||||
{`x${quantity}`}
|
||||
</span>
|
||||
</div>
|
||||
<div className={styles.section}>
|
||||
<strong className={styles.title}>
|
||||
{renderText(lang('BoostingGiveawayPrizes'), ['simple_markdown'])}
|
||||
</strong>
|
||||
<p className={styles.description}>
|
||||
{renderText(lang('Chat.Giveaway.Info.Subscriptions', quantity), ['simple_markdown'])}
|
||||
<br />
|
||||
{renderText(lang(
|
||||
'ActionGiftPremiumSubtitle',
|
||||
lang('Chat.Giveaway.Info.Months', months),
|
||||
), ['simple_markdown'])}
|
||||
</p>
|
||||
</div>
|
||||
<div className={styles.section}>
|
||||
<strong className={styles.title}>
|
||||
{renderText(lang('BoostingGiveawayMsgParticipants'), ['simple_markdown'])}
|
||||
</strong>
|
||||
<p className={styles.description}>
|
||||
{renderText(lang('BoostingGiveawayMsgAllSubsPlural', channelIds.length), ['simple_markdown'])}
|
||||
</p>
|
||||
<div className={styles.channels}>
|
||||
{channelIds.map((channelId) => (
|
||||
<PickerSelectedItem
|
||||
peerId={channelId}
|
||||
forceShowSelf
|
||||
fluid
|
||||
className={styles.channel}
|
||||
clickArg={channelId}
|
||||
onClick={handleChannelClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
{countries?.length && (
|
||||
<span>{renderText(lang('Chat.Giveaway.Message.CountriesFrom', countryList))}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.section}>
|
||||
<strong className={styles.title}>
|
||||
{renderText(lang('BoostingWinnersDate'), ['simple_markdown'])}
|
||||
</strong>
|
||||
<p className={styles.description}>
|
||||
{formatDateTimeToString(untilDate * 1000, lang.code, true)}
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
className={styles.button}
|
||||
color="adaptive"
|
||||
size="smaller"
|
||||
onClick={handleShowInfoClick}
|
||||
>
|
||||
{lang('BoostingHowItWork')}
|
||||
</Button>
|
||||
<ConfirmDialog
|
||||
isOpen={Boolean(giveawayInfo)}
|
||||
isOnlyConfirm
|
||||
title={giveawayInfoTitle}
|
||||
confirmHandler={handleCloseInfo}
|
||||
onClose={handleCloseInfo}
|
||||
>
|
||||
{renderGiveawayInfo()}
|
||||
</ConfirmDialog>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { message }): StateProps => {
|
||||
const duration = message.content.giveaway!.months;
|
||||
const chat = selectChat(global, message.chatId)!;
|
||||
const sender = selectChat(global, message.content.giveaway?.channelIds[0]!)
|
||||
|| selectForwardedSender(global, message) || chat;
|
||||
|
||||
return {
|
||||
chat,
|
||||
sender,
|
||||
giftSticker: selectGiftStickerForDuration(global, duration),
|
||||
canPlayAnimatedEmojis: selectCanPlayAnimatedEmojis(global),
|
||||
};
|
||||
},
|
||||
)(Giveaway));
|
||||
@ -152,6 +152,7 @@ import CommentButton from './CommentButton';
|
||||
import Contact from './Contact';
|
||||
import ContextMenuContainer from './ContextMenuContainer.async';
|
||||
import Game from './Game';
|
||||
import Giveaway from './Giveaway';
|
||||
import InlineButtons from './InlineButtons';
|
||||
import Invoice from './Invoice';
|
||||
import InvoiceMediaPreview from './InvoiceMediaPreview';
|
||||
@ -630,7 +631,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
text, photo, video, audio,
|
||||
voice, document, sticker, contact,
|
||||
poll, webPage, invoice, location,
|
||||
action, game, storyData,
|
||||
action, game, storyData, giveaway,
|
||||
} = getMessageContent(message);
|
||||
|
||||
const { replyToMsgId, replyToPeerId, isQuote } = messageReplyInfo || {};
|
||||
@ -1137,6 +1138,9 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
{poll && (
|
||||
<Poll message={message} poll={poll} onSendVote={handleVoteSend} />
|
||||
)}
|
||||
{giveaway && (
|
||||
<Giveaway message={message} />
|
||||
)}
|
||||
{game && (
|
||||
<Game
|
||||
message={message}
|
||||
|
||||
@ -90,7 +90,7 @@ const Poll: FC<OwnProps & StateProps> = ({
|
||||
const chosen = poll.results.results?.find((result) => result.isChosen);
|
||||
if (isSubmitting && chosen) {
|
||||
if (chosen.isCorrect) {
|
||||
requestConfetti();
|
||||
requestConfetti({});
|
||||
}
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
|
||||
@ -825,7 +825,8 @@
|
||||
.forwarded-message {
|
||||
.message-content.contact &,
|
||||
.message-content.voice &,
|
||||
.message-content.poll & {
|
||||
.message-content.poll &,
|
||||
.message-content.giveaway & {
|
||||
// MessageOutgoingStatus's icon needs more space
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
@ -34,7 +34,7 @@ export function buildContentClassName(
|
||||
} = {},
|
||||
) {
|
||||
const {
|
||||
text, photo, video, audio, voice, document, poll, webPage, contact, location, invoice, storyData,
|
||||
text, photo, video, audio, voice, document, poll, webPage, contact, location, invoice, storyData, giveaway,
|
||||
} = getMessageContent(message);
|
||||
|
||||
const classNames = [MESSAGE_CONTENT_CLASS_NAME];
|
||||
@ -87,6 +87,8 @@ export function buildContentClassName(
|
||||
classNames.push('contact');
|
||||
} else if (poll) {
|
||||
classNames.push('poll');
|
||||
} else if (giveaway) {
|
||||
classNames.push('giveaway');
|
||||
} else if (webPage) {
|
||||
classNames.push('web-page');
|
||||
|
||||
|
||||
@ -172,7 +172,7 @@ const BoostModal = ({
|
||||
const handleApplyBoost = useLastCallback(() => {
|
||||
closeReplaceModal();
|
||||
applyBoost({ chatId: chat!.id, slots: [boost!.slot] });
|
||||
requestConfetti();
|
||||
requestConfetti({});
|
||||
});
|
||||
|
||||
const handleProceedPremium = useLastCallback(() => {
|
||||
|
||||
18
src/components/modals/giftcode/GiftCodeModal.async.tsx
Normal file
18
src/components/modals/giftcode/GiftCodeModal.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 './GiftCodeModal';
|
||||
|
||||
import { Bundles } from '../../../util/moduleLoader';
|
||||
|
||||
import useModuleLoader from '../../../hooks/useModuleLoader';
|
||||
|
||||
const GiftCodeModalAsync: FC<OwnProps> = (props) => {
|
||||
const { modal } = props;
|
||||
const GiftCodeModal = useModuleLoader(Bundles.Extra, 'GiftCodeModal', !modal);
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return GiftCodeModal ? <GiftCodeModal {...props} /> : undefined;
|
||||
};
|
||||
|
||||
export default GiftCodeModalAsync;
|
||||
38
src/components/modals/giftcode/GiftCodeModal.module.scss
Normal file
38
src/components/modals/giftcode/GiftCodeModal.module.scss
Normal file
@ -0,0 +1,38 @@
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
max-height: min(92vh, 40rem) !important;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
color: var(--color-primary);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.title {
|
||||
background-color: var(--color-background-secondary);
|
||||
}
|
||||
|
||||
.table td {
|
||||
border: 1px solid var(--color-borders);
|
||||
padding: 0.25rem 0.5rem;
|
||||
}
|
||||
|
||||
.chat-item {
|
||||
margin: 0;
|
||||
width: fit-content;
|
||||
background-color: var(--color-background);
|
||||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.logo {
|
||||
width: 6.25rem;
|
||||
height: 6.25rem;
|
||||
align-self: center;
|
||||
}
|
||||
|
||||
.centered {
|
||||
text-align: center !important;
|
||||
}
|
||||
160
src/components/modals/giftcode/GiftCodeModal.tsx
Normal file
160
src/components/modals/giftcode/GiftCodeModal.tsx
Normal file
@ -0,0 +1,160 @@
|
||||
import React, { memo } from '../../../lib/teact/teact';
|
||||
import { getActions } from '../../../global';
|
||||
|
||||
import type { TabState } from '../../../global/types';
|
||||
|
||||
import { TME_LINK_PREFIX } from '../../../config';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { formatDateTimeToString } from '../../../util/dateFormat';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import LinkField from '../../common/LinkField';
|
||||
import PickerSelectedItem from '../../common/PickerSelectedItem';
|
||||
import Button from '../../ui/Button';
|
||||
import Modal from '../../ui/Modal';
|
||||
|
||||
import styles from './GiftCodeModal.module.scss';
|
||||
|
||||
import PremiumLogo from '../../../assets/premium/PremiumLogo.svg';
|
||||
|
||||
export type OwnProps = {
|
||||
currentUserId?: string;
|
||||
modal: TabState['giftCodeModal'];
|
||||
};
|
||||
|
||||
const GIFTCODE_PATH = 'giftcode';
|
||||
|
||||
const GiftCodeModal = ({
|
||||
currentUserId,
|
||||
modal,
|
||||
}: OwnProps) => {
|
||||
const {
|
||||
closeGiftCodeModal, openChat, applyGiftCode, focusMessage,
|
||||
} = getActions();
|
||||
const lang = useLang();
|
||||
const isOpen = Boolean(modal);
|
||||
|
||||
const canUse = (!modal?.info.toId || modal?.info.toId === currentUserId) && !modal?.info.usedAt;
|
||||
|
||||
const handleOpenChat = useLastCallback((peerId: string) => {
|
||||
openChat({ id: peerId });
|
||||
closeGiftCodeModal();
|
||||
});
|
||||
|
||||
const handleOpenGiveaway = useLastCallback(() => {
|
||||
if (!modal || !modal.info.giveawayMessageId) return;
|
||||
focusMessage({
|
||||
chatId: modal.info.fromId!,
|
||||
messageId: modal.info.giveawayMessageId,
|
||||
});
|
||||
closeGiftCodeModal();
|
||||
});
|
||||
|
||||
const handleButtonClick = useLastCallback(() => {
|
||||
if (canUse) {
|
||||
applyGiftCode({ slug: modal!.slug });
|
||||
return;
|
||||
}
|
||||
closeGiftCodeModal();
|
||||
});
|
||||
|
||||
function renderContent() {
|
||||
if (!modal) return undefined;
|
||||
const { slug, info } = modal;
|
||||
|
||||
return (
|
||||
<>
|
||||
<img className={styles.logo} src={PremiumLogo} alt="" draggable={false} />
|
||||
<p className={styles.centered}>{renderText(lang('lng_gift_link_about'), ['simple_markdown'])}</p>
|
||||
<LinkField title="BoostingGiftLink" link={`${TME_LINK_PREFIX}/${GIFTCODE_PATH}/${slug}`} />
|
||||
<table className={styles.table}>
|
||||
<tr>
|
||||
<td className={styles.title}>{lang('BoostingFrom')}</td>
|
||||
<td>
|
||||
{info.fromId ? (
|
||||
<PickerSelectedItem
|
||||
peerId={info.fromId}
|
||||
className={styles.chatItem}
|
||||
forceShowSelf
|
||||
fluid
|
||||
clickArg={info.fromId}
|
||||
onClick={handleOpenChat}
|
||||
/>
|
||||
) : lang('BoostingNoRecipient')}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className={styles.title}>
|
||||
{lang('BoostingTo')}
|
||||
</td>
|
||||
<td>
|
||||
{info.toId ? (
|
||||
<PickerSelectedItem
|
||||
peerId={info.toId}
|
||||
className={styles.chatItem}
|
||||
forceShowSelf
|
||||
fluid
|
||||
clickArg={info.toId}
|
||||
onClick={handleOpenChat}
|
||||
/>
|
||||
) : lang('BoostingNoRecipient')}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className={styles.title}>
|
||||
{lang('BoostingGift')}
|
||||
</td>
|
||||
<td>
|
||||
{lang('BoostingTelegramPremiumFor', lang('Months', info.months, 'i'))}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className={styles.title}>
|
||||
{lang('BoostingReason')}
|
||||
</td>
|
||||
<td className={buildClassName(info.giveawayMessageId && styles.clickable)} onClick={handleOpenGiveaway}>
|
||||
{info.isFromGiveaway && !info.toId ? lang('BoostingIncompleteGiveaway')
|
||||
: lang(info.isFromGiveaway ? 'BoostingGiveaway' : 'BoostingYouWereSelected')}
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td className={styles.title}>
|
||||
{lang('BoostingDate')}
|
||||
</td>
|
||||
<td>
|
||||
{formatDateTimeToString(info.date * 1000, lang.code, true)}
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<span className={styles.centered}>
|
||||
{renderText(
|
||||
info.usedAt ? lang('BoostingUsedLinkDate', formatDateTimeToString(info.usedAt * 1000, lang.code, true))
|
||||
: lang('BoostingSendLinkToAnyone'),
|
||||
['simple_markdown'],
|
||||
)}
|
||||
</span>
|
||||
<Button onClick={handleButtonClick}>
|
||||
{canUse ? lang('BoostingUseLink') : lang('Close')}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
hasCloseButton
|
||||
isSlim
|
||||
title={lang('lng_gift_link_title')}
|
||||
contentClassName={styles.content}
|
||||
onClose={closeGiftCodeModal}
|
||||
>
|
||||
{renderContent()}
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(GiftCodeModal);
|
||||
@ -22,7 +22,7 @@ import useInterval from '../../../hooks/useInterval';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
|
||||
import AnimatedIcon from '../../common/AnimatedIcon';
|
||||
import InviteLink from '../../common/InviteLink';
|
||||
import LinkField from '../../common/LinkField';
|
||||
import NothingFound from '../../common/NothingFound';
|
||||
import Button from '../../ui/Button';
|
||||
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||
@ -283,9 +283,10 @@ const ManageInvites: FC<OwnProps & StateProps> = ({
|
||||
<p className="text-muted">{isChannel ? lang('PrimaryLinkHelpChannel') : lang('PrimaryLinkHelp')}</p>
|
||||
</div>
|
||||
{primaryInviteLink && (
|
||||
<InviteLink
|
||||
<LinkField
|
||||
className="section"
|
||||
inviteLink={primaryInviteLink}
|
||||
link={primaryInviteLink}
|
||||
withShare
|
||||
onRevoke={!chat?.usernames ? handlePrimaryRevoke : undefined}
|
||||
title={chat?.usernames ? lang('PublicLink') : lang('lng_create_permanent_link_title')}
|
||||
/>
|
||||
|
||||
@ -13,7 +13,7 @@ import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import Icon from '../../common/Icon';
|
||||
import InviteLink from '../../common/InviteLink';
|
||||
import LinkField from '../../common/LinkField';
|
||||
import PremiumProgress from '../../common/PremiumProgress';
|
||||
import PrivateChatInfo from '../../common/PrivateChatInfo';
|
||||
import ListItem from '../../ui/ListItem';
|
||||
@ -135,7 +135,7 @@ const BoostStatistics = ({
|
||||
</ListItem>
|
||||
)}
|
||||
</div>
|
||||
<InviteLink className={styles.section} inviteLink={status!.boostUrl} title={lang('LinkForBoosting')} />
|
||||
<LinkField className={styles.section} link={status!.boostUrl} withShare title={lang('LinkForBoosting')} />
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -237,6 +237,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.adaptive {
|
||||
--ripple-color: var(--accent-background-active-color);
|
||||
background-color: var(--accent-background-color);
|
||||
color: var(--accent-color);
|
||||
|
||||
@include active-styles() {
|
||||
background-color: var(--accent-background-active-color);
|
||||
}
|
||||
|
||||
@include no-ripple-styles() {
|
||||
background-color: var(--accent-background-active-color);
|
||||
}
|
||||
}
|
||||
|
||||
&.dark {
|
||||
background-color: rgba(0, 0, 0, 0.75);
|
||||
color: white;
|
||||
|
||||
@ -20,7 +20,7 @@ export type OwnProps = {
|
||||
size?: 'default' | 'smaller' | 'tiny';
|
||||
color?: (
|
||||
'primary' | 'secondary' | 'gray' | 'danger' | 'translucent' | 'translucent-white' | 'translucent-black'
|
||||
| 'translucent-bordered' | 'dark' | 'green'
|
||||
| 'translucent-bordered' | 'dark' | 'green' | 'adaptive'
|
||||
);
|
||||
backgroundImage?: string;
|
||||
id?: string;
|
||||
|
||||
@ -52,7 +52,7 @@ export const CUSTOM_EMOJI_PREVIEW_CACHE_DISABLED = false;
|
||||
export const CUSTOM_EMOJI_PREVIEW_CACHE_NAME = 'tt-custom-emoji-preview';
|
||||
export const MEDIA_CACHE_MAX_BYTES = 512 * 1024; // 512 KB
|
||||
export const CUSTOM_BG_CACHE_NAME = 'tt-custom-bg';
|
||||
export const LANG_CACHE_NAME = 'tt-lang-packs-v25';
|
||||
export const LANG_CACHE_NAME = 'tt-lang-packs-v26';
|
||||
export const ASSET_CACHE_NAME = 'tt-assets';
|
||||
export const AUTODOWNLOAD_FILESIZE_MB_LIMITS = [1, 5, 10, 50, 100, 500];
|
||||
export const DATA_BROADCAST_CHANNEL_NAME = 'tt-global';
|
||||
|
||||
@ -975,6 +975,7 @@ addActionHandler('openTelegramLink', (global, actions, payload): ActionReturnTyp
|
||||
openChatByUsername: openChatByUsernameAction,
|
||||
openStoryViewerByUsername,
|
||||
processBoostParameters,
|
||||
checkGiftCode,
|
||||
} = actions;
|
||||
|
||||
if (url.match(RE_TG_LINK)) {
|
||||
@ -1057,6 +1058,12 @@ addActionHandler('openTelegramLink', (global, actions, payload): ActionReturnTyp
|
||||
return;
|
||||
}
|
||||
|
||||
if (part1 === 'giftcode') {
|
||||
const slug = part2;
|
||||
checkGiftCode({ slug, tabId });
|
||||
return;
|
||||
}
|
||||
|
||||
const chatOrChannelPostId = part2 || undefined;
|
||||
const messageId = part3 ? Number(part3) : undefined;
|
||||
const commentId = params.comment ? Number(params.comment) : undefined;
|
||||
|
||||
@ -5,12 +5,14 @@ import { PaymentStep } from '../../../types';
|
||||
|
||||
import { DEBUG_PAYMENT_SMART_GLOCAL } from '../../../config';
|
||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||
import { buildCollectionByKey } from '../../../util/iteratees';
|
||||
import { buildCollectionByKey, unique } from '../../../util/iteratees';
|
||||
import * as langProvider from '../../../util/langProvider';
|
||||
import { buildQueryString } from '../../../util/requestQuery';
|
||||
import { callApi } from '../../../api/gramjs';
|
||||
import { getStripeError } from '../../helpers';
|
||||
import { getStripeError, isChatChannel } from '../../helpers';
|
||||
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
||||
import {
|
||||
addChats,
|
||||
addUsers, closeInvoice,
|
||||
setInvoiceInfo, setPaymentForm,
|
||||
setPaymentStep,
|
||||
@ -459,3 +461,213 @@ async function validateRequestedInfo<T extends GlobalState>(
|
||||
}
|
||||
setGlobal(global);
|
||||
}
|
||||
|
||||
addActionHandler('openBoostModal', async (global, actions, payload): Promise<void> => {
|
||||
const { chatId, tabId = getCurrentTabId() } = payload;
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat || !isChatChannel(chat)) return;
|
||||
|
||||
global = updateTabState(global, {
|
||||
boostModal: {
|
||||
chatId,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
|
||||
const result = await callApi('fetchBoostsStatus', {
|
||||
chat,
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
actions.closeBoostModal({ tabId });
|
||||
return;
|
||||
}
|
||||
|
||||
global = getGlobal();
|
||||
global = updateTabState(global, {
|
||||
boostModal: {
|
||||
chatId,
|
||||
boostStatus: result,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
|
||||
const myBoosts = await callApi('fetchMyBoosts');
|
||||
|
||||
if (!myBoosts) return;
|
||||
|
||||
global = getGlobal();
|
||||
const tabState = selectTabState(global, tabId);
|
||||
if (!tabState.boostModal) return;
|
||||
|
||||
global = addChats(global, buildCollectionByKey(myBoosts.chats, 'id'));
|
||||
global = addUsers(global, buildCollectionByKey(myBoosts.users, 'id'));
|
||||
global = updateTabState(global, {
|
||||
boostModal: {
|
||||
...tabState.boostModal,
|
||||
myBoosts: myBoosts.boosts,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('openBoostStatistics', async (global, actions, payload): Promise<void> => {
|
||||
const { chatId, tabId = getCurrentTabId() } = payload;
|
||||
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) return;
|
||||
|
||||
global = updateTabState(global, {
|
||||
boostStatistics: {
|
||||
chatId,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
|
||||
const [boostsListResult, boostStatusResult] = await Promise.all([
|
||||
callApi('fetchBoostsList', { chat }),
|
||||
callApi('fetchBoostsStatus', { chat }),
|
||||
]);
|
||||
|
||||
global = getGlobal();
|
||||
if (!boostsListResult || !boostStatusResult) {
|
||||
global = updateTabState(global, {
|
||||
boostStatistics: undefined,
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
return;
|
||||
}
|
||||
|
||||
global = addUsers(global, buildCollectionByKey(boostsListResult.users, 'id'));
|
||||
global = updateTabState(global, {
|
||||
boostStatistics: {
|
||||
chatId,
|
||||
boostStatus: boostStatusResult,
|
||||
boosters: boostsListResult.boosters,
|
||||
boosterIds: boostsListResult.boosterIds,
|
||||
count: boostsListResult.count,
|
||||
nextOffset: boostsListResult.nextOffset,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('loadMoreBoosters', async (global, actions, payload): Promise<void> => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
let tabState = selectTabState(global, tabId);
|
||||
if (!tabState.boostStatistics) return;
|
||||
|
||||
const chat = selectChat(global, tabState.boostStatistics.chatId);
|
||||
if (!chat) return;
|
||||
|
||||
global = updateTabState(global, {
|
||||
boostStatistics: {
|
||||
...tabState.boostStatistics,
|
||||
isLoadingBoosters: true,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
|
||||
const result = await callApi('fetchBoostsList', {
|
||||
chat,
|
||||
offset: tabState.boostStatistics.nextOffset,
|
||||
});
|
||||
if (!result) return;
|
||||
|
||||
global = getGlobal();
|
||||
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
|
||||
|
||||
tabState = selectTabState(global, tabId);
|
||||
if (!tabState.boostStatistics) return;
|
||||
|
||||
global = updateTabState(global, {
|
||||
boostStatistics: {
|
||||
...tabState.boostStatistics,
|
||||
boosters: {
|
||||
...tabState.boostStatistics.boosters,
|
||||
...result.boosters,
|
||||
},
|
||||
boosterIds: unique([...tabState.boostStatistics.boosterIds || [], ...result.boosterIds]),
|
||||
count: result.count,
|
||||
nextOffset: result.nextOffset,
|
||||
isLoadingBoosters: false,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('applyBoost', async (global, actions, payload): Promise<void> => {
|
||||
const { chatId, slots, tabId = getCurrentTabId() } = payload;
|
||||
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) return;
|
||||
|
||||
const result = await callApi('applyBoost', {
|
||||
slots,
|
||||
chat,
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newStatusResult = await callApi('fetchBoostsStatus', {
|
||||
chat,
|
||||
});
|
||||
|
||||
if (!newStatusResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
global = getGlobal();
|
||||
const tabState = selectTabState(global, tabId);
|
||||
if (!tabState.boostModal?.boostStatus) return;
|
||||
global = updateTabState(global, {
|
||||
boostModal: {
|
||||
...tabState.boostModal,
|
||||
boostStatus: newStatusResult,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('checkGiftCode', async (global, actions, payload): Promise<void> => {
|
||||
const { slug, tabId = getCurrentTabId() } = payload;
|
||||
|
||||
const result = await callApi('checkGiftCode', {
|
||||
slug,
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
actions.showNotification({
|
||||
message: langProvider.translate('lng_gift_link_expired'),
|
||||
tabId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
global = getGlobal();
|
||||
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
|
||||
global = addChats(global, buildCollectionByKey(result.chats, 'id'));
|
||||
global = updateTabState(global, {
|
||||
giftCodeModal: {
|
||||
slug,
|
||||
info: result.code,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('applyGiftCode', async (global, actions, payload): Promise<void> => {
|
||||
const { slug, tabId = getCurrentTabId() } = payload;
|
||||
|
||||
const result = await callApi('applyGiftCode', {
|
||||
slug,
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
actions.requestConfetti({ tabId });
|
||||
actions.closeGiftCodeModal({ tabId });
|
||||
});
|
||||
|
||||
@ -2,11 +2,11 @@ import type { ActionReturnType } from '../../types';
|
||||
|
||||
import { DEBUG, PREVIEW_AVATAR_COUNT } from '../../../config';
|
||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||
import { buildCollectionByKey, unique } from '../../../util/iteratees';
|
||||
import { buildCollectionByKey } from '../../../util/iteratees';
|
||||
import { translate } from '../../../util/langProvider';
|
||||
import { getServerTime } from '../../../util/serverTime';
|
||||
import { callApi } from '../../../api/gramjs';
|
||||
import { buildApiInputPrivacyRules, isChatChannel } from '../../helpers';
|
||||
import { buildApiInputPrivacyRules } from '../../helpers';
|
||||
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
||||
import {
|
||||
addChats,
|
||||
@ -26,10 +26,8 @@ import {
|
||||
updateStoryViews,
|
||||
updateStoryViewsLoading,
|
||||
} from '../../reducers';
|
||||
import { updateTabState } from '../../reducers/tabs';
|
||||
import {
|
||||
selectChat,
|
||||
selectPeer, selectPeerStories, selectPeerStory, selectTabState,
|
||||
selectPeer, selectPeerStories, selectPeerStory,
|
||||
} from '../../selectors';
|
||||
|
||||
const INFINITE_LOOP_MARKER = 100;
|
||||
@ -504,172 +502,3 @@ addActionHandler('activateStealthMode', (global, actions, payload): ActionReturn
|
||||
|
||||
callApi('activateStealthMode', { isForPast: isForPast || true, isForFuture: isForFuture || true });
|
||||
});
|
||||
|
||||
addActionHandler('openBoostModal', async (global, actions, payload): Promise<void> => {
|
||||
const { chatId, tabId = getCurrentTabId() } = payload;
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat || !isChatChannel(chat)) return;
|
||||
|
||||
global = updateTabState(global, {
|
||||
boostModal: {
|
||||
chatId,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
|
||||
const result = await callApi('fetchBoostsStatus', {
|
||||
chat,
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
actions.closeBoostModal({ tabId });
|
||||
return;
|
||||
}
|
||||
|
||||
global = getGlobal();
|
||||
global = updateTabState(global, {
|
||||
boostModal: {
|
||||
chatId,
|
||||
boostStatus: result,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
|
||||
const myBoosts = await callApi('fetchMyBoosts');
|
||||
|
||||
if (!myBoosts) return;
|
||||
|
||||
global = getGlobal();
|
||||
const tabState = selectTabState(global, tabId);
|
||||
if (!tabState.boostModal) return;
|
||||
|
||||
global = addChats(global, buildCollectionByKey(myBoosts.chats, 'id'));
|
||||
global = addUsers(global, buildCollectionByKey(myBoosts.users, 'id'));
|
||||
global = updateTabState(global, {
|
||||
boostModal: {
|
||||
...tabState.boostModal,
|
||||
myBoosts: myBoosts.boosts,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('openBoostStatistics', async (global, actions, payload): Promise<void> => {
|
||||
const { chatId, tabId = getCurrentTabId() } = payload;
|
||||
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) return;
|
||||
|
||||
global = updateTabState(global, {
|
||||
boostStatistics: {
|
||||
chatId,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
|
||||
const [boostersListResult, boostStatusResult] = await Promise.all([
|
||||
callApi('fetchBoostersList', { chat }),
|
||||
callApi('fetchBoostsStatus', { chat }),
|
||||
]);
|
||||
|
||||
global = getGlobal();
|
||||
if (!boostersListResult || !boostStatusResult) {
|
||||
global = updateTabState(global, {
|
||||
boostStatistics: undefined,
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
return;
|
||||
}
|
||||
|
||||
global = addUsers(global, buildCollectionByKey(boostersListResult.users, 'id'));
|
||||
global = updateTabState(global, {
|
||||
boostStatistics: {
|
||||
chatId,
|
||||
boostStatus: boostStatusResult,
|
||||
boosters: boostersListResult.boosters,
|
||||
boosterIds: boostersListResult.boosterIds,
|
||||
count: boostersListResult.count,
|
||||
nextOffset: boostersListResult.nextOffset,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('loadMoreBoosters', async (global, actions, payload): Promise<void> => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
let tabState = selectTabState(global, tabId);
|
||||
if (!tabState.boostStatistics) return;
|
||||
|
||||
const chat = selectChat(global, tabState.boostStatistics.chatId);
|
||||
if (!chat) return;
|
||||
|
||||
global = updateTabState(global, {
|
||||
boostStatistics: {
|
||||
...tabState.boostStatistics,
|
||||
isLoadingBoosters: true,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
|
||||
const result = await callApi('fetchBoostersList', {
|
||||
chat,
|
||||
offset: tabState.boostStatistics.nextOffset,
|
||||
});
|
||||
if (!result) return;
|
||||
|
||||
global = getGlobal();
|
||||
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
|
||||
|
||||
tabState = selectTabState(global, tabId);
|
||||
if (!tabState.boostStatistics) return;
|
||||
|
||||
global = updateTabState(global, {
|
||||
boostStatistics: {
|
||||
...tabState.boostStatistics,
|
||||
boosters: {
|
||||
...tabState.boostStatistics.boosters,
|
||||
...result.boosters,
|
||||
},
|
||||
boosterIds: unique([...tabState.boostStatistics.boosterIds || [], ...result.boosterIds]),
|
||||
count: result.count,
|
||||
nextOffset: result.nextOffset,
|
||||
isLoadingBoosters: false,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('applyBoost', async (global, actions, payload): Promise<void> => {
|
||||
const { chatId, slots, tabId = getCurrentTabId() } = payload;
|
||||
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) return;
|
||||
|
||||
const result = await callApi('applyBoost', {
|
||||
slots,
|
||||
chat,
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
|
||||
const newStatusResult = await callApi('fetchBoostsStatus', {
|
||||
chat,
|
||||
});
|
||||
|
||||
if (!newStatusResult) {
|
||||
return;
|
||||
}
|
||||
|
||||
global = getGlobal();
|
||||
const tabState = selectTabState(global, tabId);
|
||||
if (!tabState.boostModal?.boostStatus) return;
|
||||
global = updateTabState(global, {
|
||||
boostModal: {
|
||||
...tabState.boostModal,
|
||||
boostStatus: newStatusResult,
|
||||
},
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
@ -455,17 +455,15 @@ addActionHandler('closeGame', (global, actions, payload): ActionReturnType => {
|
||||
|
||||
addActionHandler('requestConfetti', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
top, left, width, height, tabId = getCurrentTabId(),
|
||||
} = payload || {};
|
||||
tabId = getCurrentTabId(), ...rest
|
||||
} = payload;
|
||||
|
||||
if (!selectCanAnimateInterface(global)) return undefined;
|
||||
|
||||
return updateTabState(global, {
|
||||
confetti: {
|
||||
lastConfettiTime: Date.now(),
|
||||
top,
|
||||
left,
|
||||
width,
|
||||
height,
|
||||
...rest,
|
||||
},
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
@ -31,3 +31,11 @@ addActionHandler('addPaymentError', (global, actions, payload): ActionReturnType
|
||||
},
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('closeGiftCodeModal', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
|
||||
return updateTabState(global, {
|
||||
giftCodeModal: undefined,
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
@ -120,6 +120,7 @@ export function getMessageSummaryDescription(
|
||||
location,
|
||||
game,
|
||||
storyData,
|
||||
giveaway,
|
||||
} = message.content;
|
||||
|
||||
let hasUsedTruncatedText = false;
|
||||
@ -190,6 +191,10 @@ export function getMessageSummaryDescription(
|
||||
summary = `🎮 ${game.title}`;
|
||||
}
|
||||
|
||||
if (giveaway) {
|
||||
summary = lang('BoostingGiveawayChannelStarted');
|
||||
}
|
||||
|
||||
if (storyData) {
|
||||
if (storyData.isMention) {
|
||||
// eslint-disable-next-line eslint-multitab-tt/no-immediate-global
|
||||
|
||||
@ -55,12 +55,12 @@ export function getMessageTranscription(message: ApiMessage) {
|
||||
export function hasMessageText(message: ApiMessage | ApiStory) {
|
||||
const {
|
||||
text, sticker, photo, video, audio, voice, document, poll, webPage, contact, invoice, location,
|
||||
game, action, storyData,
|
||||
game, action, storyData, giveaway,
|
||||
} = message.content;
|
||||
|
||||
return Boolean(text) || !(
|
||||
sticker || photo || video || audio || voice || document || contact || poll || webPage || invoice || location
|
||||
|| game || action?.phoneCall || storyData
|
||||
|| game || action?.phoneCall || storyData || giveaway
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -569,7 +569,7 @@ export function selectAllowedMessageActions<T extends GlobalState>(global: T, me
|
||||
|| getServerTime() - message.date < MESSAGE_EDIT_ALLOWED_TIME
|
||||
) && !(
|
||||
content.sticker || content.contact || content.poll || content.action || content.audio
|
||||
|| (content.video?.isRound) || content.location || content.invoice
|
||||
|| (content.video?.isRound) || content.location || content.invoice || content.giveaway
|
||||
)
|
||||
&& !isForwarded
|
||||
&& !message.viaBotId
|
||||
|
||||
@ -6,6 +6,15 @@ import { getCurrentTabId } from '../../util/establishMultitabRole';
|
||||
import { selectTabState } from './tabs';
|
||||
import { selectIsCurrentUserPremium } from './users';
|
||||
|
||||
// https://github.com/DrKLO/Telegram/blob/c319639e9a4dff2f22da6762dcebd12d49f5afa1/TMessagesProj/src/main/java/org/telegram/ui/Components/Premium/boosts/cells/msg/GiveawayMessageCell.java#L59
|
||||
const MONTH_EMOTICON: Record<number, string> = {
|
||||
1: `${1}\u{FE0F}\u20E3`,
|
||||
3: `${2}\u{FE0F}\u20E3`,
|
||||
6: `${3}\u{FE0F}\u20E3`,
|
||||
12: `${4}\u{FE0F}\u20E3`,
|
||||
24: `${5}\u{FE0F}\u20E3`,
|
||||
};
|
||||
|
||||
export function selectIsStickerFavorite<T extends GlobalState>(global: T, sticker: ApiSticker) {
|
||||
const { stickers } = global.stickers.favorite;
|
||||
return stickers && stickers.some(({ id }) => id === sticker.id);
|
||||
@ -140,3 +149,10 @@ export function selectIsAlwaysHighPriorityEmoji<T extends GlobalState>(
|
||||
return stickerSet.id === global.appConfig?.defaultEmojiStatusesStickerSetId
|
||||
|| stickerSet.id === RESTRICTED_EMOJI_SET_ID;
|
||||
}
|
||||
|
||||
export function selectGiftStickerForDuration<T extends GlobalState>(global: T, duration = 1) {
|
||||
const stickers = global.premiumGifts?.stickers;
|
||||
if (!stickers) return undefined;
|
||||
const emoji = MONTH_EMOTICON[duration];
|
||||
return stickers.find((sticker) => sticker.emoji === emoji) || stickers[0];
|
||||
}
|
||||
|
||||
@ -14,6 +14,7 @@ import type {
|
||||
ApiChatlistInvite,
|
||||
ApiChatReactions,
|
||||
ApiChatType,
|
||||
ApiCheckedGiftCode,
|
||||
ApiConfig,
|
||||
ApiContact,
|
||||
ApiCountry,
|
||||
@ -634,6 +635,11 @@ export type TabState = {
|
||||
nextOffset?: string;
|
||||
count?: number;
|
||||
};
|
||||
|
||||
giftCodeModal?: {
|
||||
slug: string;
|
||||
info: ApiCheckedGiftCode;
|
||||
};
|
||||
};
|
||||
|
||||
export type GlobalState = {
|
||||
@ -1805,6 +1811,14 @@ export interface ActionPayloads {
|
||||
isEnabled: boolean;
|
||||
};
|
||||
|
||||
checkGiftCode: {
|
||||
slug: string;
|
||||
} & WithTabId;
|
||||
applyGiftCode: {
|
||||
slug: string;
|
||||
} & WithTabId;
|
||||
closeGiftCodeModal: WithTabId | undefined;
|
||||
|
||||
checkChatlistInvite: {
|
||||
slug: string;
|
||||
} & WithTabId;
|
||||
@ -2522,7 +2536,7 @@ export interface ActionPayloads {
|
||||
left: number;
|
||||
width: number;
|
||||
height: number;
|
||||
} & WithTabId) | undefined;
|
||||
} & WithTabId) | WithTabId;
|
||||
|
||||
updateAttachmentSettings: {
|
||||
shouldCompress?: boolean;
|
||||
|
||||
@ -1433,6 +1433,9 @@ 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.checkGiftCode#8e51b4c1 slug:string = payments.CheckedGiftCode;
|
||||
payments.applyGiftCode#f6e26854 slug:string = Updates;
|
||||
payments.getGiveawayInfo#f4239425 peer:InputPeer msg_id:int = payments.GiveawayInfo;
|
||||
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;
|
||||
|
||||
@ -318,5 +318,8 @@
|
||||
"premium.getBoostsStatus",
|
||||
"premium.getBoostersList",
|
||||
"premium.applyBoost",
|
||||
"premium.getMyBoosts"
|
||||
"premium.getMyBoosts",
|
||||
"payments.checkGiftCode",
|
||||
"payments.applyGiftCode",
|
||||
"payments.getGiveawayInfo"
|
||||
]
|
||||
|
||||
@ -7,7 +7,7 @@ import { IS_SAFARI } from './windowEnvironment';
|
||||
|
||||
type DeepLinkMethod = 'resolve' | 'login' | 'passport' | 'settings' | 'join' | 'addstickers' | 'addemoji' |
|
||||
'setlanguage' | 'addtheme' | 'confirmphone' | 'socks' | 'proxy' | 'privatepost' | 'bg' | 'share' | 'msg' | 'msg_url' |
|
||||
'invoice' | 'addlist' | 'boost';
|
||||
'invoice' | 'addlist' | 'boost' | 'giftcode';
|
||||
|
||||
export const processDeepLink = (url: string) => {
|
||||
const {
|
||||
@ -29,6 +29,7 @@ export const processDeepLink = (url: string) => {
|
||||
checkChatlistInvite,
|
||||
openStoryViewerByUsername,
|
||||
processBoostParameters,
|
||||
checkGiftCode,
|
||||
} = getActions();
|
||||
|
||||
// Safari thinks the path in tg://path links is hostname for some reason
|
||||
@ -157,6 +158,12 @@ export const processDeepLink = (url: string) => {
|
||||
processBoostParameters({ usernameOrId: channel || domain, isPrivate });
|
||||
break;
|
||||
}
|
||||
|
||||
case 'giftcode': {
|
||||
const { slug } = params;
|
||||
checkGiftCode({ slug });
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// Unsupported deeplink
|
||||
|
||||
|
||||
@ -115,6 +115,11 @@ export function uncompressEmoji(data: EmojiRawData): EmojiData {
|
||||
}
|
||||
|
||||
export function isoToEmoji(iso: string) {
|
||||
// Special case for Fragment numbers
|
||||
if (iso === 'FT') {
|
||||
return '\uD83C\uDFF4\u200D\u2620\uFE0F';
|
||||
}
|
||||
|
||||
const code = iso.toUpperCase();
|
||||
|
||||
if (!/^[A-Z]{2}$/.test(code)) return iso;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user