Introduce giveaway support (#3960)

This commit is contained in:
Alexander Zinchuk 2023-11-10 13:55:30 +04:00
parent 1eea365bb2
commit e050f427ee
50 changed files with 1368 additions and 388 deletions

View File

@ -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;

View File

@ -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,

View File

@ -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,
};
}

View File

@ -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),
};
}

View File

@ -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';

View File

@ -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,
});
}

View File

@ -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,
};
}

View File

@ -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 {

View File

@ -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;
};

View File

@ -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;
};

View File

@ -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';

View File

@ -9,7 +9,7 @@
text-overflow: ellipsis;
}
.moreMenu {
.moreMenu, .copy {
position: absolute;
right: 0.5rem;
top: 50%;

View File

@ -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>
);
};

View File

@ -51,6 +51,10 @@
}
}
&.fluid {
max-width: unset;
}
.SearchInput & {
flex: 1 0 auto;
position: relative;

View File

@ -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 (

View File

@ -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>

View File

@ -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}
/>

View File

@ -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,
};

View File

@ -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,

View File

@ -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;

View File

@ -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);

View 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;
}

View 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));

View File

@ -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}

View File

@ -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);
}

View File

@ -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;
}

View File

@ -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');

View File

@ -172,7 +172,7 @@ const BoostModal = ({
const handleApplyBoost = useLastCallback(() => {
closeReplaceModal();
applyBoost({ chatId: chat!.id, slots: [boost!.slot] });
requestConfetti();
requestConfetti({});
});
const handleProceedPremium = useLastCallback(() => {

View 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;

View 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;
}

View 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);

View File

@ -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')}
/>

View File

@ -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>

View File

@ -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;

View File

@ -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;

View File

@ -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';

View File

@ -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;

View File

@ -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 });
});

View File

@ -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);
});

View File

@ -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);
});

View File

@ -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);
});

View File

@ -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

View File

@ -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
);
}

View File

@ -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

View File

@ -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];
}

View File

@ -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;

View File

@ -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;

View File

@ -318,5 +318,8 @@
"premium.getBoostsStatus",
"premium.getBoostersList",
"premium.applyBoost",
"premium.getMyBoosts"
"premium.getMyBoosts",
"payments.checkGiftCode",
"payments.applyGiftCode",
"payments.getGiveawayInfo"
]

View File

@ -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

View File

@ -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;