Introduce Paid reactions (#4906)
This commit is contained in:
parent
dfe3666c00
commit
eda7c3ee77
@ -65,6 +65,7 @@ export interface GramJsAppConfig extends LimitsConfig {
|
||||
giveaway_boosts_per_premium: number;
|
||||
giveaway_countries_max: number;
|
||||
boosts_per_sent_gift: number;
|
||||
stars_paid_reaction_amount_max: number;
|
||||
// Forums
|
||||
topics_pinned_limit: number;
|
||||
// Stories
|
||||
@ -163,6 +164,7 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp
|
||||
bandwidthPremiumUploadSpeedup: appConfig.upload_premium_speedup_upload,
|
||||
bandwidthPremiumDownloadSpeedup: appConfig.upload_premium_speedup_download,
|
||||
channelRestrictAdsLevelMin: appConfig.channel_restrict_sponsored_level_min,
|
||||
paidReactionMaxAmount: appConfig.stars_paid_reaction_amount_max,
|
||||
isChannelRevenueWithdrawalEnabled: appConfig.channel_revenue_withdrawal_enabled,
|
||||
isStarsGiftsEnabled: appConfig.stars_gifts_enabled,
|
||||
};
|
||||
|
||||
@ -530,7 +530,7 @@ export function buildApiChatReactions(chatReactions?: GramJs.TypeChatReactions):
|
||||
if (chatReactions instanceof GramJs.ChatReactionsSome) {
|
||||
return {
|
||||
type: 'some',
|
||||
allowed: chatReactions.reactions.map(buildApiReaction).filter(Boolean),
|
||||
allowed: chatReactions.reactions.map((r) => buildApiReaction(r)).filter(Boolean),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -503,7 +503,7 @@ export function buildApiStarsTransactionPeer(peer: GramJs.TypeStarsTransactionPe
|
||||
|
||||
export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction): ApiStarsTransaction {
|
||||
const {
|
||||
date, id, peer, stars, description, photo, title, refund, extendedMedia, failed, msgId, pending, gift,
|
||||
date, id, peer, stars, description, photo, title, refund, extendedMedia, failed, msgId, pending, gift, reaction,
|
||||
} = transaction;
|
||||
|
||||
if (photo) {
|
||||
@ -527,6 +527,7 @@ export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction):
|
||||
messageId: msgId,
|
||||
isGift: gift,
|
||||
extendedMedia: boughtExtendedMedia,
|
||||
isReaction: reaction,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -3,11 +3,12 @@ import { Api as GramJs } from '../../../lib/gramjs';
|
||||
import type {
|
||||
ApiAvailableEffect,
|
||||
ApiAvailableReaction,
|
||||
ApiMessageReactor,
|
||||
ApiPeerReaction,
|
||||
ApiReaction,
|
||||
ApiReactionCount,
|
||||
ApiReactionEmoji,
|
||||
ApiReactions,
|
||||
ApiReactionWithPaid,
|
||||
ApiSavedReactionTag,
|
||||
} from '../../types';
|
||||
|
||||
@ -16,7 +17,7 @@ import { getApiChatIdFromMtpPeer } from './peers';
|
||||
|
||||
export function buildMessageReactions(reactions: GramJs.MessageReactions): ApiReactions {
|
||||
const {
|
||||
recentReactions, results, canSeeList, reactionsAsTags,
|
||||
recentReactions, results, canSeeList, reactionsAsTags, topReactors,
|
||||
} = reactions;
|
||||
|
||||
return {
|
||||
@ -24,15 +25,21 @@ export function buildMessageReactions(reactions: GramJs.MessageReactions): ApiRe
|
||||
canSeeList,
|
||||
results: results.map(buildReactionCount).filter(Boolean).sort(reactionCountComparator),
|
||||
recentReactions: recentReactions?.map(buildMessagePeerReaction).filter(Boolean),
|
||||
topReactors: topReactors?.map(buildApiMessageReactor).filter(Boolean),
|
||||
};
|
||||
}
|
||||
|
||||
function reactionCountComparator(a: ApiReactionCount, b: ApiReactionCount) {
|
||||
if (a.reaction.type === 'paid') return -1;
|
||||
if (b.reaction.type === 'paid') return 1;
|
||||
|
||||
const diff = b.count - a.count;
|
||||
if (diff) return diff;
|
||||
|
||||
if (a.chosenOrder !== undefined && b.chosenOrder !== undefined) {
|
||||
return a.chosenOrder - b.chosenOrder;
|
||||
}
|
||||
|
||||
if (a.chosenOrder !== undefined) return 1;
|
||||
if (b.chosenOrder !== undefined) return -1;
|
||||
return 0;
|
||||
@ -41,7 +48,7 @@ function reactionCountComparator(a: ApiReactionCount, b: ApiReactionCount) {
|
||||
export function buildReactionCount(reactionCount: GramJs.ReactionCount): ApiReactionCount | undefined {
|
||||
const { chosenOrder, count, reaction } = reactionCount;
|
||||
|
||||
const apiReaction = buildApiReaction(reaction);
|
||||
const apiReaction = buildApiReaction(reaction, true);
|
||||
if (!apiReaction) return undefined;
|
||||
|
||||
return {
|
||||
@ -51,6 +58,20 @@ export function buildReactionCount(reactionCount: GramJs.ReactionCount): ApiReac
|
||||
};
|
||||
}
|
||||
|
||||
export function buildApiMessageReactor(reactor: GramJs.MessageReactor): ApiMessageReactor {
|
||||
const {
|
||||
count, my, top, anonymous, peerId,
|
||||
} = reactor;
|
||||
|
||||
return {
|
||||
peerId: peerId && getApiChatIdFromMtpPeer(peerId),
|
||||
count,
|
||||
isMe: my,
|
||||
isTop: top,
|
||||
isAnonymous: anonymous,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildMessagePeerReaction(userReaction: GramJs.MessagePeerReaction): ApiPeerReaction | undefined {
|
||||
const {
|
||||
peerId, reaction, big, unread, date, my,
|
||||
@ -69,19 +90,29 @@ export function buildMessagePeerReaction(userReaction: GramJs.MessagePeerReactio
|
||||
};
|
||||
}
|
||||
|
||||
export function buildApiReaction(reaction: GramJs.TypeReaction): ApiReaction | undefined {
|
||||
export function buildApiReaction(reaction: GramJs.TypeReaction, withPaid?: never): ApiReaction | undefined;
|
||||
export function buildApiReaction(reaction: GramJs.TypeReaction, withPaid: true): ApiReactionWithPaid | undefined;
|
||||
export function buildApiReaction(reaction: GramJs.TypeReaction, withPaid?: true): ApiReactionWithPaid | undefined {
|
||||
if (reaction instanceof GramJs.ReactionEmoji) {
|
||||
return {
|
||||
type: 'emoji',
|
||||
emoticon: reaction.emoticon,
|
||||
};
|
||||
}
|
||||
|
||||
if (reaction instanceof GramJs.ReactionCustomEmoji) {
|
||||
return {
|
||||
type: 'custom',
|
||||
documentId: reaction.documentId.toString(),
|
||||
};
|
||||
}
|
||||
|
||||
if (withPaid && reaction instanceof GramJs.ReactionPaid) {
|
||||
return {
|
||||
type: 'paid',
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -112,7 +143,7 @@ export function buildApiAvailableReaction(availableReaction: GramJs.AvailableRea
|
||||
staticIcon: buildApiDocument(staticIcon),
|
||||
aroundAnimation: aroundAnimation ? buildApiDocument(aroundAnimation) : undefined,
|
||||
centerIcon: centerIcon ? buildApiDocument(centerIcon) : undefined,
|
||||
reaction: { emoticon: reaction } as ApiReactionEmoji,
|
||||
reaction: { type: 'emoji', emoticon: reaction },
|
||||
title,
|
||||
isInactive: inactive,
|
||||
isPremium: premium,
|
||||
|
||||
@ -58,6 +58,8 @@ export function buildApiStory(peerId: string, story: GramJs.TypeStoryItem): ApiT
|
||||
content.text = buildMessageTextContent(caption, entities);
|
||||
}
|
||||
|
||||
const reaction = sentReaction && buildApiReaction(sentReaction);
|
||||
|
||||
return omitUndefined<ApiStory>({
|
||||
id,
|
||||
peerId,
|
||||
@ -75,7 +77,7 @@ export function buildApiStory(peerId: string, story: GramJs.TypeStoryItem): ApiT
|
||||
isOut: out,
|
||||
visibility: privacy && buildPrivacyRules(privacy),
|
||||
mediaAreas: mediaAreas?.map(buildApiMediaArea).filter(Boolean),
|
||||
sentReaction: sentReaction && buildApiReaction(sentReaction),
|
||||
sentReaction: reaction,
|
||||
forwardInfo: fwdFrom && buildApiStoryForwardInfo(fwdFrom),
|
||||
fromId: fromId && getApiChatIdFromMtpPeer(fromId),
|
||||
});
|
||||
@ -197,7 +199,9 @@ export function buildApiMediaArea(area: GramJs.TypeMediaArea): ApiMediaArea | un
|
||||
} = area;
|
||||
|
||||
const apiReaction = buildApiReaction(reaction);
|
||||
if (!apiReaction) return undefined;
|
||||
if (!apiReaction) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
type: 'suggestedReaction',
|
||||
|
||||
@ -18,7 +18,8 @@ import type {
|
||||
ApiPhoneCall,
|
||||
ApiPhoto,
|
||||
ApiPoll,
|
||||
ApiPremiumGiftCodeOption, ApiReaction,
|
||||
ApiPremiumGiftCodeOption,
|
||||
ApiReactionWithPaid,
|
||||
ApiReportReason,
|
||||
ApiRequestInputInvoice,
|
||||
ApiSendMessageAction,
|
||||
@ -291,6 +292,15 @@ export function generateRandomBigInt() {
|
||||
return readBigIntFromBuffer(generateRandomBytes(8), true, true);
|
||||
}
|
||||
|
||||
export function generateRandomTimestampedBigInt() {
|
||||
// 32 bits for timestamp, 32 bits are random
|
||||
const buffer = generateRandomBytes(8);
|
||||
const timestampBuffer = Buffer.alloc(4);
|
||||
timestampBuffer.writeUInt32LE(Math.floor(Date.now() / 1000), 0);
|
||||
buffer.set(timestampBuffer, 4);
|
||||
return readBigIntFromBuffer(buffer, true, true);
|
||||
}
|
||||
|
||||
export function generateRandomInt() {
|
||||
return readBigIntFromBuffer(generateRandomBytes(4), true, true).toJSNumber();
|
||||
}
|
||||
@ -650,20 +660,21 @@ export function buildInputInvoice(invoice: ApiRequestInputInvoice) {
|
||||
}
|
||||
}
|
||||
|
||||
export function buildInputReaction(reaction?: ApiReaction) {
|
||||
if (reaction && 'emoticon' in reaction) {
|
||||
return new GramJs.ReactionEmoji({
|
||||
emoticon: reaction.emoticon,
|
||||
});
|
||||
export function buildInputReaction(reaction?: ApiReactionWithPaid) {
|
||||
switch (reaction?.type) {
|
||||
case 'emoji':
|
||||
return new GramJs.ReactionEmoji({
|
||||
emoticon: reaction.emoticon,
|
||||
});
|
||||
case 'custom':
|
||||
return new GramJs.ReactionCustomEmoji({
|
||||
documentId: BigInt(reaction.documentId),
|
||||
});
|
||||
case 'paid':
|
||||
return new GramJs.ReactionPaid();
|
||||
default:
|
||||
return new GramJs.ReactionEmpty();
|
||||
}
|
||||
|
||||
if (reaction && 'documentId' in reaction) {
|
||||
return new GramJs.ReactionCustomEmoji({
|
||||
documentId: BigInt(reaction.documentId),
|
||||
});
|
||||
}
|
||||
|
||||
return new GramJs.ReactionEmpty();
|
||||
}
|
||||
|
||||
export function buildInputChatReactions(chatReactions?: ApiChatReactions) {
|
||||
|
||||
@ -602,6 +602,7 @@ async function getFullChannelInfo(
|
||||
boostsApplied,
|
||||
boostsUnrestrict,
|
||||
canViewRevenue: canViewMonetization,
|
||||
paidReactionsAvailable,
|
||||
} = result.fullChat;
|
||||
|
||||
if (chatPhoto) {
|
||||
@ -691,6 +692,7 @@ async function getFullChannelInfo(
|
||||
hasPinnedStories: Boolean(storiesPinnedAvailable),
|
||||
boostsApplied,
|
||||
boostsToUnrestrict: boostsUnrestrict,
|
||||
isPaidReactionAvailable: paidReactionsAvailable,
|
||||
},
|
||||
chats,
|
||||
userStatusesById: statusesById,
|
||||
|
||||
@ -1069,6 +1069,10 @@ export async function fetchFactChecks({
|
||||
return results.flatMap((result) => result!).map(buildApiFactCheck);
|
||||
}
|
||||
|
||||
export function fetchPaidReactionPrivacy() {
|
||||
return invokeRequest(new GramJs.messages.GetPaidReactionPrivacy(), { shouldReturnTrue: true });
|
||||
}
|
||||
|
||||
export async function fetchDiscussionMessage({
|
||||
chat, messageId,
|
||||
}: {
|
||||
|
||||
@ -20,7 +20,7 @@ import {
|
||||
buildMessagePeerReaction,
|
||||
} from '../apiBuilders/reactions';
|
||||
import { buildStickerFromDocument } from '../apiBuilders/symbols';
|
||||
import { buildInputPeer, buildInputReaction } from '../gramjsBuilders';
|
||||
import { buildInputPeer, buildInputReaction, generateRandomTimestampedBigInt } from '../gramjsBuilders';
|
||||
import localDb from '../localDb';
|
||||
import { invokeRequest } from './client';
|
||||
|
||||
@ -150,6 +150,29 @@ export function sendReaction({
|
||||
});
|
||||
}
|
||||
|
||||
export function sendPaidReaction({
|
||||
chat,
|
||||
messageId,
|
||||
count,
|
||||
isPrivate,
|
||||
}: {
|
||||
chat: ApiChat;
|
||||
messageId: number;
|
||||
count: number;
|
||||
isPrivate?: boolean;
|
||||
}) {
|
||||
return invokeRequest(new GramJs.messages.SendPaidReaction({
|
||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||
msgId: messageId,
|
||||
randomId: generateRandomTimestampedBigInt(),
|
||||
count,
|
||||
private: isPrivate || undefined,
|
||||
}), {
|
||||
shouldReturnTrue: true,
|
||||
shouldThrow: true,
|
||||
});
|
||||
}
|
||||
|
||||
export function fetchMessageReactions({
|
||||
ids, chat,
|
||||
}: {
|
||||
@ -215,7 +238,7 @@ export async function fetchTopReactions({ hash = '0' }: { hash?: string }) {
|
||||
|
||||
return {
|
||||
hash: String(result.hash),
|
||||
reactions: result.reactions.map(buildApiReaction).filter(Boolean),
|
||||
reactions: result.reactions.map((r) => buildApiReaction(r)).filter(Boolean),
|
||||
};
|
||||
}
|
||||
|
||||
@ -231,7 +254,7 @@ export async function fetchRecentReactions({ hash = '0' }: { hash?: string }) {
|
||||
|
||||
return {
|
||||
hash: String(result.hash),
|
||||
reactions: result.reactions.map(buildApiReaction).filter(Boolean),
|
||||
reactions: result.reactions.map((r) => buildApiReaction(r)).filter(Boolean),
|
||||
};
|
||||
}
|
||||
|
||||
@ -250,7 +273,7 @@ export async function fetchDefaultTagReactions({ hash = '0' }: { hash?: string }
|
||||
|
||||
return {
|
||||
hash: String(result.hash),
|
||||
reactions: result.reactions.map(buildApiReaction).filter(Boolean),
|
||||
reactions: result.reactions.map((r) => buildApiReaction(r)).filter(Boolean),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -1009,11 +1009,12 @@ export function updater(update: Update) {
|
||||
lastReadId: update.maxId,
|
||||
});
|
||||
} else if (update instanceof GramJs.UpdateSentStoryReaction) {
|
||||
const reaction = buildApiReaction(update.reaction);
|
||||
sendApiUpdate({
|
||||
'@type': 'updateSentStoryReaction',
|
||||
peerId: getApiChatIdFromMtpPeer(update.peer),
|
||||
storyId: update.storyId,
|
||||
reaction: buildApiReaction(update.reaction),
|
||||
reaction,
|
||||
});
|
||||
} else if (update instanceof GramJs.UpdateStoriesStealthMode) {
|
||||
sendApiUpdate({
|
||||
@ -1044,6 +1045,11 @@ export function updater(update: Update) {
|
||||
'@type': 'updateStarsBalance',
|
||||
balance: update.balance.toJSNumber(),
|
||||
});
|
||||
} else if (update instanceof GramJs.UpdatePaidReactionPrivacy) {
|
||||
sendApiUpdate({
|
||||
'@type': 'updatePaidReactionPrivacy',
|
||||
isPrivate: update.private,
|
||||
});
|
||||
} else if (update instanceof LocalUpdatePremiumFloodWait) {
|
||||
sendApiUpdate({
|
||||
'@type': 'updatePremiumFloodWait',
|
||||
|
||||
@ -136,6 +136,7 @@ export interface ApiChatFullInfo {
|
||||
areParticipantsHidden?: boolean;
|
||||
isTranslationDisabled?: true;
|
||||
hasPinnedStories?: boolean;
|
||||
isPaidReactionAvailable?: boolean;
|
||||
|
||||
boostsApplied?: number;
|
||||
boostsToUnrestrict?: number;
|
||||
|
||||
@ -688,6 +688,7 @@ export interface ApiReactions {
|
||||
areTags?: boolean;
|
||||
results: ApiReactionCount[];
|
||||
recentReactions?: ApiPeerReaction[];
|
||||
topReactors?: ApiMessageReactor[];
|
||||
}
|
||||
|
||||
export interface ApiPeerReaction {
|
||||
@ -699,10 +700,20 @@ export interface ApiPeerReaction {
|
||||
addedDate: number;
|
||||
}
|
||||
|
||||
export interface ApiMessageReactor {
|
||||
isTop?: true;
|
||||
isMe?: true;
|
||||
count: number;
|
||||
isAnonymous?: true;
|
||||
peerId?: string;
|
||||
}
|
||||
|
||||
export interface ApiReactionCount {
|
||||
chosenOrder?: number;
|
||||
count: number;
|
||||
reaction: ApiReaction;
|
||||
reaction: ApiReactionWithPaid;
|
||||
localAmount?: number;
|
||||
localIsPrivate?: boolean;
|
||||
}
|
||||
|
||||
export interface ApiAvailableReaction {
|
||||
@ -741,16 +752,23 @@ type ApiChatReactionsSome = {
|
||||
export type ApiChatReactions = ApiChatReactionsAll | ApiChatReactionsSome;
|
||||
|
||||
export type ApiReactionEmoji = {
|
||||
type: 'emoji';
|
||||
emoticon: string;
|
||||
};
|
||||
|
||||
export type ApiReactionCustomEmoji = {
|
||||
type: 'custom';
|
||||
documentId: string;
|
||||
};
|
||||
|
||||
export type ApiReaction = ApiReactionEmoji | ApiReactionCustomEmoji;
|
||||
export type ApiReactionPaid = {
|
||||
type: 'paid';
|
||||
};
|
||||
|
||||
export type ApiReactionKey = `${string}-${string}`;
|
||||
export type ApiReaction = ApiReactionEmoji | ApiReactionCustomEmoji;
|
||||
export type ApiReactionWithPaid = ApiReaction | ApiReactionPaid;
|
||||
|
||||
export type ApiReactionKey = `${string}-${string}` | 'paid' | 'unsupported';
|
||||
|
||||
export type ApiSavedReactionTag = {
|
||||
reaction: ApiReaction;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { ApiLimitType, ApiPremiumSection, CallbackAction } from '../../global/types';
|
||||
import type { IconName } from '../../types/icons';
|
||||
import type { ApiDocument, ApiPhoto, ApiReaction } from './messages';
|
||||
import type { ApiUser } from './users';
|
||||
|
||||
@ -110,10 +111,15 @@ export type ApiNotification = {
|
||||
localId: string;
|
||||
title?: string;
|
||||
message: string;
|
||||
cacheBreaker?: string;
|
||||
actionText?: string;
|
||||
action?: CallbackAction | CallbackAction[];
|
||||
className?: string;
|
||||
duration?: number;
|
||||
disableClickDismiss?: boolean;
|
||||
shouldShowTimer?: boolean;
|
||||
icon?: IconName;
|
||||
dismissAction?: CallbackAction;
|
||||
};
|
||||
|
||||
export type ApiError = {
|
||||
@ -210,6 +216,7 @@ export interface ApiAppConfig {
|
||||
bandwidthPremiumUploadSpeedup?: number;
|
||||
bandwidthPremiumDownloadSpeedup?: number;
|
||||
channelRestrictAdsLevelMin?: number;
|
||||
paidReactionMaxAmount?: number;
|
||||
isChannelRevenueWithdrawalEnabled?: boolean;
|
||||
isStarsGiftsEnabled?: boolean;
|
||||
}
|
||||
|
||||
@ -303,6 +303,7 @@ export interface ApiStarsTransaction {
|
||||
isGift?: true;
|
||||
isPrizeStars?: true;
|
||||
isMyGift?: true; // Used only for outgoing star gift messages
|
||||
isReaction?: true;
|
||||
hasFailed?: true;
|
||||
isPending?: true;
|
||||
date: number;
|
||||
|
||||
@ -1,6 +1,12 @@
|
||||
import type { ApiPrivacySettings } from '../../types';
|
||||
import type {
|
||||
ApiGeoPoint, ApiMessage, ApiReaction, ApiReactionCount, ApiSticker, ApiStoryForwardInfo, MediaContent,
|
||||
ApiGeoPoint,
|
||||
ApiMessage,
|
||||
ApiReaction,
|
||||
ApiReactionCount,
|
||||
ApiSticker,
|
||||
ApiStoryForwardInfo,
|
||||
MediaContent,
|
||||
} from './messages';
|
||||
|
||||
export interface ApiStory {
|
||||
|
||||
@ -765,6 +765,11 @@ export type ApiUpdateEntities = {
|
||||
threadInfos?: ApiThreadInfo[];
|
||||
};
|
||||
|
||||
export type ApiUpdatePaidReactionPrivacy = {
|
||||
'@type': 'updatePaidReactionPrivacy';
|
||||
isPrivate: boolean;
|
||||
};
|
||||
|
||||
export type ApiUpdate = (
|
||||
ApiUpdateReady | ApiUpdateSession | ApiUpdateWebAuthTokenFailed | ApiUpdateRequestUserUpdate |
|
||||
ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser |
|
||||
@ -797,7 +802,7 @@ export type ApiUpdate = (
|
||||
ApiUpdateViewForumAsMessages | ApiUpdateSavedDialogPinned | ApiUpdatePinnedSavedDialogIds | ApiUpdateChatLastMessage |
|
||||
ApiUpdateDeleteSavedHistory | ApiUpdatePremiumFloodWait | ApiUpdateStarsBalance |
|
||||
ApiUpdateQuickReplyMessage | ApiUpdateQuickReplies | ApiDeleteQuickReply | ApiUpdateDeleteQuickReplyMessages |
|
||||
ApiUpdateDeleteProfilePhoto | ApiUpdateNewProfilePhoto | ApiUpdateEntities
|
||||
ApiUpdateDeleteProfilePhoto | ApiUpdateNewProfilePhoto | ApiUpdateEntities | ApiUpdatePaidReactionPrivacy
|
||||
);
|
||||
|
||||
export type OnApiUpdate = (update: ApiUpdate) => void;
|
||||
|
||||
BIN
src/assets/fonts/Roboto-Round-Regular.woff2
Normal file
BIN
src/assets/fonts/Roboto-Round-Regular.woff2
Normal file
Binary file not shown.
@ -145,3 +145,9 @@
|
||||
font-display:swap;
|
||||
unicode-range: U+0600-06FF;
|
||||
}
|
||||
|
||||
@font-face {
|
||||
font-family: 'Roboto Round';
|
||||
src: url('Roboto-Round-Regular.woff2') format('woff2');
|
||||
unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, U+FEFF, U+FFFD;
|
||||
}
|
||||
|
||||
@ -1283,6 +1283,10 @@
|
||||
"CreditsBoxHistoryEntryGiftOutAbout" = "With Stars, {user} will be able to unlock content and services on Telegram. {link}"
|
||||
"CreditsBoxOutAbout" = "Review the {link} for Stars."
|
||||
"GiftStarsOutgoing" = "With Stars, {user} will be able to unlock content and services on Telegram."
|
||||
"SendPaidReaction" = "Send ⭐️{amount}"
|
||||
"StarsReactionTerms" = "By sending Stars you agree to the {link}"
|
||||
"StarsReactionLinkText" = "Terms of Service"
|
||||
"StarsReactionLink" = "https://telegram.org/tos/stars"
|
||||
"MiniAppsMoreTabs_one" = "{botName} & {count} Other";
|
||||
"MiniAppsMoreTabs_other" = "{botName} & {count} Others";
|
||||
"PrizeCredits" = "Your prize is {count} Stars."
|
||||
|
||||
BIN
src/assets/tgs/stars/StarReaction.tgs
Normal file
BIN
src/assets/tgs/stars/StarReaction.tgs
Normal file
Binary file not shown.
BIN
src/assets/tgs/stars/StarReactionEffect.tgs
Normal file
BIN
src/assets/tgs/stars/StarReactionEffect.tgs
Normal file
Binary file not shown.
@ -30,6 +30,7 @@ export { default as ChatlistModal } from '../components/modals/chatlist/Chatlist
|
||||
export { default as StarsBalanceModal } from '../components/modals/stars/StarsBalanceModal';
|
||||
export { default as StarPaymentModal } from '../components/modals/stars/StarsPaymentModal';
|
||||
export { default as StarsTransactionInfoModal } from '../components/modals/stars/transaction/StarsTransactionModal';
|
||||
export { default as PaidReactionModal } from '../components/modals/paidReaction/PaidReactionModal';
|
||||
|
||||
export { default as AboutAdsModal } from '../components/common/AboutAdsModal';
|
||||
export { default as AboutMonetizationModal } from '../components/common/AboutMonetizationModal';
|
||||
|
||||
@ -16,6 +16,7 @@ type OwnProps = {
|
||||
text: string;
|
||||
className?: string;
|
||||
isDisabled?: boolean;
|
||||
ref?: React.RefObject<HTMLSpanElement>;
|
||||
};
|
||||
|
||||
const ANIMATION_TIME = 200;
|
||||
@ -31,6 +32,7 @@ const AnimatedCounter: FC<OwnProps> = ({
|
||||
text,
|
||||
className,
|
||||
isDisabled,
|
||||
ref,
|
||||
}) => {
|
||||
const { isRtl } = useLang();
|
||||
|
||||
@ -58,7 +60,7 @@ const AnimatedCounter: FC<OwnProps> = ({
|
||||
}, [shouldAnimate, text]);
|
||||
|
||||
return (
|
||||
<span className={buildClassName(className, !isDisabled && styles.root)} dir={isRtl ? 'rtl' : undefined}>
|
||||
<span ref={ref} className={buildClassName(className, !isDisabled && styles.root)} dir={isRtl ? 'rtl' : undefined}>
|
||||
{characters}
|
||||
</span>
|
||||
);
|
||||
|
||||
@ -50,11 +50,13 @@ import {
|
||||
import { requestMeasure, requestNextMutation } from '../../lib/fasterdom/fasterdom';
|
||||
import {
|
||||
getAllowedAttachmentOptions,
|
||||
getReactionKey,
|
||||
getStoryKey,
|
||||
hasReplaceableMedia,
|
||||
isChatAdmin,
|
||||
isChatChannel,
|
||||
isChatSuperGroup,
|
||||
isSameReaction,
|
||||
isUserId,
|
||||
} from '../../global/helpers';
|
||||
import {
|
||||
@ -436,8 +438,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
const { emojiSet, members: groupChatMembers, botCommands: chatBotCommands } = chatFullInfo || {};
|
||||
const chatEmojiSetId = emojiSet?.id;
|
||||
|
||||
const isSentStoryReactionHeart = sentStoryReaction && 'emoticon' in sentStoryReaction
|
||||
? sentStoryReaction.emoticon === HEART_REACTION.emoticon : false;
|
||||
const isSentStoryReactionHeart = sentStoryReaction && isSameReaction(sentStoryReaction, HEART_REACTION);
|
||||
|
||||
useEffect(processMessageInputForCustomEmoji, [getHtml]);
|
||||
|
||||
@ -1503,9 +1504,11 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
let text: string | undefined;
|
||||
let entities: ApiMessageEntity[] | undefined;
|
||||
|
||||
if ('emoticon' in reaction) {
|
||||
if (reaction.type === 'emoji') {
|
||||
text = reaction.emoticon;
|
||||
} else {
|
||||
}
|
||||
|
||||
if (reaction.type === 'custom') {
|
||||
const sticker = getGlobal().customEmojis.byId[reaction.documentId];
|
||||
if (!sticker) {
|
||||
return;
|
||||
@ -1983,7 +1986,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
>
|
||||
{sentStoryReaction && (
|
||||
<ReactionAnimatedEmoji
|
||||
key={'documentId' in sentStoryReaction ? sentStoryReaction.documentId : sentStoryReaction.emoticon}
|
||||
key={getReactionKey(sentStoryReaction)}
|
||||
containerId={getStoryKey(chatId, storyId!)}
|
||||
reaction={sentStoryReaction}
|
||||
withEffectOnly={isSentStoryReactionHeart}
|
||||
|
||||
@ -5,7 +5,7 @@ import React, {
|
||||
import { getGlobal, withGlobal } from '../../global';
|
||||
|
||||
import type {
|
||||
ApiAvailableReaction, ApiReaction, ApiSticker, ApiStickerSet,
|
||||
ApiAvailableReaction, ApiReaction, ApiReactionWithPaid, ApiSticker, ApiStickerSet,
|
||||
} from '../../api/types';
|
||||
import type { StickerSetOrReactionsSetOrRecent } from '../../types';
|
||||
|
||||
@ -58,12 +58,13 @@ type OwnProps = {
|
||||
loadAndPlay: boolean;
|
||||
idPrefix?: string;
|
||||
withDefaultTopicIcons?: boolean;
|
||||
onCustomEmojiSelect: (sticker: ApiSticker) => void;
|
||||
onReactionSelect?: (reaction: ApiReaction) => void;
|
||||
selectedReactionIds?: string[];
|
||||
isStatusPicker?: boolean;
|
||||
isReactionPicker?: boolean;
|
||||
isTranslucent?: boolean;
|
||||
onCustomEmojiSelect: (sticker: ApiSticker) => void;
|
||||
onReactionSelect?: (reaction: ApiReactionWithPaid) => void;
|
||||
onReactionContext?: (reaction: ApiReactionWithPaid) => void;
|
||||
onContextMenuOpen?: NoneToVoidFunction;
|
||||
onContextMenuClose?: NoneToVoidFunction;
|
||||
onContextMenuClick?: NoneToVoidFunction;
|
||||
@ -86,6 +87,7 @@ type StateProps = {
|
||||
canAnimate?: boolean;
|
||||
isSavedMessages?: boolean;
|
||||
isCurrentUserPremium?: boolean;
|
||||
isWithPaidReaction?: boolean;
|
||||
};
|
||||
|
||||
const HEADER_BUTTON_WIDTH = 2.5 * REM; // px (including margin)
|
||||
@ -128,8 +130,10 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
defaultTopicIconsId,
|
||||
defaultStatusIconsId,
|
||||
defaultTagReactions,
|
||||
isWithPaidReaction,
|
||||
onCustomEmojiSelect,
|
||||
onReactionSelect,
|
||||
onReactionContext,
|
||||
onContextMenuOpen,
|
||||
onContextMenuClose,
|
||||
onContextMenuClick,
|
||||
@ -186,7 +190,10 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
|
||||
if (isReactionPicker && !isSavedMessages) {
|
||||
const topReactionsSlice = topReactions?.slice(0, TOP_REACTIONS_COUNT) || [];
|
||||
const topReactionsSlice: ApiReactionWithPaid[] = topReactions?.slice(0, TOP_REACTIONS_COUNT) || [];
|
||||
if (isWithPaidReaction) {
|
||||
topReactionsSlice.unshift({ type: 'paid' });
|
||||
}
|
||||
if (topReactionsSlice?.length) {
|
||||
defaultSets.push({
|
||||
id: TOP_SYMBOL_SET_ID,
|
||||
@ -271,6 +278,7 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
addedCustomEmojiIds, isReactionPicker, isStatusPicker, withDefaultTopicIcons, recentCustomEmojis,
|
||||
customEmojiFeaturedIds, stickerSetsById, topReactions, availableReactions, lang, recentReactions,
|
||||
defaultStatusIconsId, defaultTopicIconsId, isSavedMessages, defaultTagReactions, chatEmojiSetId,
|
||||
isWithPaidReaction,
|
||||
]);
|
||||
|
||||
const noPopulatedSets = useMemo(() => (
|
||||
@ -303,10 +311,6 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
onCustomEmojiSelect(emoji);
|
||||
});
|
||||
|
||||
const handleReactionSelect = useLastCallback((reaction: ApiReaction) => {
|
||||
onReactionSelect?.(reaction);
|
||||
});
|
||||
|
||||
function renderCover(stickerSet: StickerSetOrReactionsSetOrRecent, index: number) {
|
||||
const firstSticker = stickerSet.stickers?.[0];
|
||||
const buttonClassName = buildClassName(
|
||||
@ -441,7 +445,8 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
selectedReactionIds={selectedReactionIds}
|
||||
availableReactions={availableReactions}
|
||||
isTranslucent={isTranslucent}
|
||||
onReactionSelect={handleReactionSelect}
|
||||
onReactionSelect={onReactionSelect}
|
||||
onReactionContext={onReactionContext}
|
||||
onStickerSelect={handleEmojiSelect}
|
||||
onContextMenuOpen={onContextMenuOpen}
|
||||
onContextMenuClose={onContextMenuClose}
|
||||
@ -495,6 +500,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
topReactions: isReactionPicker ? topReactions : undefined,
|
||||
recentReactions: isReactionPicker ? recentReactions : undefined,
|
||||
chatEmojiSetId: chatFullInfo?.emojiSet?.id,
|
||||
isWithPaidReaction: isReactionPicker && chatFullInfo?.isPaidReactionAvailable,
|
||||
availableReactions: isReactionPicker ? availableReactions : undefined,
|
||||
defaultTagReactions: isReactionPicker ? defaultTags : undefined,
|
||||
};
|
||||
|
||||
44
src/components/common/PeerBadge.module.scss
Normal file
44
src/components/common/PeerBadge.module.scss
Normal file
@ -0,0 +1,44 @@
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.top {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
bottom: -0.5rem;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.125rem;
|
||||
font-size: 0.75rem;
|
||||
font-weight: 500;
|
||||
line-height: 1;
|
||||
white-space: nowrap;
|
||||
padding: 0.25rem;
|
||||
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-white);
|
||||
border: 2px solid var(--color-background);
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.text {
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
text-align: center;
|
||||
font-size: 0.875rem;
|
||||
width: 100%;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
50
src/components/common/PeerBadge.tsx
Normal file
50
src/components/common/PeerBadge.tsx
Normal file
@ -0,0 +1,50 @@
|
||||
import React, { memo } from '../../lib/teact/teact';
|
||||
|
||||
import type { ApiPeer } from '../../api/types';
|
||||
import type { CustomPeer } from '../../types';
|
||||
import type { IconName } from '../../types/icons';
|
||||
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
|
||||
import Avatar from './Avatar';
|
||||
import Icon from './icons/Icon';
|
||||
|
||||
import styles from './PeerBadge.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
peer: ApiPeer | CustomPeer;
|
||||
text?: string;
|
||||
badgeText: string;
|
||||
badgeIcon?: IconName;
|
||||
className?: string;
|
||||
badgeClassName?: string;
|
||||
onClick?: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
const PeerBadge = ({
|
||||
peer,
|
||||
text,
|
||||
badgeText,
|
||||
badgeIcon,
|
||||
className,
|
||||
badgeClassName,
|
||||
onClick,
|
||||
}: OwnProps) => {
|
||||
return (
|
||||
<div
|
||||
className={buildClassName(styles.root, onClick && styles.clickable, className)}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className={styles.top}>
|
||||
<Avatar size="large" peer={peer} />
|
||||
<div className={buildClassName(styles.badge, badgeClassName)}>
|
||||
{badgeIcon && <Icon name={badgeIcon} />}
|
||||
{badgeText}
|
||||
</div>
|
||||
</div>
|
||||
{text && <p className={styles.text}>{text}</p>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(PeerBadge);
|
||||
49
src/components/common/Sparkles.module.scss
Normal file
49
src/components/common/Sparkles.module.scss
Normal file
@ -0,0 +1,49 @@
|
||||
.root {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
z-index: -1;
|
||||
line-height: 1;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.progress {
|
||||
--_progress: 0;
|
||||
|
||||
z-index: 0;
|
||||
font-size: 0.75rem;
|
||||
opacity: 0.8;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.reaction {
|
||||
font-size: 0.5rem;
|
||||
}
|
||||
|
||||
.symbol {
|
||||
--_duration-shift: 0s;
|
||||
--_shift-x: 0;
|
||||
--_shift-y: 0;
|
||||
|
||||
position: absolute;
|
||||
|
||||
width: 0.5rem;
|
||||
height: 0.5rem;
|
||||
|
||||
animation: sparkle 5s infinite;
|
||||
animation-delay: var(--_duration-shift);
|
||||
}
|
||||
|
||||
@keyframes sparkle {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translate(0, 0);
|
||||
}
|
||||
15% {
|
||||
opacity: 1;
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translate(var(--_shift-x), var(--_shift-y));
|
||||
}
|
||||
}
|
||||
156
src/components/common/Sparkles.tsx
Normal file
156
src/components/common/Sparkles.tsx
Normal file
@ -0,0 +1,156 @@
|
||||
import React, { memo } from '../../lib/teact/teact';
|
||||
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import buildStyle from '../../util/buildStyle';
|
||||
|
||||
import styles from './Sparkles.module.scss';
|
||||
|
||||
type ReactionParameters = {
|
||||
preset: 'reaction';
|
||||
};
|
||||
|
||||
type ProgressParameters = {
|
||||
preset: 'progress';
|
||||
};
|
||||
|
||||
type PresetParameters = ReactionParameters | ProgressParameters;
|
||||
|
||||
type OwnProps = {
|
||||
className?: string;
|
||||
} & PresetParameters;
|
||||
|
||||
const SYMBOL = '✦';
|
||||
const ANIMATION_DURATION = 5;
|
||||
|
||||
// Values are in percents
|
||||
const REACTION_POSITIONS = [{
|
||||
x: 20,
|
||||
y: 0,
|
||||
size: 100,
|
||||
durationShift: 10,
|
||||
}, {
|
||||
x: 15,
|
||||
y: 15,
|
||||
size: 75,
|
||||
durationShift: 70,
|
||||
}, {
|
||||
x: 10,
|
||||
y: 35,
|
||||
size: 75,
|
||||
durationShift: 90,
|
||||
}, {
|
||||
x: 20,
|
||||
y: 70,
|
||||
size: 125,
|
||||
durationShift: 30,
|
||||
}, {
|
||||
x: 40,
|
||||
y: 10,
|
||||
size: 125,
|
||||
durationShift: 0,
|
||||
}, {
|
||||
x: 45,
|
||||
y: 60,
|
||||
size: 75,
|
||||
durationShift: 60,
|
||||
}, {
|
||||
x: 60,
|
||||
y: -10,
|
||||
size: 100,
|
||||
durationShift: 20,
|
||||
}, {
|
||||
x: 55,
|
||||
y: 40,
|
||||
size: 75,
|
||||
durationShift: 60,
|
||||
}, {
|
||||
x: 70,
|
||||
y: 65,
|
||||
size: 100,
|
||||
durationShift: 90,
|
||||
}, {
|
||||
x: 80,
|
||||
y: 10,
|
||||
size: 75,
|
||||
durationShift: 30,
|
||||
}, {
|
||||
x: 80,
|
||||
y: 45,
|
||||
size: 125,
|
||||
durationShift: 0,
|
||||
}];
|
||||
const PROGRESS_POSITIONS = generateRandomProgressPositions(100);
|
||||
|
||||
const Sparkles = ({
|
||||
className,
|
||||
...presetSettings
|
||||
}: OwnProps) => {
|
||||
if (presetSettings.preset === 'reaction') {
|
||||
return (
|
||||
<div className={buildClassName(styles.root, styles.reaction, className)}>
|
||||
{REACTION_POSITIONS.map((position) => {
|
||||
const shiftX = Math.cos(Math.atan2(-50 + position.y, -50 + position.x)) * 100;
|
||||
const shiftY = Math.sin(Math.atan2(-50 + position.y, -50 + position.x)) * 100;
|
||||
return (
|
||||
<div
|
||||
className={styles.symbol}
|
||||
style={buildStyle(
|
||||
`top: ${position.y}%`,
|
||||
`left: ${position.x}%`,
|
||||
`--_duration-shift: ${(-position.durationShift / 100) * ANIMATION_DURATION}s`,
|
||||
`--_shift-x: ${shiftX}%`,
|
||||
`--_shift-y: ${shiftY}%`,
|
||||
`scale: ${position.size}%`,
|
||||
)}
|
||||
>
|
||||
{SYMBOL}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (presetSettings.preset === 'progress') {
|
||||
return (
|
||||
<div className={buildClassName(styles.root, styles.progress, className)}>
|
||||
{PROGRESS_POSITIONS.map((position) => {
|
||||
return (
|
||||
<div
|
||||
className={styles.symbol}
|
||||
style={buildStyle(
|
||||
`top: ${position.y}%`,
|
||||
`left: ${position.x}%`,
|
||||
`--_shift-x: ${position.velocityX}%`,
|
||||
`--_shift-y: ${position.velocityY}%`,
|
||||
`scale: ${position.scale}%`,
|
||||
`--_duration-shift: ${(-position.durationShift / 100) * ANIMATION_DURATION}s`,
|
||||
)}
|
||||
>
|
||||
{SYMBOL}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
};
|
||||
|
||||
function generateRandomProgressPositions(count: number) {
|
||||
const positions = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
positions.push({
|
||||
x: Math.random() * 100,
|
||||
y: Math.random() * 100,
|
||||
velocityX: (Math.random() * 5 + 15) * 100,
|
||||
velocityY: (Math.random() * 10 - 5) * 100,
|
||||
scale: (Math.random() * 0.5 + 0.5) * 100,
|
||||
durationShift: Math.random() * 100,
|
||||
});
|
||||
}
|
||||
return positions;
|
||||
}
|
||||
|
||||
export default memo(Sparkles);
|
||||
@ -4,7 +4,7 @@ import React, {
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions, getGlobal } from '../../global';
|
||||
|
||||
import type { ApiAvailableReaction, ApiReaction, ApiSticker } from '../../api/types';
|
||||
import type { ApiAvailableReaction, ApiReactionWithPaid, ApiSticker } from '../../api/types';
|
||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
import type { StickerSetOrReactionsSetOrRecent } from '../../types';
|
||||
|
||||
@ -35,7 +35,7 @@ import useWindowSize from '../../hooks/window/useWindowSize';
|
||||
import Button from '../ui/Button';
|
||||
import ConfirmDialog from '../ui/ConfirmDialog';
|
||||
import Icon from './icons/Icon';
|
||||
import ReactionEmoji from './ReactionEmoji';
|
||||
import ReactionEmoji from './reactions/ReactionEmoji';
|
||||
import StickerButton from './StickerButton';
|
||||
|
||||
import grey from '../../assets/icons/forumTopic/grey.svg';
|
||||
@ -65,7 +65,8 @@ type OwnProps = {
|
||||
observeIntersectionForShowingItems: ObserveFn;
|
||||
availableReactions?: ApiAvailableReaction[];
|
||||
onStickerSelect?: (sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean) => void;
|
||||
onReactionSelect?: (reaction: ApiReaction) => void;
|
||||
onReactionSelect?: (reaction: ApiReactionWithPaid) => void;
|
||||
onReactionContext?: (reaction: ApiReactionWithPaid) => void;
|
||||
onStickerUnfave?: (sticker: ApiSticker) => void;
|
||||
onStickerFave?: (sticker: ApiSticker) => void;
|
||||
onStickerRemoveRecent?: (sticker: ApiSticker) => void;
|
||||
@ -105,6 +106,7 @@ const StickerSet: FC<OwnProps> = ({
|
||||
observeIntersectionForPlayingItems,
|
||||
observeIntersectionForShowingItems,
|
||||
onReactionSelect,
|
||||
onReactionContext,
|
||||
onStickerSelect,
|
||||
onStickerUnfave,
|
||||
onStickerFave,
|
||||
@ -351,6 +353,7 @@ const StickerSet: FC<OwnProps> = ({
|
||||
availableReactions={availableReactions}
|
||||
observeIntersection={observeIntersectionForPlayingItems}
|
||||
onClick={onReactionSelect!}
|
||||
onContextMenu={onReactionContext}
|
||||
sharedCanvasRef={sharedCanvasRef}
|
||||
sharedCanvasHqRef={sharedCanvasHqRef}
|
||||
forcePlayback={forcePlayback}
|
||||
|
||||
@ -26,6 +26,8 @@ import FoldersAll from '../../../assets/tgs/settings/FoldersAll.tgs';
|
||||
import FoldersNew from '../../../assets/tgs/settings/FoldersNew.tgs';
|
||||
import FoldersShare from '../../../assets/tgs/settings/FoldersShare.tgs';
|
||||
import Lock from '../../../assets/tgs/settings/Lock.tgs';
|
||||
import StarReaction from '../../../assets/tgs/stars/StarReaction.tgs';
|
||||
import StarReactionEffect from '../../../assets/tgs/stars/StarReactionEffect.tgs';
|
||||
import Unlock from '../../../assets/tgs/Unlock.tgs';
|
||||
|
||||
export const LOCAL_TGS_URLS = {
|
||||
@ -58,4 +60,6 @@ export const LOCAL_TGS_URLS = {
|
||||
LastSeen,
|
||||
Mention,
|
||||
Fragment,
|
||||
StarReactionEffect,
|
||||
StarReaction,
|
||||
};
|
||||
|
||||
@ -11,18 +11,19 @@ type OwnProps = {
|
||||
type?: 'gold' | 'premium' | 'regular';
|
||||
size?: 'small' | 'middle' | 'big' | 'adaptive';
|
||||
className?: string;
|
||||
style?: string;
|
||||
onClick?: VoidFunction;
|
||||
};
|
||||
|
||||
/* eslint-disable max-len */
|
||||
const STAR_PATH = 'M6.63869 12.1902L3.50621 14.1092C3.18049 14.3087 2.75468 14.2064 2.55515 13.8807C2.45769 13.7216 2.42864 13.5299 2.47457 13.3491L2.95948 11.4405C3.13452 10.7515 3.60599 10.1756 4.24682 9.86791L7.6642 8.22716C7.82352 8.15067 7.89067 7.95951 7.81418 7.80019C7.75223 7.67116 7.61214 7.59896 7.47111 7.62338L3.66713 8.28194C2.89387 8.41581 2.1009 8.20228 1.49941 7.69823L0.297703 6.69116C0.00493565 6.44581 -0.0335059 6.00958 0.211842 5.71682C0.33117 5.57442 0.502766 5.48602 0.687982 5.47153L4.35956 5.18419C4.61895 5.16389 4.845 4.99974 4.94458 4.75937L6.36101 1.3402C6.5072 0.987302 6.91179 0.819734 7.26469 0.965925C7.43413 1.03612 7.56876 1.17075 7.63896 1.3402L9.05539 4.75937C9.15496 4.99974 9.38101 5.16389 9.6404 5.18419L13.3322 5.47311C13.713 5.50291 13.9975 5.83578 13.9677 6.2166C13.9534 6.39979 13.8667 6.56975 13.7269 6.68896L10.9114 9.08928C10.7131 9.25826 10.6267 9.52425 10.6876 9.77748L11.5532 13.3733C11.6426 13.7447 11.414 14.1182 11.0427 14.2076C10.8642 14.2506 10.676 14.2208 10.5195 14.1249L7.36128 12.1902C7.13956 12.0544 6.8604 12.0544 6.63869 12.1902Z';
|
||||
const GOLD_STAR_PATH = 'M10.5197 16.2049L6.46899 18.6864C6.04779 18.9444 5.49716 18.8121 5.23913 18.3909C5.11311 18.1852 5.07554 17.9373 5.13494 17.7035L5.762 15.2354C5.98835 14.3444 6.59803 13.5997 7.42671 13.2018L11.8459 11.0801C12.0519 10.9812 12.1387 10.734 12.0398 10.528C11.9597 10.3611 11.7786 10.2677 11.5962 10.2993L6.67709 11.1509C5.67715 11.324 4.65172 11.0479 3.87392 10.3961L2.31994 9.09382C1.94135 8.77655 1.89164 8.21245 2.20891 7.83386C2.36321 7.64972 2.58511 7.53541 2.82462 7.51667L7.5725 7.1451C7.90793 7.11885 8.20025 6.90658 8.32901 6.59574L10.1607 2.17427C10.3497 1.71792 10.8729 1.50123 11.3292 1.69028C11.5484 1.78105 11.7225 1.95514 11.8132 2.17427L13.6449 6.59574C13.7736 6.90658 14.066 7.11885 14.4014 7.1451L19.1754 7.51871C19.6678 7.55725 20.0358 7.9877 19.9972 8.48015C19.9787 8.71704 19.8666 8.93682 19.6858 9.09098L16.0449 12.1949C15.7886 12.4134 15.6768 12.7574 15.7556 13.0849L16.8749 17.7348C16.9905 18.215 16.6949 18.698 16.2147 18.8137C15.9839 18.8692 15.7406 18.8307 15.5382 18.7068L11.4541 16.2049C11.1674 16.0292 10.8064 16.0292 10.5197 16.2049Z';
|
||||
/* eslint-enable max-len */
|
||||
|
||||
const StarIcon: FC<OwnProps> = ({
|
||||
type = 'regular',
|
||||
size = 'small',
|
||||
className,
|
||||
style,
|
||||
onClick,
|
||||
}) => {
|
||||
const randomId = useUniqueId();
|
||||
@ -38,6 +39,7 @@ const StarIcon: FC<OwnProps> = ({
|
||||
onClick && styles.clickable,
|
||||
styles[size],
|
||||
)}
|
||||
style={style}
|
||||
>
|
||||
{type === 'gold'
|
||||
? <GoldStarIcon randomId={validSvgRandomId} />
|
||||
@ -49,66 +51,75 @@ const StarIcon: FC<OwnProps> = ({
|
||||
};
|
||||
|
||||
function GoldStarIcon({ randomId }: { randomId: string }) {
|
||||
const fillId = `${randomId}-fill`;
|
||||
const stroke1Id = `${randomId}-stroke1`;
|
||||
const stroke2Id = `${randomId}-stroke2`;
|
||||
const mask1Id = `${randomId}-mask1`;
|
||||
const mask2Id = `${randomId}-mask2`;
|
||||
const gradient1Id = `${randomId}-gradient1`;
|
||||
const gradient2Id = `${randomId}-gradient2`;
|
||||
const gradient3Id = `${randomId}-gradient3`;
|
||||
|
||||
return (
|
||||
<svg className={styles.svg} width="21" height="20" viewBox="0 0 21 20" fill="none">
|
||||
<svg className={styles.svg} width="20" height="20" viewBox="0 0 20 20" fill="none">
|
||||
<g clip-path="url(#clip0_4913_7387)">
|
||||
<mask id={mask1Id} style="mask-type:luminance" maskUnits="userSpaceOnUse" x="-2" y="-2" width="24" height="24">
|
||||
<path d="M21.416 -1.42493H-1.08398V21.0751H21.416V-1.42493Z" fill="white" />
|
||||
</mask>
|
||||
<g mask={`url(#${mask1Id})`}>
|
||||
<mask
|
||||
id={mask2Id}
|
||||
style="mask-type:luminance"
|
||||
maskUnits="userSpaceOnUse"
|
||||
x="-2"
|
||||
y="-2"
|
||||
width="24"
|
||||
height="24"
|
||||
>
|
||||
<path d="M-1.08398 -1.42493H21.416V21.0751H-1.08398V-1.42493Z" fill="white" />
|
||||
</mask>
|
||||
<g mask={`url(#${mask2Id})`}>
|
||||
<path d="M7.26843 6.25162L9.28943 2.22541C9.52311 1.76121 10.0884 1.5749 10.5494 1.80857C10.7294 1.90015 10.8747 2.04857 10.9662 2.23172L12.8767 6.11583C13.0314 6.43477 13.3378 6.64951 13.6883 6.69056L17.6829 7.17055C18.2261 7.23686 18.6145 7.73264 18.5513 8.27894C18.5229 8.50314 18.4218 8.71156 18.2608 8.86945L15.0998 11.9862C14.9703 12.1125 14.9103 12.2894 14.9324 12.4694L15.4598 16.6756C15.5356 17.2787 15.1093 17.8282 14.5093 17.904C14.2819 17.9324 14.0546 17.8913 13.8525 17.7808L10.5147 15.9556C10.2715 15.823 9.98099 15.8198 9.73784 15.9461L6.27687 17.7208C5.79057 17.9703 5.1969 17.7745 4.94743 17.285C4.8527 17.1019 4.82112 16.8966 4.84954 16.6945L5.12427 14.7619C5.26006 13.8177 5.84425 12.9967 6.69055 12.5641L10.5305 10.6031C10.6315 10.5526 10.6726 10.4263 10.622 10.322C10.581 10.2431 10.4957 10.1957 10.4073 10.2084L5.70847 10.8841C4.99164 10.9852 4.26535 10.7831 3.7001 10.322L2.13698 9.04629C1.69173 8.68314 1.6191 8.02 1.98225 7.57159C2.15277 7.36317 2.39592 7.22739 2.66118 7.19265L6.6716 6.67793C6.92739 6.64319 7.15159 6.4853 7.26843 6.25162Z" fill={`url(#${gradient1Id})`} />
|
||||
<path d="M10.8242 2.9422C10.4168 2.85062 9.98417 3.0464 9.78839 3.43797L7.76423 7.46419C7.64739 7.69787 7.42634 7.85576 7.1674 7.89049L3.15698 8.40837C2.89804 8.44311 2.6612 8.56942 2.49384 8.76837L3.97801 9.98097C4.44537 10.3631 5.05167 10.5304 5.64849 10.4452L10.3442 9.77255C10.6221 9.73466 10.8936 9.8736 11.0168 10.1231C11.1778 10.442 11.0515 10.8336 10.7326 10.9978L9.50734 11.623L10.9031 11.4209C10.9915 11.4083 11.0799 11.4557 11.1178 11.5346C11.1715 11.6388 11.1273 11.7651 11.0263 11.8157L7.18635 13.7767C6.34006 14.2093 5.75586 15.0303 5.62007 15.9745L5.44008 17.2282C5.60428 17.3924 5.86322 17.4366 6.07796 17.326L9.53261 15.5514C9.90839 15.3587 10.3536 15.365 10.7231 15.5671L14.0609 17.3924C14.1809 17.4587 14.3199 17.4839 14.4556 17.4682C14.8093 17.4239 15.0651 17.0955 15.0209 16.7324L14.4967 12.523C14.4588 12.2072 14.5662 11.8946 14.7904 11.6736L17.9513 8.55679C18.005 8.50311 18.0461 8.43995 18.0745 8.37048L14.1841 7.90312C13.8336 7.86207 13.5304 7.64418 13.3725 7.3284L13.2209 7.01578C12.902 6.87683 12.6399 6.63052 12.482 6.31159L10.8242 2.9422Z" fill={`url(#${gradient2Id})`} />
|
||||
<path d="M10.7484 1.41397C10.0663 1.06977 9.23893 1.3445 8.89789 2.02659L6.87373 6.05596C6.8232 6.15701 6.72531 6.22649 6.61478 6.23912L2.60436 6.757C2.22542 6.80437 1.87806 7.00015 1.63807 7.29699C1.12334 7.93486 1.22124 8.86958 1.85596 9.38746L3.42223 10.6664C4.08222 11.2032 4.93167 11.44 5.77165 11.32L9.09051 10.8432L6.48847 12.1727C5.51271 12.6716 4.84325 13.6126 4.68852 14.6989L4.41063 16.6315C4.36958 16.9252 4.41695 17.222 4.55273 17.4873C4.90956 18.1915 5.77481 18.4725 6.479 18.1125L9.93681 16.3378C10.0505 16.2778 10.1863 16.281 10.3 16.3441L13.6378 18.1662C13.9188 18.3209 14.2441 18.3841 14.563 18.343C15.4061 18.2357 15.9998 17.462 15.8956 16.622L15.3714 12.4126C15.3651 12.3716 15.3809 12.3274 15.4093 12.299L18.5703 9.1822C18.8008 8.95168 18.9492 8.65169 18.9871 8.32959C19.0787 7.5433 18.5229 6.82963 17.7366 6.73489L13.7388 6.25491C13.5399 6.2328 13.363 6.10649 13.2715 5.92334L11.3641 2.03606C11.2315 1.76765 11.0136 1.54976 10.7484 1.41397ZM9.68418 2.42132C9.80734 2.17501 10.1073 2.07712 10.3505 2.20027C10.4452 2.24764 10.5242 2.32974 10.5715 2.42763L12.482 6.31175C12.7031 6.76332 13.1357 7.06962 13.6346 7.12962L17.6324 7.60961C17.9324 7.64435 18.1471 7.91908 18.1124 8.22538C18.0966 8.3517 18.0398 8.46854 17.9513 8.55695L14.7904 11.6737C14.5662 11.8948 14.4588 12.2074 14.4967 12.5232L15.0209 16.7325C15.0651 17.0957 14.8093 17.4241 14.4556 17.4683C14.3199 17.4841 14.1809 17.4588 14.0609 17.3925L10.7231 15.5673C10.3536 15.3652 9.90839 15.362 9.53261 15.5547L6.0748 17.3294C5.80638 17.4652 5.47797 17.3578 5.33903 17.0862C5.2885 16.9852 5.26956 16.8683 5.28534 16.7546L5.56007 14.8252C5.67376 14.0231 6.17269 13.3253 6.88952 12.959L10.7294 10.9979C11.0515 10.8337 11.1778 10.4422 11.0168 10.1232C10.8905 9.87376 10.6221 9.73482 10.3442 9.77271L5.64534 10.4453C5.04851 10.5306 4.44537 10.3632 3.97801 9.98113L2.41489 8.70221C2.15595 8.49064 2.1149 8.10854 2.32647 7.8496C2.42436 7.72961 2.56331 7.65066 2.71488 7.63171L6.72531 7.11383C7.12951 7.06331 7.48003 6.81384 7.66318 6.45069L9.68418 2.42132Z" fill={`url(#${gradient3Id})`} />
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
<defs>
|
||||
<linearGradient
|
||||
id={fillId}
|
||||
x1="0.434893"
|
||||
y1="22.5796"
|
||||
x2="34.2364"
|
||||
y2="-15.5089"
|
||||
id={gradient1Id}
|
||||
x1="10.1547"
|
||||
y1="1.70752"
|
||||
x2="10.1547"
|
||||
y2="17.9134"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#FDEB32" />
|
||||
<stop offset="0.439058" stop-color="#FEBD04" />
|
||||
<stop offset="1" stop-color="#D75902" />
|
||||
<stop stop-color="#FFF0C2" />
|
||||
<stop offset="1" stop-color="#FFEBBA" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id={stroke1Id}
|
||||
x1="22.5"
|
||||
y1="2.5"
|
||||
x2="8"
|
||||
y2="12.5"
|
||||
id={gradient2Id}
|
||||
x1="10.2842"
|
||||
y1="2.92009"
|
||||
x2="14.7304"
|
||||
y2="17.4713"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="#DB5A00" />
|
||||
<stop offset="1" stop-color="#FF9145" />
|
||||
<stop stop-color="#FFD147" />
|
||||
<stop offset="1" stop-color="#FFB526" />
|
||||
</linearGradient>
|
||||
<linearGradient
|
||||
id={stroke2Id}
|
||||
x1="24.5"
|
||||
y1="2"
|
||||
x2="11"
|
||||
y2="10.2302"
|
||||
id={gradient3Id}
|
||||
x1="10.1547"
|
||||
y1="1.26556"
|
||||
x2="14.0546"
|
||||
y2="18.3525"
|
||||
gradientUnits="userSpaceOnUse"
|
||||
>
|
||||
<stop stop-color="white" stop-opacity="0" />
|
||||
<stop offset="0.395833" stop-color="white" stop-opacity="0.85" />
|
||||
<stop offset="0.520833" stop-color="white" />
|
||||
<stop offset="0.645833" stop-color="white" stop-opacity="0.85" />
|
||||
<stop offset="1" stop-color="white" stop-opacity="0" />
|
||||
<stop stop-color="#E58F0D" />
|
||||
<stop offset="1" stop-color="#EB7814" />
|
||||
</linearGradient>
|
||||
<clipPath id="clip0_4913_7387">
|
||||
<rect width="20" height="20" fill="white" />
|
||||
</clipPath>
|
||||
</defs>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d={GOLD_STAR_PATH}
|
||||
fill={`url(#${fillId})`}
|
||||
stroke={`url(#${stroke1Id})`}
|
||||
/>
|
||||
<path
|
||||
fill-rule="evenodd"
|
||||
clip-rule="evenodd"
|
||||
d={GOLD_STAR_PATH}
|
||||
stroke={`url(#${stroke2Id})`}
|
||||
stroke-width="2"
|
||||
style="mix-blend-mode:soft-light"
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
148
src/components/common/reactions/PaidReactionEmoji.tsx
Normal file
148
src/components/common/reactions/PaidReactionEmoji.tsx
Normal file
@ -0,0 +1,148 @@
|
||||
import React, {
|
||||
memo, useMemo, useRef, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiReaction, ApiReactionPaid } from '../../../api/types';
|
||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
|
||||
import { isSameReaction } from '../../../global/helpers';
|
||||
import { selectPerformanceSettingsValue, selectTabState } from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { IS_ANDROID, IS_IOS } from '../../../util/windowEnvironment';
|
||||
import { LOCAL_TGS_URLS } from '../helpers/animatedAssets';
|
||||
import { REM } from '../helpers/mediaDimensions';
|
||||
|
||||
import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps';
|
||||
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useShowTransition from '../../../hooks/useShowTransition';
|
||||
|
||||
import AnimatedIcon from '../AnimatedIcon';
|
||||
import StarIcon from '../icons/StarIcon';
|
||||
|
||||
import styles from './ReactionAnimatedEmoji.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
containerId: string;
|
||||
reaction: ApiReactionPaid;
|
||||
className?: string;
|
||||
size?: number;
|
||||
effectSize?: number;
|
||||
localAmount?: number;
|
||||
observeIntersection?: ObserveFn;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
activeReactions?: ApiReaction[];
|
||||
withEffects?: boolean;
|
||||
};
|
||||
|
||||
const ICON_SIZE = 1.5 * REM;
|
||||
const EFFECT_SIZE = 6.5 * REM;
|
||||
const MAX_EFFECT_COUNT = (IS_IOS || IS_ANDROID) ? 2 : 5;
|
||||
const QUALITY = (IS_IOS || IS_ANDROID) ? 2 : 3;
|
||||
|
||||
const PaidReactionEmoji = ({
|
||||
containerId,
|
||||
reaction,
|
||||
className,
|
||||
size = ICON_SIZE,
|
||||
effectSize = EFFECT_SIZE,
|
||||
activeReactions,
|
||||
localAmount,
|
||||
withEffects,
|
||||
observeIntersection,
|
||||
}: OwnProps & StateProps) => {
|
||||
const { stopActiveReaction } = getActions();
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const effectRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [effectsIds, setEffectsIds] = useState<number[]>([]);
|
||||
|
||||
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
||||
|
||||
const activeReaction = useMemo(() => (
|
||||
activeReactions?.find((active) => isSameReaction(active, reaction))
|
||||
), [activeReactions, reaction]);
|
||||
|
||||
const shouldPlayEffect = Boolean(
|
||||
withEffects && activeReaction,
|
||||
);
|
||||
const canAddMoreEffects = effectsIds.length < MAX_EFFECT_COUNT;
|
||||
|
||||
useEffectWithPrevDeps(([prevLocalAmount]) => {
|
||||
if (!shouldPlayEffect) {
|
||||
setEffectsIds([]);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!localAmount || localAmount <= (prevLocalAmount || 0)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (canAddMoreEffects) {
|
||||
setEffectsIds((prev) => [...prev, Date.now()]);
|
||||
}
|
||||
}, [localAmount, canAddMoreEffects, shouldPlayEffect]);
|
||||
|
||||
const {
|
||||
shouldRender: shouldRenderEffect,
|
||||
} = useShowTransition({
|
||||
ref: effectRef,
|
||||
noMountTransition: true,
|
||||
isOpen: shouldPlayEffect,
|
||||
className: 'slow',
|
||||
withShouldRender: true,
|
||||
});
|
||||
|
||||
const handleEnded = useLastCallback(() => {
|
||||
const newEffectsIds = effectsIds.slice(1);
|
||||
setEffectsIds(newEffectsIds);
|
||||
if (!newEffectsIds.length) {
|
||||
stopActiveReaction({ containerId, reaction });
|
||||
}
|
||||
});
|
||||
|
||||
const rootClassName = buildClassName(
|
||||
styles.root,
|
||||
shouldRenderEffect && styles.animating,
|
||||
className,
|
||||
);
|
||||
|
||||
return (
|
||||
<div className={rootClassName} ref={ref} teactFastList>
|
||||
<StarIcon key="icon" type="gold" size="adaptive" style={`width: ${size}px; height: ${size}px`} />
|
||||
{shouldRenderEffect && effectsIds.map((id) => (
|
||||
<AnimatedIcon
|
||||
key={id}
|
||||
ref={effectRef}
|
||||
className={styles.effect}
|
||||
size={effectSize}
|
||||
tgsUrl={LOCAL_TGS_URLS.StarReactionEffect}
|
||||
play={isIntersecting}
|
||||
noLoop
|
||||
nonInteractive
|
||||
quality={QUALITY}
|
||||
onEnded={handleEnded}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { containerId }) => {
|
||||
const { activeReactions } = selectTabState(global);
|
||||
|
||||
const withEffects = selectPerformanceSettingsValue(global, 'reactionEffects');
|
||||
|
||||
return {
|
||||
activeReactions: activeReactions?.[containerId],
|
||||
withEffects,
|
||||
};
|
||||
},
|
||||
)(PaidReactionEmoji));
|
||||
@ -3,7 +3,11 @@ import React, {
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiAvailableReaction, ApiReaction, ApiStickerSet } from '../../../api/types';
|
||||
import type {
|
||||
ApiAvailableReaction,
|
||||
ApiReaction,
|
||||
ApiStickerSet,
|
||||
} from '../../../api/types';
|
||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
|
||||
import { isSameReaction } from '../../../global/helpers';
|
||||
@ -21,8 +25,8 @@ import useCustomEmoji from '../hooks/useCustomEmoji';
|
||||
|
||||
import AnimatedSticker from '../AnimatedSticker';
|
||||
import CustomEmoji from '../CustomEmoji';
|
||||
import ReactionStaticEmoji from '../ReactionStaticEmoji';
|
||||
import CustomEmojiEffect from './CustomEmojiEffect';
|
||||
import ReactionStaticEmoji from './ReactionStaticEmoji';
|
||||
|
||||
import styles from './ReactionAnimatedEmoji.module.scss';
|
||||
|
||||
@ -73,7 +77,7 @@ const ReactionAnimatedEmoji = ({
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const isCustom = 'documentId' in reaction;
|
||||
const isCustom = reaction.type === 'custom';
|
||||
|
||||
const availableReaction = useMemo(() => (
|
||||
availableReactions?.find((r) => isSameReaction(r.reaction, reaction))
|
||||
|
||||
@ -1,27 +1,29 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useMemo, useRef,
|
||||
} from '../../lib/teact/teact';
|
||||
memo, useEffect, useMemo, useRef,
|
||||
} from '../../../lib/teact/teact';
|
||||
|
||||
import type { ApiAvailableReaction, ApiReaction } from '../../api/types';
|
||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
import type { ApiAvailableReaction, ApiReactionWithPaid } from '../../../api/types';
|
||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
|
||||
import { EMOJI_SIZE_PICKER } from '../../config';
|
||||
import { getDocumentMediaHash, isSameReaction } from '../../global/helpers';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { EMOJI_SIZE_PICKER } from '../../../config';
|
||||
import { getDocumentMediaHash, isSameReaction } from '../../../global/helpers';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { LOCAL_TGS_URLS } from '../helpers/animatedAssets';
|
||||
|
||||
import useCoordsInSharedCanvas from '../../hooks/useCoordsInSharedCanvas';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useMedia from '../../hooks/useMedia';
|
||||
import useMediaTransitionDeprecated from '../../hooks/useMediaTransitionDeprecated';
|
||||
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
||||
import useCoordsInSharedCanvas from '../../../hooks/useCoordsInSharedCanvas';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useMedia from '../../../hooks/useMedia';
|
||||
import useMediaTransitionDeprecated from '../../../hooks/useMediaTransitionDeprecated';
|
||||
|
||||
import AnimatedIconWithPreview from './AnimatedIconWithPreview';
|
||||
import CustomEmoji from './CustomEmoji';
|
||||
import AnimatedIconWithPreview from '../AnimatedIconWithPreview';
|
||||
import CustomEmoji from '../CustomEmoji';
|
||||
|
||||
import styles from './ReactionEmoji.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
reaction: ApiReaction;
|
||||
reaction: ApiReactionWithPaid;
|
||||
availableReactions?: ApiAvailableReaction[];
|
||||
className?: string;
|
||||
isSelected?: boolean;
|
||||
@ -30,7 +32,8 @@ type OwnProps = {
|
||||
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>;
|
||||
sharedCanvasHqRef?: React.RefObject<HTMLCanvasElement>;
|
||||
forcePlayback?: boolean;
|
||||
onClick: (reaction: ApiReaction) => void;
|
||||
onClick: (reaction: ApiReactionWithPaid) => void;
|
||||
onContextMenu?: (reaction: ApiReactionWithPaid) => void;
|
||||
};
|
||||
|
||||
const ReactionEmoji: FC<OwnProps> = ({
|
||||
@ -43,10 +46,11 @@ const ReactionEmoji: FC<OwnProps> = ({
|
||||
sharedCanvasHqRef,
|
||||
forcePlayback,
|
||||
onClick,
|
||||
onContextMenu,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const isCustom = 'documentId' in reaction;
|
||||
const isCustom = reaction.type === 'custom';
|
||||
const availableReaction = useMemo(() => (
|
||||
availableReactions?.find((available) => isSameReaction(available.reaction, reaction))
|
||||
), [availableReactions, reaction]);
|
||||
@ -57,6 +61,25 @@ const ReactionEmoji: FC<OwnProps> = ({
|
||||
availableReaction?.selectAnimation ? getDocumentMediaHash(availableReaction.selectAnimation, 'full') : undefined,
|
||||
!animationId,
|
||||
);
|
||||
|
||||
const {
|
||||
isContextMenuOpen,
|
||||
handleBeforeContextMenu,
|
||||
handleContextMenu,
|
||||
handleContextMenuClose,
|
||||
handleContextMenuHide,
|
||||
} = useContextMenuHandlers(ref, reaction.type !== 'paid', undefined, undefined, undefined, true);
|
||||
|
||||
useEffect(() => {
|
||||
if (isContextMenuOpen) {
|
||||
onContextMenu?.(reaction);
|
||||
|
||||
handleContextMenuClose();
|
||||
handleContextMenuHide();
|
||||
}
|
||||
}, [handleContextMenuClose, onContextMenu, handleContextMenuHide, isContextMenuOpen, reaction]);
|
||||
|
||||
const tgsUrl = reaction.type === 'paid' ? LOCAL_TGS_URLS.StarReaction : mediaData;
|
||||
const handleClick = useLastCallback(() => {
|
||||
onClick(reaction);
|
||||
});
|
||||
@ -75,6 +98,8 @@ const ReactionEmoji: FC<OwnProps> = ({
|
||||
onClick={handleClick}
|
||||
title={availableReaction?.title}
|
||||
data-sticker-id={isCustom ? reaction.documentId : undefined}
|
||||
onMouseDown={handleBeforeContextMenu}
|
||||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
{isCustom ? (
|
||||
<CustomEmoji
|
||||
@ -90,7 +115,7 @@ const ReactionEmoji: FC<OwnProps> = ({
|
||||
/>
|
||||
) : (
|
||||
<AnimatedIconWithPreview
|
||||
tgsUrl={mediaData}
|
||||
tgsUrl={tgsUrl}
|
||||
thumbDataUri={thumbDataUri}
|
||||
play={loadAndPlay}
|
||||
noLoop={false}
|
||||
@ -1,21 +1,20 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, { memo, useMemo } from '../../lib/teact/teact';
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { memo, useMemo } from '../../../lib/teact/teact';
|
||||
|
||||
import type { ApiAvailableReaction, ApiReaction } from '../../api/types';
|
||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
import { ApiMediaFormat } from '../../api/types';
|
||||
import type { ApiAvailableReaction, ApiReaction } from '../../../api/types';
|
||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
|
||||
import { isSameReaction } from '../../global/helpers';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { isSameReaction } from '../../../global/helpers';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
|
||||
import useMedia from '../../hooks/useMedia';
|
||||
import useMediaTransitionDeprecated from '../../hooks/useMediaTransitionDeprecated';
|
||||
import useMedia from '../../../hooks/useMedia';
|
||||
import useMediaTransitionDeprecated from '../../../hooks/useMediaTransitionDeprecated';
|
||||
|
||||
import CustomEmoji from './CustomEmoji';
|
||||
import CustomEmoji from '../CustomEmoji';
|
||||
|
||||
import './ReactionStaticEmoji.scss';
|
||||
|
||||
import blankUrl from '../../assets/blank.png';
|
||||
import blankUrl from '../../../assets/blank.png';
|
||||
|
||||
type OwnProps = {
|
||||
reaction: ApiReaction;
|
||||
@ -34,19 +33,19 @@ const ReactionStaticEmoji: FC<OwnProps> = ({
|
||||
withIconHeart,
|
||||
observeIntersection,
|
||||
}) => {
|
||||
const isCustom = 'documentId' in reaction;
|
||||
const availableReaction = useMemo(() => (
|
||||
availableReactions?.find((available) => isSameReaction(available.reaction, reaction))
|
||||
), [availableReactions, reaction]);
|
||||
const staticIconId = availableReaction?.staticIcon?.id;
|
||||
const mediaData = useMedia(`document${staticIconId}`, !staticIconId, ApiMediaFormat.BlobUrl);
|
||||
const mediaHash = staticIconId ? `document${staticIconId}` : undefined;
|
||||
const mediaData = useMedia(mediaHash);
|
||||
|
||||
const transitionClassNames = useMediaTransitionDeprecated(mediaData);
|
||||
|
||||
const shouldApplySizeFix = 'emoticon' in reaction && reaction.emoticon === '🦄';
|
||||
const shouldReplaceWithHeartIcon = withIconHeart && 'emoticon' in reaction && reaction.emoticon === '❤';
|
||||
const shouldApplySizeFix = reaction.type === 'emoji' && reaction.emoticon === '🦄';
|
||||
const shouldReplaceWithHeartIcon = withIconHeart && reaction.type === 'emoji' && reaction.emoticon === '❤';
|
||||
|
||||
if (isCustom) {
|
||||
if (reaction.type === 'custom') {
|
||||
return (
|
||||
<CustomEmoji
|
||||
documentId={reaction.documentId}
|
||||
@ -6,7 +6,7 @@ import type { ApiAvailableReaction } from '../../../api/types';
|
||||
|
||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
|
||||
import ReactionStaticEmoji from '../../common/ReactionStaticEmoji';
|
||||
import ReactionStaticEmoji from '../../common/reactions/ReactionStaticEmoji';
|
||||
import RadioGroup from '../../ui/RadioGroup';
|
||||
|
||||
type OwnProps = {
|
||||
@ -47,7 +47,7 @@ const SettingsQuickReaction: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const handleChange = useCallback((reaction: string) => {
|
||||
setDefaultReaction({
|
||||
reaction: { emoticon: reaction },
|
||||
reaction: { type: 'emoji', emoticon: reaction },
|
||||
});
|
||||
}, [setDefaultReaction]);
|
||||
|
||||
|
||||
@ -22,7 +22,7 @@ import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import ReactionStaticEmoji from '../../common/ReactionStaticEmoji';
|
||||
import ReactionStaticEmoji from '../../common/reactions/ReactionStaticEmoji';
|
||||
import StickerSetCard from '../../common/StickerSetCard';
|
||||
import Checkbox from '../../ui/Checkbox';
|
||||
import ListItem from '../../ui/ListItem';
|
||||
|
||||
@ -247,6 +247,7 @@ const Main = ({
|
||||
loadStarStatus,
|
||||
loadAvailableEffects,
|
||||
loadTopBotApps,
|
||||
loadPaidReactionPrivacy,
|
||||
} = getActions();
|
||||
|
||||
if (DEBUG && !DEBUG_isLogged) {
|
||||
@ -330,6 +331,7 @@ const Main = ({
|
||||
loadSavedReactionTags();
|
||||
loadAuthorizations();
|
||||
loadTopBotApps();
|
||||
loadPaidReactionPrivacy();
|
||||
}
|
||||
}, [isMasterTab, isSynced]);
|
||||
|
||||
|
||||
@ -23,18 +23,23 @@ const Notifications: FC<StateProps> = ({ notifications }) => {
|
||||
|
||||
return (
|
||||
<div id="Notifications">
|
||||
{notifications.map(({
|
||||
message, className, localId, action, actionText, title, duration,
|
||||
}) => (
|
||||
{notifications.map((notification) => (
|
||||
<Notification
|
||||
title={title ? renderText(title, ['simple_markdown', 'emoji', 'br', 'links']) : undefined}
|
||||
action={action}
|
||||
actionText={actionText}
|
||||
className={className}
|
||||
duration={duration}
|
||||
message={renderText(message, ['simple_markdown', 'emoji', 'br', 'links'])}
|
||||
key={notification.localId}
|
||||
title={notification.title
|
||||
? renderText(notification.title, ['simple_markdown', 'emoji', 'br', 'links']) : undefined}
|
||||
action={notification.action}
|
||||
actionText={notification.actionText}
|
||||
className={notification.className}
|
||||
duration={notification.duration}
|
||||
icon={notification.icon}
|
||||
cacheBreaker={notification.cacheBreaker}
|
||||
message={renderText(notification.message, ['simple_markdown', 'emoji', 'br', 'links'])}
|
||||
shouldDisableClickDismiss={notification.disableClickDismiss}
|
||||
dismissAction={notification.dismissAction}
|
||||
shouldShowTimer={notification.shouldShowTimer}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onDismiss={() => dismissNotification({ localId })}
|
||||
onDismiss={() => dismissNotification({ localId: notification.localId })}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
@ -26,7 +26,7 @@ import useOldLang from '../../hooks/useOldLang';
|
||||
import Avatar from '../common/Avatar';
|
||||
import FullNameTitle from '../common/FullNameTitle';
|
||||
import PrivateChatInfo from '../common/PrivateChatInfo';
|
||||
import ReactionStaticEmoji from '../common/ReactionStaticEmoji';
|
||||
import ReactionStaticEmoji from '../common/reactions/ReactionStaticEmoji';
|
||||
import Button from '../ui/Button';
|
||||
import InfiniteScroll from '../ui/InfiniteScroll';
|
||||
import ListItem from '../ui/ListItem';
|
||||
|
||||
@ -134,6 +134,7 @@ type StateProps = {
|
||||
isInSavedMessages?: boolean;
|
||||
isChannel?: boolean;
|
||||
canReplyInChat?: boolean;
|
||||
isWithPaidReaction?: boolean;
|
||||
};
|
||||
|
||||
const selection = window.getSelection();
|
||||
@ -192,9 +193,10 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
canSelectLanguage,
|
||||
isReactionPickerOpen,
|
||||
isInSavedMessages,
|
||||
canReplyInChat,
|
||||
isWithPaidReaction,
|
||||
onClose,
|
||||
onCloseAnimationEnd,
|
||||
canReplyInChat,
|
||||
}) => {
|
||||
const {
|
||||
openThread,
|
||||
@ -229,6 +231,8 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
loadOutboxReadDate,
|
||||
copyMessageLink,
|
||||
openDeleteMessageModal,
|
||||
addLocalPaidReaction,
|
||||
openPaidReactionModal,
|
||||
} = getActions();
|
||||
|
||||
const lang = useOldLang();
|
||||
@ -531,6 +535,22 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
closeMenu();
|
||||
});
|
||||
|
||||
const handleSendPaidReaction = useLastCallback(() => {
|
||||
addLocalPaidReaction({
|
||||
chatId: message.chatId, messageId: message.id, count: 1,
|
||||
});
|
||||
closeMenu();
|
||||
});
|
||||
|
||||
const handlePaidReactionModalOpen = useLastCallback(() => {
|
||||
openPaidReactionModal({
|
||||
chatId: message.chatId,
|
||||
messageId: message.id,
|
||||
});
|
||||
|
||||
closeMenu();
|
||||
});
|
||||
|
||||
const handleReactionPickerOpen = useLastCallback((position: IAnchorPosition) => {
|
||||
openMessageReactionPicker({ chatId: message.chatId, messageId: message.id, position });
|
||||
});
|
||||
@ -577,6 +597,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
availableReactions={availableReactions}
|
||||
topReactions={topReactions}
|
||||
defaultTagReactions={defaultTagReactions}
|
||||
isWithPaidReaction={isWithPaidReaction}
|
||||
message={message}
|
||||
isPrivate={isPrivate}
|
||||
isCurrentUserPremium={isCurrentUserPremium}
|
||||
@ -644,6 +665,8 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
onClosePoll={openClosePollDialog}
|
||||
onShowSeenBy={handleOpenSeenByModal}
|
||||
onToggleReaction={handleToggleReaction}
|
||||
onSendPaidReaction={handleSendPaidReaction}
|
||||
onShowPaidReactionModal={handlePaidReactionModalOpen}
|
||||
onShowReactors={handleOpenReactorListModal}
|
||||
onReactionPickerOpen={handleReactionPickerOpen}
|
||||
onTranslate={handleTranslate}
|
||||
@ -821,6 +844,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
isInSavedMessages,
|
||||
isChannel,
|
||||
canReplyInChat,
|
||||
isWithPaidReaction: chatFullInfo?.isPaidReactionAvailable,
|
||||
};
|
||||
},
|
||||
)(ContextMenuContainer));
|
||||
|
||||
@ -153,7 +153,7 @@ import FakeIcon from '../../common/FakeIcon';
|
||||
import Icon from '../../common/icons/Icon';
|
||||
import StarIcon from '../../common/icons/StarIcon';
|
||||
import MessageText from '../../common/MessageText';
|
||||
import ReactionStaticEmoji from '../../common/ReactionStaticEmoji';
|
||||
import ReactionStaticEmoji from '../../common/reactions/ReactionStaticEmoji';
|
||||
import TopicChip from '../../common/TopicChip';
|
||||
import Button from '../../ui/Button';
|
||||
import Album from './Album';
|
||||
@ -294,6 +294,7 @@ type StateProps = {
|
||||
canTranscribeVoice?: boolean;
|
||||
viaBusinessBot?: ApiUser;
|
||||
effect?: ApiAvailableEffect;
|
||||
availableStars?: number;
|
||||
};
|
||||
|
||||
type MetaPosition =
|
||||
@ -414,6 +415,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
canTranscribeVoice,
|
||||
viaBusinessBot,
|
||||
effect,
|
||||
availableStars,
|
||||
onIntersectPinnedMessage,
|
||||
}) => {
|
||||
const {
|
||||
@ -1042,6 +1044,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
noRecentReactors={isChannel}
|
||||
tags={tags}
|
||||
isCurrentUserPremium={isPremium}
|
||||
availableStars={availableStars}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -1649,6 +1652,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
observeIntersection={observeIntersectionForPlaying}
|
||||
noRecentReactors={isChannel}
|
||||
tags={tags}
|
||||
availableStars={availableStars}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@ -1798,6 +1802,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
|
||||
const effect = effectId ? global.availableEffectById[effectId] : undefined;
|
||||
|
||||
const { balance: availableStars } = global.stars || {};
|
||||
|
||||
return {
|
||||
theme: selectTheme(global),
|
||||
forceSenderName,
|
||||
@ -1884,6 +1890,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
canTranscribeVoice,
|
||||
viaBusinessBot,
|
||||
effect,
|
||||
availableStars,
|
||||
};
|
||||
},
|
||||
)(Message));
|
||||
|
||||
@ -51,6 +51,7 @@ type OwnProps = {
|
||||
message: ApiMessage | ApiSponsoredMessage;
|
||||
canSendNow?: boolean;
|
||||
enabledReactions?: ApiChatReactions;
|
||||
isWithPaidReaction?: boolean;
|
||||
reactionsLimit?: number;
|
||||
canReschedule?: boolean;
|
||||
canReply?: boolean;
|
||||
@ -121,6 +122,8 @@ type OwnProps = {
|
||||
onShowOriginal?: NoneToVoidFunction;
|
||||
onSelectLanguage?: NoneToVoidFunction;
|
||||
onToggleReaction?: (reaction: ApiReaction) => void;
|
||||
onSendPaidReaction?: NoneToVoidFunction;
|
||||
onShowPaidReactionModal?: NoneToVoidFunction;
|
||||
onReactionPickerOpen?: (position: IAnchorPosition) => void;
|
||||
};
|
||||
|
||||
@ -138,6 +141,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
isPrivate,
|
||||
isCurrentUserPremium,
|
||||
enabledReactions,
|
||||
isWithPaidReaction,
|
||||
reactionsLimit,
|
||||
anchor,
|
||||
targetHref,
|
||||
@ -201,6 +205,8 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
onShowSeenBy,
|
||||
onShowReactors,
|
||||
onToggleReaction,
|
||||
onSendPaidReaction,
|
||||
onShowPaidReactionModal,
|
||||
onCopyMessages,
|
||||
onAboutAdsClick,
|
||||
onSponsoredHide,
|
||||
@ -356,6 +362,9 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
currentReactions={!isSponsoredMessage ? message.reactions?.results : undefined}
|
||||
reactionsLimit={reactionsLimit}
|
||||
onToggleReaction={onToggleReaction!}
|
||||
onSendPaidReaction={onSendPaidReaction}
|
||||
onShowPaidReactionModal={onShowPaidReactionModal}
|
||||
isWithPaidReaction={isWithPaidReaction}
|
||||
isPrivate={isPrivate}
|
||||
isReady={isReady}
|
||||
canBuyPremium={canBuyPremium}
|
||||
|
||||
@ -11,6 +11,18 @@
|
||||
--reaction-text-color: var(--text-color-reaction-chosen);
|
||||
}
|
||||
|
||||
&.paid {
|
||||
--reaction-background: #FFBC2E33 !important;
|
||||
--reaction-background-hover: #FFBC2E55 !important;
|
||||
--reaction-text-color: #E98111 !important;
|
||||
}
|
||||
|
||||
&.paid.chosen {
|
||||
--reaction-background: #FFBC2E !important;
|
||||
--reaction-background-hover: #FFBC2ECC !important;
|
||||
--reaction-text-color: #FFFFFF !important;
|
||||
}
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
height: 1.875rem;
|
||||
@ -114,3 +126,17 @@
|
||||
.disabled {
|
||||
opacity: 0.5;
|
||||
}
|
||||
|
||||
.paidCounter {
|
||||
font-family: var(--font-family-rounded);
|
||||
font-size: 2.5rem;
|
||||
font-variant-numeric: tabular-nums;
|
||||
color: #FFBC2E;
|
||||
|
||||
position: absolute;
|
||||
top: -150%;
|
||||
right: 50%;
|
||||
transform: translateX(50%);
|
||||
-webkit-text-stroke: 1px #E58E0D;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import React, { memo } from '../../../../lib/teact/teact';
|
||||
import React, { memo, useEffect, useRef } from '../../../../lib/teact/teact';
|
||||
import { getActions } from '../../../../global';
|
||||
|
||||
import type {
|
||||
ApiPeer, ApiReaction, ApiReactionCount,
|
||||
} from '../../../../api/types';
|
||||
import type { GlobalState } from '../../../../global/types';
|
||||
import type { ObserveFn } from '../../../../hooks/useIntersectionObserver';
|
||||
|
||||
import { isReactionChosen } from '../../../../global/helpers';
|
||||
@ -10,28 +12,44 @@ import buildClassName from '../../../../util/buildClassName';
|
||||
import { formatIntegerCompact } from '../../../../util/textFormat';
|
||||
import { REM } from '../../../common/helpers/mediaDimensions';
|
||||
|
||||
import useSelector from '../../../../hooks/data/useSelector';
|
||||
import useContextMenuHandlers from '../../../../hooks/useContextMenuHandlers';
|
||||
import useEffectWithPrevDeps from '../../../../hooks/useEffectWithPrevDeps';
|
||||
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||
import usePrevious from '../../../../hooks/usePrevious';
|
||||
import useShowTransition from '../../../../hooks/useShowTransition';
|
||||
|
||||
import AnimatedCounter from '../../../common/AnimatedCounter';
|
||||
import AvatarList from '../../../common/AvatarList';
|
||||
import PaidReactionEmoji from '../../../common/reactions/PaidReactionEmoji';
|
||||
import ReactionAnimatedEmoji from '../../../common/reactions/ReactionAnimatedEmoji';
|
||||
import Sparkles from '../../../common/Sparkles';
|
||||
import Button from '../../../ui/Button';
|
||||
|
||||
import styles from './ReactionButton.module.scss';
|
||||
|
||||
const REACTION_SIZE = 1.25 * REM;
|
||||
const MAX_SCALE = 3;
|
||||
|
||||
type OwnProps = {
|
||||
chatId: string;
|
||||
messageId: number;
|
||||
reaction: ApiReactionCount;
|
||||
containerId: string;
|
||||
isOwnMessage?: boolean;
|
||||
recentReactors?: ApiPeer[];
|
||||
className?: string;
|
||||
chosenClassName?: string;
|
||||
availableStars?: number;
|
||||
observeIntersection?: ObserveFn;
|
||||
onClick?: (reaction: ApiReaction) => void;
|
||||
onPaidClick?: (count: number) => void;
|
||||
};
|
||||
|
||||
function selectAreStarsLoaded(global: GlobalState) {
|
||||
return Boolean(global.stars);
|
||||
}
|
||||
|
||||
const ReactionButton = ({
|
||||
reaction,
|
||||
containerId,
|
||||
@ -39,36 +57,153 @@ const ReactionButton = ({
|
||||
recentReactors,
|
||||
className,
|
||||
chosenClassName,
|
||||
availableStars,
|
||||
chatId,
|
||||
messageId,
|
||||
observeIntersection,
|
||||
onClick,
|
||||
onPaidClick,
|
||||
}: OwnProps) => {
|
||||
const handleClick = useLastCallback(() => {
|
||||
const { openStarsBalanceModal, resetLocalPaidReactions, openPaidReactionModal } = getActions();
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLButtonElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const counterRef = useRef<HTMLSpanElement>(null);
|
||||
const animationRef = useRef<Animation>();
|
||||
|
||||
const isPaid = reaction.reaction.type === 'paid';
|
||||
|
||||
const areStarsLoaded = useSelector(selectAreStarsLoaded);
|
||||
|
||||
const handlePaidClick = useLastCallback((count = 1) => {
|
||||
onPaidClick?.(count);
|
||||
});
|
||||
|
||||
const handleClick = useLastCallback((e: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
if (reaction.reaction.type === 'paid') {
|
||||
e.stopPropagation(); // Prevent default message double click behavior
|
||||
handlePaidClick();
|
||||
return;
|
||||
}
|
||||
|
||||
onClick?.(reaction.reaction);
|
||||
});
|
||||
|
||||
const {
|
||||
isContextMenuOpen,
|
||||
handleBeforeContextMenu,
|
||||
handleContextMenu,
|
||||
handleContextMenuClose,
|
||||
handleContextMenuHide,
|
||||
} = useContextMenuHandlers(ref, reaction.reaction.type !== 'paid', undefined, undefined, undefined, true);
|
||||
|
||||
useEffect(() => {
|
||||
if (isContextMenuOpen) {
|
||||
openPaidReactionModal({
|
||||
chatId,
|
||||
messageId,
|
||||
});
|
||||
|
||||
handleContextMenuClose();
|
||||
handleContextMenuHide();
|
||||
}
|
||||
}, [handleContextMenuClose, handleContextMenuHide, isContextMenuOpen, chatId, messageId]);
|
||||
|
||||
useEffectWithPrevDeps(([prevReaction]) => {
|
||||
const amount = reaction.localAmount;
|
||||
const button = ref.current;
|
||||
if (!amount || !button || amount === prevReaction?.localAmount) return;
|
||||
|
||||
if (areStarsLoaded && (!availableStars || amount > availableStars)) {
|
||||
openStarsBalanceModal({
|
||||
originReaction: {
|
||||
chatId,
|
||||
messageId,
|
||||
amount,
|
||||
},
|
||||
});
|
||||
resetLocalPaidReactions({
|
||||
chatId,
|
||||
messageId,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const currentScale = Number(getComputedStyle(button).scale) || 1;
|
||||
animationRef.current?.cancel();
|
||||
// Animate scaling by 20%, and then returning to 1
|
||||
animationRef.current = button.animate([
|
||||
{ scale: currentScale },
|
||||
{ scale: Math.min(currentScale * 1.2, MAX_SCALE), offset: 0.2 },
|
||||
{ scale: 1 },
|
||||
], {
|
||||
duration: 500 * currentScale,
|
||||
easing: 'ease-out',
|
||||
});
|
||||
}, [reaction, availableStars, areStarsLoaded, chatId, messageId]);
|
||||
|
||||
const prevAmount = usePrevious(reaction.localAmount);
|
||||
|
||||
const {
|
||||
shouldRender: shouldRenderPaidCounter,
|
||||
} = useShowTransition({
|
||||
isOpen: Boolean(reaction.localAmount),
|
||||
ref: counterRef,
|
||||
className: 'slow',
|
||||
withShouldRender: true,
|
||||
});
|
||||
|
||||
return (
|
||||
<Button
|
||||
className={buildClassName(
|
||||
styles.root,
|
||||
isOwnMessage && styles.own,
|
||||
isPaid && styles.paid,
|
||||
isReactionChosen(reaction) && styles.chosen,
|
||||
isReactionChosen(reaction) && chosenClassName,
|
||||
className,
|
||||
)}
|
||||
size="tiny"
|
||||
ref={ref}
|
||||
onMouseDown={handleBeforeContextMenu}
|
||||
onContextMenu={handleContextMenu}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<ReactionAnimatedEmoji
|
||||
className={styles.animatedEmoji}
|
||||
containerId={containerId}
|
||||
reaction={reaction.reaction}
|
||||
size={REACTION_SIZE}
|
||||
observeIntersection={observeIntersection}
|
||||
/>
|
||||
{reaction.reaction.type === 'paid' ? (
|
||||
<>
|
||||
<PaidReactionEmoji
|
||||
className={styles.animatedEmoji}
|
||||
containerId={containerId}
|
||||
reaction={reaction.reaction}
|
||||
size={REACTION_SIZE}
|
||||
localAmount={reaction.localAmount}
|
||||
observeIntersection={observeIntersection}
|
||||
/>
|
||||
<Sparkles preset="reaction" />
|
||||
{shouldRenderPaidCounter && (
|
||||
<AnimatedCounter
|
||||
ref={counterRef}
|
||||
text={`+${formatIntegerCompact(reaction.localAmount || prevAmount!)}`}
|
||||
className={styles.paidCounter}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<ReactionAnimatedEmoji
|
||||
className={styles.animatedEmoji}
|
||||
containerId={containerId}
|
||||
reaction={reaction.reaction}
|
||||
size={REACTION_SIZE}
|
||||
observeIntersection={observeIntersection}
|
||||
/>
|
||||
)}
|
||||
{recentReactors?.length ? (
|
||||
<AvatarList size="mini" peers={recentReactors} />
|
||||
) : (
|
||||
<AnimatedCounter text={formatIntegerCompact(reaction.count)} className={styles.counter} />
|
||||
<AnimatedCounter
|
||||
text={formatIntegerCompact(reaction.count + (reaction.localAmount || 0))}
|
||||
className={styles.counter}
|
||||
/>
|
||||
)}
|
||||
</Button>
|
||||
);
|
||||
|
||||
@ -2,18 +2,19 @@ import type { FC } from '../../../../lib/teact/teact';
|
||||
import React, { memo, useMemo, useRef } from '../../../../lib/teact/teact';
|
||||
import { getActions, getGlobal, withGlobal } from '../../../../global';
|
||||
|
||||
import type { IAnchorPosition } from '../../../../types';
|
||||
import {
|
||||
type ApiAvailableEffect,
|
||||
type ApiMessage,
|
||||
type ApiMessageEntity,
|
||||
type ApiReaction,
|
||||
type ApiReactionCustomEmoji,
|
||||
type ApiSticker,
|
||||
type ApiStory,
|
||||
type ApiStorySkipped,
|
||||
MAIN_THREAD_ID,
|
||||
import type {
|
||||
ApiAvailableEffect,
|
||||
ApiMessage,
|
||||
ApiMessageEntity,
|
||||
ApiReaction,
|
||||
ApiReactionCustomEmoji,
|
||||
ApiReactionWithPaid,
|
||||
ApiSticker,
|
||||
ApiStory,
|
||||
ApiStorySkipped,
|
||||
} from '../../../../api/types';
|
||||
import type { IAnchorPosition } from '../../../../types';
|
||||
import { MAIN_THREAD_ID } from '../../../../api/types';
|
||||
|
||||
import { getReactionKey, getStoryKey, isUserId } from '../../../../global/helpers';
|
||||
import {
|
||||
@ -78,7 +79,7 @@ const ReactionPicker: FC<OwnProps & StateProps> = ({
|
||||
}) => {
|
||||
const {
|
||||
toggleReaction, closeReactionPicker, sendMessage, showNotification, sendStoryReaction, saveEffectInDraft,
|
||||
requestEffectInComposer,
|
||||
requestEffectInComposer, addLocalPaidReaction, openPaidReactionModal,
|
||||
} = getActions();
|
||||
|
||||
const lang = useOldLang();
|
||||
@ -128,22 +129,40 @@ const ReactionPicker: FC<OwnProps & StateProps> = ({
|
||||
closeReactionPicker();
|
||||
});
|
||||
|
||||
const handleToggleReaction = useLastCallback((reaction: ApiReaction) => {
|
||||
const handleToggleReaction = useLastCallback((reaction: ApiReactionWithPaid) => {
|
||||
if (!renderedChatId || !renderedMessageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
toggleReaction({
|
||||
chatId: renderedChatId, messageId: renderedMessageId, reaction, shouldAddToRecent: true,
|
||||
if (reaction.type === 'paid') {
|
||||
addLocalPaidReaction({
|
||||
chatId: renderedChatId, messageId: renderedMessageId, count: 1,
|
||||
});
|
||||
} else {
|
||||
toggleReaction({
|
||||
chatId: renderedChatId, messageId: renderedMessageId, reaction, shouldAddToRecent: true,
|
||||
});
|
||||
}
|
||||
closeReactionPicker();
|
||||
});
|
||||
|
||||
const handleReactionContextMenu = useLastCallback((reaction: ApiReactionWithPaid) => {
|
||||
if (reaction.type !== 'paid') return;
|
||||
|
||||
openPaidReactionModal({
|
||||
chatId: renderedChatId!,
|
||||
messageId: renderedMessageId!,
|
||||
});
|
||||
closeReactionPicker();
|
||||
});
|
||||
|
||||
const handleStoryReactionSelect = useLastCallback((item: ApiReaction | ApiSticker) => {
|
||||
const reaction = 'id' in item ? { documentId: item.id } : item;
|
||||
const handleStoryReactionSelect = useLastCallback((item: ApiReactionWithPaid | ApiSticker) => {
|
||||
if ('type' in item && item.type === 'paid') return; // Not supported for stories
|
||||
|
||||
const sticker = 'documentId' in item
|
||||
? getGlobal().customEmojis.byId[item.documentId] : 'emoticon' in item ? undefined : item;
|
||||
const reaction = 'id' in item ? { type: 'custom', documentId: item.id } as const : item;
|
||||
|
||||
const sticker = 'type' in item && item.type === 'custom' ? getGlobal().customEmojis.byId[item.documentId]
|
||||
: 'id' in item ? item : undefined;
|
||||
|
||||
if (sticker && !sticker.isFree && !isCurrentUserPremium) {
|
||||
showNotification({
|
||||
@ -175,7 +194,7 @@ const ReactionPicker: FC<OwnProps & StateProps> = ({
|
||||
let text: string | undefined;
|
||||
let entities: ApiMessageEntity[] | undefined;
|
||||
|
||||
if ('emoticon' in item) {
|
||||
if ('type' in item && item.type === 'emoji') {
|
||||
text = item.emoticon;
|
||||
} else {
|
||||
const customEmojiMessage = parseHtmlAsFormattedText(buildCustomEmojiHtml(sticker!));
|
||||
@ -193,7 +212,7 @@ const ReactionPicker: FC<OwnProps & StateProps> = ({
|
||||
|
||||
if (chatId) saveEffectInDraft({ chatId, threadId: MAIN_THREAD_ID, effectId });
|
||||
|
||||
if (effectId) requestEffectInComposer({ });
|
||||
if (effectId) requestEffectInComposer({});
|
||||
closeReactionPicker();
|
||||
});
|
||||
|
||||
@ -253,12 +272,14 @@ const ReactionPicker: FC<OwnProps & StateProps> = ({
|
||||
isTranslucent={isTranslucent}
|
||||
onCustomEmojiSelect={renderedStoryId ? handleStoryReactionSelect : handleToggleCustomReaction}
|
||||
onReactionSelect={renderedStoryId ? handleStoryReactionSelect : handleToggleReaction}
|
||||
onReactionContext={handleReactionContextMenu}
|
||||
/>
|
||||
{!shouldUseFullPicker && Boolean(renderedChatId) && (
|
||||
<ReactionPickerLimited
|
||||
chatId={renderedChatId}
|
||||
loadAndPlay={isOpen}
|
||||
onReactionSelect={renderedStoryId ? handleStoryReactionSelect : handleToggleReaction}
|
||||
onReactionContext={handleReactionContextMenu}
|
||||
selectedReactionIds={selectedReactionIds}
|
||||
message={message}
|
||||
/>
|
||||
@ -285,7 +306,7 @@ export default memo(withGlobal<OwnProps>((global): StateProps => {
|
||||
const areSomeReactionsAllowed = chatFullInfo?.enabledReactions?.type === 'some';
|
||||
const { maxUniqueReactions } = global.appConfig || {};
|
||||
const areAllReactionsAllowed = chatFullInfo?.enabledReactions?.type === 'all'
|
||||
&& chatFullInfo?.enabledReactions?.areCustomAllowed;
|
||||
&& chatFullInfo?.enabledReactions?.areCustomAllowed;
|
||||
|
||||
const currentReactions = message?.reactions?.results;
|
||||
const shouldUseCurrentReactions = Boolean(maxUniqueReactions && currentReactions
|
||||
|
||||
@ -8,6 +8,7 @@ import { withGlobal } from '../../../../global';
|
||||
import type {
|
||||
ApiAvailableReaction, ApiChatReactions, ApiMessage,
|
||||
ApiReaction,
|
||||
ApiReactionWithPaid,
|
||||
} from '../../../../api/types';
|
||||
|
||||
import {
|
||||
@ -20,22 +21,24 @@ import { REM } from '../../../common/helpers/mediaDimensions';
|
||||
import useAppLayout from '../../../../hooks/useAppLayout';
|
||||
import useWindowSize from '../../../../hooks/window/useWindowSize';
|
||||
|
||||
import ReactionEmoji from '../../../common/ReactionEmoji';
|
||||
import ReactionEmoji from '../../../common/reactions/ReactionEmoji';
|
||||
|
||||
import styles from './ReactionPickerLimited.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
chatId: string;
|
||||
loadAndPlay: boolean;
|
||||
onReactionSelect?: (reaction: ApiReaction) => void;
|
||||
selectedReactionIds?: string[];
|
||||
message?: ApiMessage;
|
||||
onReactionSelect: (reaction: ApiReactionWithPaid) => void;
|
||||
onReactionContext?: (reaction: ApiReactionWithPaid) => void;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
enabledReactions?: ApiChatReactions;
|
||||
availableReactions?: ApiAvailableReaction[];
|
||||
topReactions: ApiReaction[];
|
||||
isWithPaidReaction?: boolean;
|
||||
canAnimate?: boolean;
|
||||
isSavedMessages?: boolean;
|
||||
reactionsLimit?: number;
|
||||
@ -56,9 +59,11 @@ const ReactionPickerLimited: FC<OwnProps & StateProps> = ({
|
||||
availableReactions,
|
||||
topReactions,
|
||||
selectedReactionIds,
|
||||
onReactionSelect,
|
||||
isWithPaidReaction,
|
||||
message,
|
||||
reactionsLimit,
|
||||
onReactionSelect,
|
||||
onReactionContext,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const sharedCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||
@ -74,18 +79,34 @@ const ReactionPickerLimited: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const allAvailableReactions = useMemo(() => {
|
||||
if (shouldUseCurrentReactions) {
|
||||
return currentReactions.map(({ reaction }) => reaction);
|
||||
const reactions = currentReactions.map(({ reaction }) => reaction);
|
||||
if (isWithPaidReaction) {
|
||||
reactions.unshift({ type: 'paid' });
|
||||
}
|
||||
return reactions;
|
||||
}
|
||||
|
||||
if (!enabledReactions) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (enabledReactions.type === 'all') {
|
||||
return sortReactions((availableReactions || []).map(({ reaction }) => reaction), topReactions);
|
||||
const reactionsToSort: ApiReactionWithPaid[] = (availableReactions || []).map(({ reaction }) => reaction);
|
||||
if (isWithPaidReaction) {
|
||||
reactionsToSort.unshift({ type: 'paid' });
|
||||
}
|
||||
return sortReactions(reactionsToSort, topReactions);
|
||||
}
|
||||
|
||||
return sortReactions(enabledReactions.allowed, topReactions);
|
||||
}, [availableReactions, enabledReactions, topReactions, shouldUseCurrentReactions, currentReactions]);
|
||||
const reactionsToSort: ApiReactionWithPaid[] = enabledReactions.allowed;
|
||||
if (isWithPaidReaction) {
|
||||
reactionsToSort.unshift({ type: 'paid' });
|
||||
}
|
||||
|
||||
return sortReactions(reactionsToSort, topReactions);
|
||||
}, [
|
||||
availableReactions, enabledReactions, topReactions, shouldUseCurrentReactions, currentReactions, isWithPaidReaction,
|
||||
]);
|
||||
|
||||
const pickerHeight = useMemo(() => {
|
||||
const pickerWidth = Math.min(MODAL_MAX_WIDTH_REM * REM, windowWidth);
|
||||
@ -118,6 +139,7 @@ const ReactionPickerLimited: FC<OwnProps & StateProps> = ({
|
||||
loadAndPlay={loadAndPlay}
|
||||
availableReactions={availableReactions}
|
||||
onClick={onReactionSelect!}
|
||||
onContextMenu={onReactionContext}
|
||||
sharedCanvasRef={sharedCanvasRef}
|
||||
sharedCanvasHqRef={sharedCanvasHqRef}
|
||||
/>
|
||||
@ -134,13 +156,14 @@ export default memo(withGlobal<OwnProps>(
|
||||
const { availableReactions, topReactions } = global.reactions;
|
||||
|
||||
const { maxUniqueReactions } = global.appConfig || {};
|
||||
const { enabledReactions } = selectChatFullInfo(global, chatId) || {};
|
||||
const { enabledReactions, isPaidReactionAvailable } = selectChatFullInfo(global, chatId) || {};
|
||||
|
||||
return {
|
||||
enabledReactions,
|
||||
availableReactions,
|
||||
topReactions,
|
||||
reactionsLimit: maxUniqueReactions,
|
||||
isWithPaidReaction: isPaidReactionAvailable,
|
||||
};
|
||||
},
|
||||
)(ReactionPickerLimited));
|
||||
|
||||
@ -3,7 +3,12 @@ import React, { memo, useMemo, useRef } from '../../../../lib/teact/teact';
|
||||
import { getActions } from '../../../../global';
|
||||
|
||||
import type {
|
||||
ApiAvailableReaction, ApiChatReactions, ApiReaction, ApiReactionCount,
|
||||
ApiAvailableReaction,
|
||||
ApiChatReactions,
|
||||
ApiReaction,
|
||||
ApiReactionCount,
|
||||
ApiReactionCustomEmoji,
|
||||
ApiReactionPaid,
|
||||
} from '../../../../api/types';
|
||||
import type { IAnchorPosition } from '../../../../types';
|
||||
|
||||
@ -22,6 +27,8 @@ import ReactionSelectorReaction from './ReactionSelectorReaction';
|
||||
|
||||
import './ReactionSelector.scss';
|
||||
|
||||
type RenderableReactions = (ApiAvailableReaction | ApiReactionCustomEmoji | ApiReactionPaid)[];
|
||||
|
||||
type OwnProps = {
|
||||
enabledReactions?: ApiChatReactions;
|
||||
isPrivate?: boolean;
|
||||
@ -39,8 +46,11 @@ type OwnProps = {
|
||||
isInSavedMessages?: boolean;
|
||||
isInStoryViewer?: boolean;
|
||||
isForEffects?: boolean;
|
||||
isWithPaidReaction?: boolean;
|
||||
onClose?: NoneToVoidFunction;
|
||||
onToggleReaction: (reaction: ApiReaction) => void;
|
||||
onSendPaidReaction?: NoneToVoidFunction;
|
||||
onShowPaidReactionModal?: NoneToVoidFunction;
|
||||
onShowMore: (position: IAnchorPosition) => void;
|
||||
};
|
||||
|
||||
@ -64,8 +74,11 @@ const ReactionSelector: FC<OwnProps> = ({
|
||||
isInStoryViewer,
|
||||
isForEffects,
|
||||
effectReactions,
|
||||
isWithPaidReaction,
|
||||
onClose,
|
||||
onToggleReaction,
|
||||
onSendPaidReaction,
|
||||
onShowPaidReactionModal,
|
||||
onShowMore,
|
||||
}) => {
|
||||
const { openPremiumModal } = getActions();
|
||||
@ -87,8 +100,8 @@ const ReactionSelector: FC<OwnProps> = ({
|
||||
return allAvailableReactions?.map((reaction) => reaction.reaction);
|
||||
})();
|
||||
|
||||
const filteredReactions = reactions?.map((reaction) => {
|
||||
const isCustomReaction = 'documentId' in reaction;
|
||||
const filteredReactions: RenderableReactions = reactions?.map((reaction) => {
|
||||
const isCustomReaction = reaction.type === 'custom';
|
||||
const availableReaction = allAvailableReactions?.find((r) => isSameReaction(r.reaction, reaction));
|
||||
|
||||
if (isForEffects) return availableReaction;
|
||||
@ -103,11 +116,14 @@ const ReactionSelector: FC<OwnProps> = ({
|
||||
return isCustomReaction ? reaction : availableReaction;
|
||||
}).filter(Boolean) || [];
|
||||
|
||||
return sortReactions(filteredReactions, topReactions);
|
||||
const sortedReactions = sortReactions(filteredReactions, topReactions);
|
||||
if (isWithPaidReaction) {
|
||||
sortedReactions.unshift({ type: 'paid' });
|
||||
}
|
||||
return sortedReactions;
|
||||
}, [
|
||||
allAvailableReactions, currentReactions, defaultTagReactions, enabledReactions, isInSavedMessages, isPrivate,
|
||||
topReactions, isForEffects, effectReactions, shouldUseCurrentReactions,
|
||||
|
||||
topReactions, isForEffects, effectReactions, shouldUseCurrentReactions, isWithPaidReaction,
|
||||
]);
|
||||
|
||||
const reactionsToRender = useMemo(() => {
|
||||
@ -196,6 +212,8 @@ const ReactionSelector: FC<OwnProps> = ({
|
||||
key={getReactionKey(reaction)}
|
||||
isReady={isReady}
|
||||
onToggleReaction={onToggleReaction}
|
||||
onSendPaidReaction={onSendPaidReaction}
|
||||
onShowPaidReactionModal={onShowPaidReactionModal}
|
||||
reaction={reaction}
|
||||
noAppearAnimation={!canPlayAnimatedEmojis}
|
||||
chosen={userReactionIndexes.has(i)}
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
import type { FC } from '../../../../lib/teact/teact';
|
||||
import React, { memo } from '../../../../lib/teact/teact';
|
||||
import React, { memo, useEffect, useRef } from '../../../../lib/teact/teact';
|
||||
|
||||
import type { ApiReaction, ApiReactionCustomEmoji } from '../../../../api/types';
|
||||
import type { ApiReaction, ApiReactionCustomEmoji, ApiReactionPaid } from '../../../../api/types';
|
||||
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
import { LOCAL_TGS_URLS } from '../../../common/helpers/animatedAssets';
|
||||
import { REM } from '../../../common/helpers/mediaDimensions';
|
||||
|
||||
import useContextMenuHandlers from '../../../../hooks/useContextMenuHandlers';
|
||||
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||
|
||||
import AnimatedIcon from '../../../common/AnimatedIcon';
|
||||
import CustomEmoji from '../../../common/CustomEmoji';
|
||||
import Icon from '../../../common/icons/Icon';
|
||||
|
||||
@ -14,13 +19,15 @@ import styles from './ReactionSelectorReaction.module.scss';
|
||||
const REACTION_SIZE = 2 * REM;
|
||||
|
||||
type OwnProps = {
|
||||
reaction: ApiReactionCustomEmoji;
|
||||
reaction: ApiReactionCustomEmoji | ApiReactionPaid;
|
||||
chosen?: boolean;
|
||||
isReady?: boolean;
|
||||
noAppearAnimation?: boolean;
|
||||
style?: string;
|
||||
isLocked?: boolean;
|
||||
onToggleReaction: (reaction: ApiReaction) => void;
|
||||
onSendPaidReaction?: NoneToVoidFunction;
|
||||
onShowPaidReactionModal?: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
const ReactionSelectorCustomReaction: FC<OwnProps> = ({
|
||||
@ -31,27 +38,64 @@ const ReactionSelectorCustomReaction: FC<OwnProps> = ({
|
||||
style,
|
||||
isLocked,
|
||||
onToggleReaction,
|
||||
onSendPaidReaction,
|
||||
onShowPaidReactionModal,
|
||||
}) => {
|
||||
function handleClick() {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const handleClick = useLastCallback(() => {
|
||||
if (reaction.type === 'paid') {
|
||||
onSendPaidReaction?.();
|
||||
return;
|
||||
}
|
||||
|
||||
onToggleReaction(reaction);
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
isContextMenuOpen,
|
||||
handleBeforeContextMenu,
|
||||
handleContextMenu,
|
||||
handleContextMenuClose,
|
||||
handleContextMenuHide,
|
||||
} = useContextMenuHandlers(ref, reaction.type !== 'paid', undefined, undefined, undefined, true);
|
||||
|
||||
useEffect(() => {
|
||||
if (isContextMenuOpen) {
|
||||
onShowPaidReactionModal?.();
|
||||
|
||||
handleContextMenuClose();
|
||||
handleContextMenuHide();
|
||||
}
|
||||
}, [handleContextMenuClose, onShowPaidReactionModal, handleContextMenuHide, isContextMenuOpen]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={buildClassName(
|
||||
styles.root,
|
||||
styles.custom,
|
||||
chosen && styles.chosen,
|
||||
chosen && reaction.type !== 'paid' && styles.chosen,
|
||||
!noAppearAnimation && isReady && styles.customAnimated,
|
||||
noAppearAnimation && styles.visible,
|
||||
)}
|
||||
ref={ref}
|
||||
style={style}
|
||||
onClick={handleClick}
|
||||
onMouseDown={handleBeforeContextMenu}
|
||||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
<CustomEmoji
|
||||
documentId={reaction.documentId}
|
||||
size={REACTION_SIZE}
|
||||
/>
|
||||
{reaction.type === 'paid' ? (
|
||||
<AnimatedIcon
|
||||
tgsUrl={LOCAL_TGS_URLS.StarReaction}
|
||||
size={REACTION_SIZE}
|
||||
noLoop={false}
|
||||
/>
|
||||
) : (
|
||||
<CustomEmoji
|
||||
documentId={reaction.documentId}
|
||||
size={REACTION_SIZE}
|
||||
/>
|
||||
)}
|
||||
{isLocked && (
|
||||
<Icon className={styles.lock} name="lock-badge" />
|
||||
)}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { FC } from '../../../../lib/teact/teact';
|
||||
import React, { memo, useMemo } from '../../../../lib/teact/teact';
|
||||
import React, { memo, useEffect, useMemo } from '../../../../lib/teact/teact';
|
||||
import { getActions, getGlobal } from '../../../../global';
|
||||
|
||||
import type {
|
||||
@ -17,6 +17,7 @@ import { selectPeer } from '../../../../global/selectors';
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
import { getMessageKey } from '../../../../util/keys/messageKey';
|
||||
|
||||
import useEffectOnce from '../../../../hooks/useEffectOnce';
|
||||
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../../hooks/useOldLang';
|
||||
|
||||
@ -35,9 +36,11 @@ type OwnProps = {
|
||||
isCurrentUserPremium?: boolean;
|
||||
observeIntersection?: ObserveFn;
|
||||
noRecentReactors?: boolean;
|
||||
availableStars?: number;
|
||||
};
|
||||
|
||||
const MAX_RECENT_AVATARS = 3;
|
||||
const PAID_SEND_DELAY = 5000;
|
||||
|
||||
const Reactions: FC<OwnProps> = ({
|
||||
message,
|
||||
@ -49,12 +52,16 @@ const Reactions: FC<OwnProps> = ({
|
||||
noRecentReactors,
|
||||
isCurrentUserPremium,
|
||||
tags,
|
||||
availableStars,
|
||||
}) => {
|
||||
const {
|
||||
toggleReaction,
|
||||
addLocalPaidReaction,
|
||||
updateMiddleSearch,
|
||||
performMiddleSearch,
|
||||
openPremiumModal,
|
||||
resetLocalPaidReactions,
|
||||
showNotification,
|
||||
} = getActions();
|
||||
const lang = useOldLang();
|
||||
|
||||
@ -109,7 +116,7 @@ const Reactions: FC<OwnProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
updateMiddleSearch({ chatId: message.chatId, threadId, update: { savedTag: reaction } });
|
||||
updateMiddleSearch({ chatId: message.chatId, threadId, update: { savedTag: reaction as ApiReaction } });
|
||||
performMiddleSearch({ chatId: message.chatId, threadId });
|
||||
return;
|
||||
}
|
||||
@ -121,6 +128,40 @@ const Reactions: FC<OwnProps> = ({
|
||||
});
|
||||
});
|
||||
|
||||
const paidLocalCount = useMemo(() => results.find((r) => r.reaction.type === 'paid')?.localAmount || 0, [results]);
|
||||
|
||||
const handlePaidClick = useLastCallback((count: number) => {
|
||||
addLocalPaidReaction({
|
||||
chatId: message.chatId,
|
||||
messageId: message.id,
|
||||
count,
|
||||
});
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!paidLocalCount) return;
|
||||
|
||||
showNotification({
|
||||
localId: getMessageKey(message),
|
||||
title: lang('StarsSentTitle'),
|
||||
message: lang('StarsSentText', paidLocalCount),
|
||||
actionText: lang('StarsSentUndo'),
|
||||
cacheBreaker: paidLocalCount.toString(),
|
||||
action: {
|
||||
action: 'resetLocalPaidReactions',
|
||||
payload: { chatId: message.chatId, messageId: message.id },
|
||||
},
|
||||
dismissAction: {
|
||||
action: 'sendPaidReaction',
|
||||
payload: { chatId: message.chatId, messageId: message.id },
|
||||
},
|
||||
duration: PAID_SEND_DELAY,
|
||||
shouldShowTimer: true,
|
||||
disableClickDismiss: true,
|
||||
icon: 'star',
|
||||
});
|
||||
}, [lang, message, paidLocalCount]);
|
||||
|
||||
const handleRemoveReaction = useLastCallback((reaction: ApiReaction) => {
|
||||
toggleReaction({
|
||||
chatId: message.chatId,
|
||||
@ -129,6 +170,14 @@ const Reactions: FC<OwnProps> = ({
|
||||
});
|
||||
});
|
||||
|
||||
// Reset paid reactions on unmount
|
||||
useEffectOnce(() => () => {
|
||||
resetLocalPaidReactions({
|
||||
chatId: message.chatId,
|
||||
messageId: message.id,
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
className={buildClassName('Reactions', isOutside && 'is-outside')}
|
||||
@ -146,7 +195,7 @@ const Reactions: FC<OwnProps> = ({
|
||||
containerId={messageKey}
|
||||
isOwnMessage={message.isOutgoing}
|
||||
isChosen={isChosen}
|
||||
reaction={reaction.reaction}
|
||||
reaction={reaction.reaction as ApiReaction}
|
||||
tag={tag}
|
||||
withContextMenu={isCurrentUserPremium}
|
||||
onClick={handleClick}
|
||||
@ -156,6 +205,8 @@ const Reactions: FC<OwnProps> = ({
|
||||
) : (
|
||||
<ReactionButton
|
||||
key={reactionKey}
|
||||
chatId={message.chatId}
|
||||
messageId={message.id}
|
||||
className="message-reaction"
|
||||
chosenClassName="chosen"
|
||||
containerId={messageKey}
|
||||
@ -163,7 +214,9 @@ const Reactions: FC<OwnProps> = ({
|
||||
recentReactors={recentReactors}
|
||||
reaction={reaction}
|
||||
onClick={handleClick}
|
||||
onPaidClick={handlePaidClick}
|
||||
observeIntersection={observeIntersection}
|
||||
availableStars={availableStars}
|
||||
/>
|
||||
)
|
||||
))}
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import React, { memo, useRef } from '../../../../lib/teact/teact';
|
||||
import { getActions } from '../../../../global';
|
||||
|
||||
import type { ApiReaction, ApiSavedReactionTag } from '../../../../api/types';
|
||||
import type {
|
||||
ApiReaction,
|
||||
ApiSavedReactionTag,
|
||||
} from '../../../../api/types';
|
||||
import type { ObserveFn } from '../../../../hooks/useIntersectionObserver';
|
||||
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
@ -90,7 +93,7 @@ const SavedTagButton = ({
|
||||
handleContextMenu,
|
||||
handleContextMenuClose,
|
||||
handleContextMenuHide,
|
||||
} = useContextMenuHandlers(ref, !withContextMenu);
|
||||
} = useContextMenuHandlers(ref, !withContextMenu, undefined, undefined, undefined, true);
|
||||
|
||||
const getTriggerElement = useLastCallback(() => ref.current);
|
||||
const getRootElement = useLastCallback(() => document.body);
|
||||
|
||||
@ -14,6 +14,7 @@ import GiftCodeModal from './giftcode/GiftCodeModal.async';
|
||||
import InviteViaLinkModal from './inviteViaLink/InviteViaLinkModal.async';
|
||||
import MapModal from './map/MapModal.async';
|
||||
import OneTimeMediaModal from './oneTimeMedia/OneTimeMediaModal.async';
|
||||
import PaidReactionModal from './paidReaction/PaidReactionModal.async';
|
||||
import ReportAdModal from './reportAd/ReportAdModal.async';
|
||||
import StarsBalanceModal from './stars/StarsBalanceModal.async';
|
||||
import StarsPaymentModal from './stars/StarsPaymentModal.async';
|
||||
@ -35,6 +36,8 @@ type ModalKey = keyof Pick<TabState,
|
||||
'reportAdModal' |
|
||||
'starsBalanceModal' |
|
||||
'isStarPaymentModalOpen' |
|
||||
'starsTransactionModal' |
|
||||
'paidReactionModal' |
|
||||
'webApps' |
|
||||
'starsTransactionModal'
|
||||
>;
|
||||
@ -66,6 +69,7 @@ const MODALS: ModalRegistry = {
|
||||
isStarPaymentModalOpen: StarsPaymentModal,
|
||||
starsBalanceModal: StarsBalanceModal,
|
||||
starsTransactionModal: StarsTransactionInfoModal,
|
||||
paidReactionModal: PaidReactionModal,
|
||||
};
|
||||
const MODAL_KEYS = Object.keys(MODALS) as ModalKey[];
|
||||
const MODAL_ENTRIES = Object.entries(MODALS) as Entries<ModalRegistry>;
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React from '../../../lib/teact/teact';
|
||||
|
||||
import type { OwnProps } from './PaidReactionModal';
|
||||
|
||||
import { Bundles } from '../../../util/moduleLoader';
|
||||
|
||||
import useModuleLoader from '../../../hooks/useModuleLoader';
|
||||
|
||||
const PaidReactionModalAsync: FC<OwnProps> = (props) => {
|
||||
const { modal } = props;
|
||||
const PaidReactionModal = useModuleLoader(Bundles.Extra, 'PaidReactionModal', !modal);
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return PaidReactionModal ? <PaidReactionModal {...props} /> : undefined;
|
||||
};
|
||||
|
||||
export default PaidReactionModalAsync;
|
||||
@ -0,0 +1,70 @@
|
||||
.content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.25rem;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0;
|
||||
margin-top: 1.25rem;
|
||||
}
|
||||
|
||||
.slider {
|
||||
margin-top: 1.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.description {
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.title, .description {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.modalBalance {
|
||||
position: absolute;
|
||||
top: 0.75rem;
|
||||
right: 1.25rem;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.topLabel {
|
||||
background-image: var(--stars-gradient);
|
||||
color: var(--color-white);
|
||||
border-radius: 1rem;
|
||||
padding: 0.25rem 0.75rem;
|
||||
}
|
||||
|
||||
.top {
|
||||
display: flex;
|
||||
justify-content: space-around;
|
||||
margin-top: 1rem;
|
||||
}
|
||||
|
||||
.topBadge {
|
||||
background-image: var(--stars-gradient);
|
||||
}
|
||||
|
||||
.buttonStar {
|
||||
margin-inline-start: 0.25rem;
|
||||
}
|
||||
|
||||
.topPeer {
|
||||
overflow: hidden;
|
||||
flex-basis: 0;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
align-self: center;
|
||||
margin-bottom: 1.5rem;
|
||||
}
|
||||
|
||||
.disclaimer {
|
||||
font-size: 0.875rem;
|
||||
align-self: center;
|
||||
color: var(--color-text-secondary);
|
||||
margin-bottom: 0;
|
||||
}
|
||||
247
src/components/modals/paidReaction/PaidReactionModal.tsx
Normal file
247
src/components/modals/paidReaction/PaidReactionModal.tsx
Normal file
@ -0,0 +1,247 @@
|
||||
import React, {
|
||||
memo, useEffect, useMemo, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, getGlobal, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiChat, ApiMessage, ApiUser } from '../../../api/types';
|
||||
import type { TabState } from '../../../global/types';
|
||||
import type { CustomPeer } from '../../../types';
|
||||
|
||||
import { getChatTitle, getUserFullName } from '../../../global/helpers';
|
||||
import { selectChat, selectChatMessage, selectUser } from '../../../global/selectors';
|
||||
import { formatInteger } from '../../../util/textFormat';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import Icon from '../../common/icons/Icon';
|
||||
import PeerBadge from '../../common/PeerBadge';
|
||||
import SafeLink from '../../common/SafeLink';
|
||||
import Button from '../../ui/Button';
|
||||
import Checkbox from '../../ui/Checkbox';
|
||||
import Modal from '../../ui/Modal';
|
||||
import Separator from '../../ui/Separator';
|
||||
import BalanceBlock from '../stars/BalanceBlock';
|
||||
import StarSlider from './StarSlider';
|
||||
|
||||
import styles from './PaidReactionModal.module.scss';
|
||||
|
||||
export type OwnProps = {
|
||||
modal: TabState['paidReactionModal'];
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
message?: ApiMessage;
|
||||
chat?: ApiChat;
|
||||
maxAmount: number;
|
||||
starBalance?: number;
|
||||
defaultPrivacy?: boolean;
|
||||
};
|
||||
|
||||
type ReactorData = {
|
||||
amount: number;
|
||||
localAmount: number;
|
||||
isMe?: boolean;
|
||||
isAnonymous?: boolean;
|
||||
user?: ApiUser;
|
||||
};
|
||||
|
||||
const MAX_TOP_REACTORS = 3;
|
||||
const DEFAULT_STARS_AMOUNT = 50;
|
||||
const MAX_REACTION_AMOUNT = 2500;
|
||||
const ANONYMOUS_PEER: CustomPeer = {
|
||||
avatarIcon: 'author-hidden',
|
||||
customPeerAvatarColor: '#9eaab5',
|
||||
isCustomPeer: true,
|
||||
titleKey: 'StarsReactionAnonymous',
|
||||
};
|
||||
|
||||
const PaidReactionModal = ({
|
||||
modal,
|
||||
chat,
|
||||
message,
|
||||
maxAmount,
|
||||
starBalance,
|
||||
defaultPrivacy,
|
||||
}: OwnProps & StateProps) => {
|
||||
const { closePaidReactionModal, addLocalPaidReaction } = getActions();
|
||||
|
||||
const [starsAmount, setStarsAmount] = useState(DEFAULT_STARS_AMOUNT);
|
||||
const [isTouched, markTouched, unmarkTouched] = useFlag();
|
||||
const [shouldShowUp, setShouldShowUp] = useState(true);
|
||||
|
||||
const oldLang = useOldLang();
|
||||
const lang = useLang();
|
||||
|
||||
const handleAnonimityChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
setShouldShowUp(e.target.checked);
|
||||
});
|
||||
|
||||
const handleAmountChange = useLastCallback((value: number) => {
|
||||
setStarsAmount(value);
|
||||
markTouched();
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!modal) {
|
||||
unmarkTouched();
|
||||
}
|
||||
}, [modal]);
|
||||
|
||||
useEffect(() => {
|
||||
const currentReactor = message?.reactions?.topReactors?.find((reactor) => reactor.isMe);
|
||||
if (currentReactor) {
|
||||
setShouldShowUp(!currentReactor.isAnonymous);
|
||||
return;
|
||||
}
|
||||
|
||||
setShouldShowUp(defaultPrivacy || true);
|
||||
}, [defaultPrivacy, message?.reactions?.topReactors]);
|
||||
|
||||
const handleSend = useLastCallback(() => {
|
||||
if (!modal) return;
|
||||
|
||||
addLocalPaidReaction({
|
||||
chatId: modal.chatId,
|
||||
messageId: modal.messageId,
|
||||
count: starsAmount,
|
||||
isPrivate: !shouldShowUp,
|
||||
});
|
||||
closePaidReactionModal();
|
||||
});
|
||||
|
||||
const topReactors = useMemo(() => {
|
||||
const global = getGlobal();
|
||||
const all = message?.reactions?.topReactors;
|
||||
if (!all) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const result: ReactorData[] = [];
|
||||
let hasMe = false;
|
||||
|
||||
all.forEach((reactor) => {
|
||||
const user = reactor.peerId ? selectUser(global, reactor.peerId) : undefined;
|
||||
if (!user && !reactor.isAnonymous && !reactor.isMe) return;
|
||||
|
||||
if (reactor.isMe) {
|
||||
hasMe = true;
|
||||
}
|
||||
|
||||
result.push({
|
||||
amount: reactor.count,
|
||||
localAmount: reactor.isMe && isTouched ? starsAmount : 0,
|
||||
isMe: reactor.isMe,
|
||||
isAnonymous: reactor.isAnonymous,
|
||||
user,
|
||||
});
|
||||
});
|
||||
|
||||
if (!hasMe && isTouched) {
|
||||
const me = selectUser(global, global.currentUserId!);
|
||||
result.push({
|
||||
amount: 0,
|
||||
localAmount: starsAmount,
|
||||
isMe: true,
|
||||
user: me,
|
||||
});
|
||||
}
|
||||
|
||||
result.sort((a, b) => (b.amount + b.localAmount) - (a.amount + a.localAmount));
|
||||
|
||||
return result.slice(0, MAX_TOP_REACTORS);
|
||||
}, [isTouched, message?.reactions?.topReactors, starsAmount]);
|
||||
|
||||
const chatTitle = chat && getChatTitle(oldLang, chat);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={Boolean(modal)}
|
||||
onClose={closePaidReactionModal}
|
||||
isSlim
|
||||
hasAbsoluteCloseButton
|
||||
contentClassName={styles.content}
|
||||
>
|
||||
{starBalance !== undefined && <BalanceBlock balance={starBalance} className={styles.modalBalance} />}
|
||||
<StarSlider
|
||||
className={styles.slider}
|
||||
defaultValue={DEFAULT_STARS_AMOUNT}
|
||||
maxValue={maxAmount}
|
||||
onChange={handleAmountChange}
|
||||
/>
|
||||
<h3 className={styles.title}>{oldLang('StarsReactionTitle')}</h3>
|
||||
<div className={styles.description}>
|
||||
{renderText(oldLang('StarsReactionText', chatTitle), ['simple_markdown', 'emoji'])}
|
||||
</div>
|
||||
<Separator>
|
||||
{topReactors && <div className={styles.topLabel}>{oldLang('StarsReactionTopSenders')}</div>}
|
||||
</Separator>
|
||||
{topReactors && (
|
||||
<div className={styles.top}>
|
||||
{topReactors.map((reactor) => {
|
||||
const countText = formatInteger(reactor.amount + reactor.localAmount);
|
||||
const peer = (reactor.isAnonymous || !reactor.user || (reactor.isMe && !shouldShowUp))
|
||||
? ANONYMOUS_PEER : reactor.user;
|
||||
const text = 'isCustomPeer' in peer ? oldLang(peer.titleKey) : getUserFullName(peer);
|
||||
return (
|
||||
<PeerBadge
|
||||
className={styles.topPeer}
|
||||
key={`${reactor.user?.id || 'anonymous'}-${countText}`}
|
||||
peer={peer}
|
||||
badgeText={countText}
|
||||
badgeIcon="star"
|
||||
badgeClassName={styles.topBadge}
|
||||
text={text}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
<Checkbox
|
||||
className={styles.checkbox}
|
||||
checked={shouldShowUp}
|
||||
onChange={handleAnonimityChange}
|
||||
label={oldLang('StarsReactionShowMeInTopSenders')}
|
||||
/>
|
||||
<Button
|
||||
size="smaller"
|
||||
onClick={handleSend}
|
||||
>
|
||||
{lang('SendPaidReaction', { amount: starsAmount }, {
|
||||
withNodes: true,
|
||||
specialReplacement: {
|
||||
'⭐️': <Icon className={styles.buttonStar} name="star" />,
|
||||
},
|
||||
})}
|
||||
</Button>
|
||||
<p className={styles.disclaimer}>
|
||||
{lang('StarsReactionTerms', {
|
||||
link: <SafeLink text={lang('StarsReactionLinkText')} url={lang('StarsReactionLink')} />,
|
||||
}, {
|
||||
withNodes: true,
|
||||
})}
|
||||
</p>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { modal }): StateProps => {
|
||||
const chat = modal && selectChat(global, modal.chatId);
|
||||
const message = modal && selectChatMessage(global, modal.chatId, modal.messageId);
|
||||
const starBalance = global.stars?.balance;
|
||||
const maxAmount = global.appConfig?.paidReactionMaxAmount || MAX_REACTION_AMOUNT;
|
||||
const defaultPrivacy = global.settings.paidReactionPrivacy;
|
||||
|
||||
return {
|
||||
chat,
|
||||
message,
|
||||
starBalance,
|
||||
maxAmount,
|
||||
defaultPrivacy,
|
||||
};
|
||||
},
|
||||
)(PaidReactionModal));
|
||||
137
src/components/modals/paidReaction/StarSlider.module.scss
Normal file
137
src/components/modals/paidReaction/StarSlider.module.scss
Normal file
@ -0,0 +1,137 @@
|
||||
@use "../../../styles/mixins";
|
||||
|
||||
.root {
|
||||
--_size: 1.875rem;
|
||||
--progress: 0;
|
||||
|
||||
position: relative;
|
||||
padding-top: 4rem;
|
||||
overflow-x: hidden;
|
||||
|
||||
@include mixins.reset-range();
|
||||
}
|
||||
|
||||
.slider {
|
||||
height: var(--_size) !important;
|
||||
margin-bottom: 0 !important;
|
||||
cursor: pointer;
|
||||
|
||||
&::-webkit-slider-runnable-track {
|
||||
height: var(--_size);
|
||||
border-radius: 1rem;
|
||||
background-color: var(--color-background-secondary);
|
||||
}
|
||||
|
||||
&::-moz-range-track {
|
||||
height: var(--_size);
|
||||
border-radius: 1rem;
|
||||
background-color: var(--color-background-secondary);
|
||||
}
|
||||
|
||||
&::-webkit-slider-thumb {
|
||||
height: var(--_size);
|
||||
width: var(--_size);
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
&::-moz-range-thumb {
|
||||
height: var(--_size);
|
||||
width: var(--_size);
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
|
||||
.sparkles {
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
height: var(--_size);
|
||||
pointer-events: none;
|
||||
|
||||
--_width: calc(var(--progress) * 100% - 1rem);
|
||||
mask-image: linear-gradient(to right, black var(--_width), transparent calc(var(--_width) + 0.5rem));
|
||||
|
||||
color: white;
|
||||
}
|
||||
|
||||
.progress {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
height: var(--_size);
|
||||
pointer-events: none;
|
||||
border-radius: 1rem;
|
||||
|
||||
min-width: var(--_size);
|
||||
width: calc(var(--_size) + (var(--progress) * (100% - var(--_size))));
|
||||
|
||||
background-image: var(--stars-gradient);
|
||||
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
right: 0.125rem;
|
||||
top: 0.125rem;
|
||||
width: 1.625rem;
|
||||
height: 1.625rem;
|
||||
border-radius: 50%;
|
||||
background-color: white;
|
||||
z-index: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.floatingBadgeWrapper {
|
||||
--_min-x: 0;
|
||||
--_max-x: 100%;
|
||||
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
transform:
|
||||
translateX(
|
||||
clamp(
|
||||
var(--_min-x),
|
||||
calc(var(--_size) / 2 + var(--progress) * (100% - var(--_size))),
|
||||
var(--_max-x),
|
||||
)
|
||||
);
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.floatingBadge {
|
||||
--_speed: 0;
|
||||
position: absolute;
|
||||
top: -1rem;
|
||||
left: 0;
|
||||
transform: translate(-50%, -100%);
|
||||
}
|
||||
|
||||
.floatingBadgeText {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.125rem;
|
||||
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 2rem;
|
||||
|
||||
background-image: var(--stars-gradient);
|
||||
|
||||
line-height: 1;
|
||||
font-size: 1.5rem;
|
||||
font-weight: 500;
|
||||
color: white;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.floatingBadgeTriangle {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
bottom: 0;
|
||||
transform: translate(-50%, 33%);
|
||||
z-index: -1;
|
||||
}
|
||||
134
src/components/modals/paidReaction/StarSlider.tsx
Normal file
134
src/components/modals/paidReaction/StarSlider.tsx
Normal file
@ -0,0 +1,134 @@
|
||||
import React, {
|
||||
memo, useMemo, useRef, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
|
||||
import { requestMeasure, requestMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { formatInteger } from '../../../util/textFormat';
|
||||
|
||||
import useEffectOnce from '../../../hooks/useEffectOnce';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useResizeObserver from '../../../hooks/useResizeObserver';
|
||||
|
||||
import AnimatedCounter from '../../common/AnimatedCounter';
|
||||
import Icon from '../../common/icons/Icon';
|
||||
import Sparkles from '../../common/Sparkles';
|
||||
|
||||
import styles from './StarSlider.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
maxValue: number;
|
||||
defaultValue: number;
|
||||
className?: string;
|
||||
onChange: (value: number) => void;
|
||||
};
|
||||
|
||||
const DEFAULT_POINTS = [50, 100, 500, 1000, 2000, 5000, 10000];
|
||||
|
||||
const StarSlider = ({
|
||||
maxValue,
|
||||
defaultValue,
|
||||
className,
|
||||
onChange,
|
||||
}: OwnProps) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const floatingBadgeRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const points = useMemo(() => {
|
||||
const result = [];
|
||||
for (let i = 0; i < DEFAULT_POINTS.length; i++) {
|
||||
if (DEFAULT_POINTS[i] < maxValue) {
|
||||
result.push(DEFAULT_POINTS[i]);
|
||||
}
|
||||
|
||||
if (DEFAULT_POINTS[i] >= maxValue) {
|
||||
result.push(maxValue);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}, [maxValue]);
|
||||
|
||||
const [value, setValue] = useState(0);
|
||||
|
||||
useEffectOnce(() => {
|
||||
setValue(getProgress(points, defaultValue));
|
||||
});
|
||||
|
||||
const updateSafeBadgePosition = useLastCallback(() => {
|
||||
const badge = floatingBadgeRef.current;
|
||||
if (!badge) return;
|
||||
const parent = badge.parentElement!;
|
||||
|
||||
requestMeasure(() => {
|
||||
const safeMinX = parent.offsetLeft + badge.offsetWidth / 2;
|
||||
const safeMaxX = parent.offsetLeft + parent.offsetWidth - badge.offsetWidth / 2;
|
||||
|
||||
requestMutation(() => {
|
||||
parent.style.setProperty('--_min-x', `${safeMinX}px`);
|
||||
parent.style.setProperty('--_max-x', `${safeMaxX}px`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
useResizeObserver(floatingBadgeRef, updateSafeBadgePosition);
|
||||
|
||||
const handleChange = useLastCallback((event: React.ChangeEvent<HTMLInputElement>) => {
|
||||
const newValue = Number(event.currentTarget.value);
|
||||
setValue(newValue);
|
||||
|
||||
onChange(getValue(points, newValue));
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={buildClassName(styles.root, className)} style={`--progress: ${value / points.length}`}>
|
||||
<div className={styles.floatingBadgeWrapper}>
|
||||
<div className={styles.floatingBadge} ref={floatingBadgeRef}>
|
||||
<div className={styles.floatingBadgeText}>
|
||||
<Icon name="star" className={styles.floatingBadgeIcon} />
|
||||
<AnimatedCounter text={formatInteger(getValue(points, value))} />
|
||||
</div>
|
||||
<svg className={styles.floatingBadgeTriangle} width="28" height="28" viewBox="0 0 28 28" fill="none">
|
||||
<defs>
|
||||
<linearGradient id="StarBadgeTriangle" x1="0" x2="1" y1="0" y2="0">
|
||||
<stop offset="-50%" stop-color="#FFAA00" />
|
||||
<stop offset="150%" stop-color="#FFCD3A" />
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<path d="m 28,4 v 9 c 0.0089,7.283278 -3.302215,5.319646 -6.750951,8.589815 l -5.8284,5.82843 c -0.781,0.78105 -2.0474,0.78104 -2.8284,0 L 6.7638083,21.589815 C 2.8288652,17.959047 0.04527024,20.332086 0,13 V 4 C 0,4 0.00150581,0.97697493 3,1 5.3786658,1.018266 22.594519,0.9142007 25,1 c 2.992326,0.1067311 3,3 3,3 z" fill="url(#StarBadgeTriangle)" />
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.progress} />
|
||||
<Sparkles preset="progress" className={styles.sparkles} />
|
||||
<input
|
||||
className={styles.slider}
|
||||
type="range"
|
||||
min={0}
|
||||
max={points.length}
|
||||
defaultValue={getProgress(points, defaultValue)}
|
||||
step="any"
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
function getProgress(points: number[], value: number) {
|
||||
const pointIndex = points.findIndex((point) => value <= point);
|
||||
const prevPoint = points[pointIndex - 1] || 1;
|
||||
const nextPoint = points[pointIndex] || points[points.length - 1];
|
||||
const progress = (value - prevPoint) / (nextPoint - prevPoint);
|
||||
return pointIndex + progress;
|
||||
}
|
||||
|
||||
function getValue(points: number[], progress: number) {
|
||||
const pointIndex = Math.floor(progress);
|
||||
const prevPoint = points[pointIndex - 1] || 1;
|
||||
const nextPoint = points[pointIndex] || points[points.length - 1];
|
||||
const value = prevPoint + (nextPoint - prevPoint) * (progress - pointIndex);
|
||||
return Math.round(value);
|
||||
}
|
||||
|
||||
export default memo(StarSlider);
|
||||
@ -1,16 +1,17 @@
|
||||
import React, {
|
||||
memo, useEffect, useMemo, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
import { getActions, getGlobal, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiUser } from '../../../api/types';
|
||||
import type { ApiStarTopupOption } from '../../../api/types';
|
||||
import type { GlobalState, TabState } from '../../../global/types';
|
||||
|
||||
import { getUserFullName } from '../../../global/helpers';
|
||||
import { selectIsPremiumPurchaseBlocked, selectUser } from '../../../global/selectors';
|
||||
import { getChatTitle, getUserFullName } from '../../../global/helpers';
|
||||
import { selectChat, selectIsPremiumPurchaseBlocked, selectUser } from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
@ -24,6 +25,7 @@ import Modal from '../../ui/Modal';
|
||||
import TabList, { type TabWithProperties } from '../../ui/TabList';
|
||||
import Transition from '../../ui/Transition';
|
||||
import BalanceBlock from './BalanceBlock';
|
||||
import StarTopupOptionList from './StarTopupOptionList';
|
||||
import TransactionItem from './transaction/StarsTransactionItem';
|
||||
|
||||
import styles from './StarsBalanceModal.module.scss';
|
||||
@ -44,15 +46,14 @@ export type OwnProps = {
|
||||
|
||||
type StateProps = {
|
||||
starsBalanceState?: GlobalState['stars'];
|
||||
originPaymentBot?: ApiUser;
|
||||
canBuyPremium?: boolean;
|
||||
};
|
||||
|
||||
const StarsBalanceModal = ({
|
||||
modal, starsBalanceState, originPaymentBot, canBuyPremium,
|
||||
modal, starsBalanceState, canBuyPremium,
|
||||
}: OwnProps & StateProps) => {
|
||||
const {
|
||||
closeStarsBalanceModal, loadStarsTransactions, openStarsGiftingModal, openStarsGiftModal,
|
||||
closeStarsBalanceModal, loadStarsTransactions, openStarsGiftingModal, openInvoice,
|
||||
} = getActions();
|
||||
|
||||
const { balance, history } = starsBalanceState || {};
|
||||
@ -62,13 +63,35 @@ const StarsBalanceModal = ({
|
||||
|
||||
const [isHeaderHidden, setHeaderHidden] = useState(true);
|
||||
const [selectedTabIndex, setSelectedTabIndex] = useState(0);
|
||||
const [areBuyOptionsShown, showBuyOptions, hideBuyOptions] = useFlag();
|
||||
|
||||
const isOpen = Boolean(modal && starsBalanceState);
|
||||
|
||||
const productStarsPrice = modal?.originPayment?.invoice?.amount;
|
||||
const starsNeeded = productStarsPrice ? productStarsPrice - (balance || 0) : undefined;
|
||||
const originBotName = originPaymentBot && getUserFullName(originPaymentBot);
|
||||
const shouldShowTransactions = Boolean(history?.all?.transactions.length && !modal?.originPayment);
|
||||
const { originPayment, originReaction } = modal || {};
|
||||
|
||||
const ongoingTransactionAmount = originPayment?.invoice?.amount || originReaction?.amount;
|
||||
const starsNeeded = ongoingTransactionAmount ? ongoingTransactionAmount - (balance || 0) : undefined;
|
||||
const starsNeededText = useMemo(() => {
|
||||
if (!starsNeeded || starsNeeded < 0) return undefined;
|
||||
const global = getGlobal();
|
||||
|
||||
if (originReaction) {
|
||||
const channel = selectChat(global, originReaction.chatId);
|
||||
if (!channel) return undefined;
|
||||
return oldLang('StarsNeededTextReactions', getChatTitle(oldLang, channel));
|
||||
}
|
||||
|
||||
if (originPayment) {
|
||||
const bot = selectUser(global, originPayment.botId!);
|
||||
if (!bot) return undefined;
|
||||
return oldLang('StarsNeededText', getUserFullName(bot));
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}, [oldLang, originPayment, originReaction, starsNeeded]);
|
||||
|
||||
const shouldShowTransactions = Boolean(history?.all?.transactions.length && !originPayment && !originReaction);
|
||||
const shouldSuggestGifting = !originPayment && !originReaction;
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
@ -77,6 +100,15 @@ const StarsBalanceModal = ({
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
useEffect(() => {
|
||||
if (ongoingTransactionAmount) {
|
||||
showBuyOptions();
|
||||
return;
|
||||
}
|
||||
|
||||
hideBuyOptions();
|
||||
}, [ongoingTransactionAmount]);
|
||||
|
||||
const tosText = useMemo(() => {
|
||||
if (!isOpen) return undefined;
|
||||
|
||||
@ -105,8 +137,13 @@ const StarsBalanceModal = ({
|
||||
openStarsGiftingModal({});
|
||||
});
|
||||
|
||||
const openStarsInfoModalHandler = useLastCallback(() => {
|
||||
openStarsGiftModal({});
|
||||
const handleBuyStars = useLastCallback((option: ApiStarTopupOption) => {
|
||||
openInvoice({
|
||||
type: 'stars',
|
||||
stars: option.stars,
|
||||
currency: option.currency,
|
||||
amount: option.amount,
|
||||
});
|
||||
});
|
||||
|
||||
return (
|
||||
@ -137,19 +174,19 @@ const StarsBalanceModal = ({
|
||||
</h2>
|
||||
<div className={styles.description}>
|
||||
{renderText(
|
||||
starsNeeded ? oldLang('StarsNeededText', originBotName) : oldLang('TelegramStarsInfo'),
|
||||
starsNeededText || oldLang('TelegramStarsInfo'),
|
||||
['simple_markdown', 'emoji'],
|
||||
)}
|
||||
</div>
|
||||
{canBuyPremium && (
|
||||
{canBuyPremium && !areBuyOptionsShown && (
|
||||
<Button
|
||||
className={styles.starButton}
|
||||
onClick={openStarsInfoModalHandler}
|
||||
onClick={showBuyOptions}
|
||||
>
|
||||
{oldLang('Star.List.BuyMoreStars')}
|
||||
</Button>
|
||||
)}
|
||||
{canBuyPremium && (
|
||||
{canBuyPremium && !areBuyOptionsShown && shouldSuggestGifting && (
|
||||
<Button
|
||||
className={buildClassName(styles.starButton, 'settings-main-menu-star')}
|
||||
color="translucent"
|
||||
@ -159,6 +196,13 @@ const StarsBalanceModal = ({
|
||||
{oldLang('TelegramStarsGift')}
|
||||
</Button>
|
||||
)}
|
||||
{areBuyOptionsShown && starsBalanceState?.topupOptions && (
|
||||
<StarTopupOptionList
|
||||
starsNeeded={starsNeeded}
|
||||
options={starsBalanceState.topupOptions}
|
||||
onClick={handleBuyStars}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.secondaryInfo}>
|
||||
{tosText}
|
||||
@ -201,13 +245,9 @@ const StarsBalanceModal = ({
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { modal }): StateProps => {
|
||||
const botId = modal?.originPayment?.botId;
|
||||
const bot = botId ? selectUser(global, botId) : undefined;
|
||||
|
||||
(global): StateProps => {
|
||||
return {
|
||||
starsBalanceState: global.stars,
|
||||
originPaymentBot: bot,
|
||||
canBuyPremium: !selectIsPremiumPurchaseBlocked(global),
|
||||
};
|
||||
},
|
||||
|
||||
@ -12,8 +12,8 @@
|
||||
}
|
||||
|
||||
.preview {
|
||||
height: 3rem;
|
||||
width: 3rem;
|
||||
height: 2.75rem;
|
||||
width: 2.75rem;
|
||||
|
||||
grid-auto-columns: 0.25rem;
|
||||
grid-auto-rows: 0.25rem;
|
||||
|
||||
@ -50,7 +50,15 @@ const StarsTransactionItem = ({ transaction }: OwnProps) => {
|
||||
const peer = useSelector(selectOptionalPeer(peerId));
|
||||
|
||||
const data = useMemo(() => {
|
||||
let title = transaction.title || (transaction.extendedMedia ? lang('StarMediaPurchase') : undefined);
|
||||
let title = transaction.title;
|
||||
if (transaction.extendedMedia) {
|
||||
title = lang('StarMediaPurchase');
|
||||
}
|
||||
|
||||
if (transaction.isReaction) {
|
||||
title = lang('StarsReactionsSent');
|
||||
}
|
||||
|
||||
let description;
|
||||
let status: string | undefined;
|
||||
let avatarPeer: ApiPeer | CustomPeer | undefined;
|
||||
|
||||
@ -133,7 +133,18 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
|
||||
const peerId = transaction.peer?.type === 'peer' ? transaction.peer.id : undefined;
|
||||
const toName = transaction.peer && oldLang(getStarsPeerTitleKey(transaction.peer));
|
||||
|
||||
const title = transaction.title || (customPeer ? oldLang(customPeer.titleKey) : undefined);
|
||||
let title = transaction.title;
|
||||
if (!title && customPeer) {
|
||||
title = oldLang(customPeer.titleKey);
|
||||
}
|
||||
|
||||
if (!title && transaction.extendedMedia) {
|
||||
title = oldLang('StarMediaPurchase');
|
||||
}
|
||||
|
||||
if (!title && transaction.isReaction) {
|
||||
title = oldLang('StarsReactionsSent');
|
||||
}
|
||||
|
||||
const messageLink = peer && transaction.messageId
|
||||
? getMessageLink(peer, undefined, transaction.messageId) : undefined;
|
||||
@ -198,7 +209,7 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
|
||||
]);
|
||||
|
||||
if (messageLink) {
|
||||
tableData.push([oldLang('Stars.Transaction.Media'), <SafeLink url={messageLink} text={messageLink} />]);
|
||||
tableData.push([oldLang('Stars.Transaction.Reaction.Post'), <SafeLink url={messageLink} text={messageLink} />]);
|
||||
}
|
||||
|
||||
if (isPrizeStars) {
|
||||
|
||||
@ -18,7 +18,7 @@ import { selectChat, selectChatFullInfo } from '../../../global/selectors';
|
||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import ReactionStaticEmoji from '../../common/ReactionStaticEmoji';
|
||||
import ReactionStaticEmoji from '../../common/reactions/ReactionStaticEmoji';
|
||||
import Checkbox from '../../ui/Checkbox';
|
||||
import FloatingActionButton from '../../ui/FloatingActionButton';
|
||||
import RadioGroup from '../../ui/RadioGroup';
|
||||
|
||||
@ -4,7 +4,9 @@ import { getActions, getGlobal } from '../../global';
|
||||
import type { ApiStory } from '../../api/types';
|
||||
|
||||
import { HEART_REACTION } from '../../config';
|
||||
import { getStoryKey, isUserId } from '../../global/helpers';
|
||||
import {
|
||||
getReactionKey, getStoryKey, isSameReaction, isUserId,
|
||||
} from '../../global/helpers';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
@ -35,8 +37,7 @@ const StoryFooter = ({
|
||||
const { viewsCount, forwardsCount, reactionsCount } = views || {};
|
||||
const isChannel = !isUserId(peerId);
|
||||
|
||||
const isSentStoryReactionHeart = sentReaction && 'emoticon' in sentReaction
|
||||
? sentReaction.emoticon === HEART_REACTION.emoticon : false;
|
||||
const isSentStoryReactionHeart = sentReaction && isSameReaction(sentReaction, HEART_REACTION);
|
||||
|
||||
const canForward = Boolean(
|
||||
(isOut || isChannel)
|
||||
@ -152,7 +153,7 @@ const StoryFooter = ({
|
||||
>
|
||||
{sentReaction && (
|
||||
<ReactionAnimatedEmoji
|
||||
key={'documentId' in sentReaction ? sentReaction.documentId : sentReaction.emoticon}
|
||||
key={getReactionKey(sentReaction)}
|
||||
containerId={containerId}
|
||||
reaction={sentReaction}
|
||||
withEffectOnly={isSentStoryReactionHeart}
|
||||
|
||||
@ -17,7 +17,7 @@ import useOldLang from '../../hooks/useOldLang';
|
||||
|
||||
import GroupChatInfo from '../common/GroupChatInfo';
|
||||
import PrivateChatInfo from '../common/PrivateChatInfo';
|
||||
import ReactionStaticEmoji from '../common/ReactionStaticEmoji';
|
||||
import ReactionStaticEmoji from '../common/reactions/ReactionStaticEmoji';
|
||||
import ListItem, { type MenuItemContextAction } from '../ui/ListItem';
|
||||
|
||||
import styles from './StoryViewModal.module.scss';
|
||||
|
||||
@ -35,11 +35,11 @@
|
||||
|
||||
&.slim {
|
||||
.modal-dialog {
|
||||
max-width: 25rem;
|
||||
max-width: 26.25rem;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
max-height: min(92vh, 32rem);
|
||||
max-height: min(92vh, 36rem);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -23,13 +23,10 @@
|
||||
}
|
||||
|
||||
.Notification {
|
||||
background:
|
||||
rgba(32, 32, 32, 0.8)
|
||||
url("data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iMjQiIGhlaWdodD0iMjQiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIj48Y2lyY2xlIGZpbGw9IiNGRkYiIGN4PSIxMiIgY3k9IjEyIiByPSIxMiIvPjxjaXJjbGUgZmlsbD0iIzAwMCIgZmlsbC1ydWxlPSJub256ZXJvIiBjeD0iMTIiIGN5PSI2LjUiIHI9IjEuNSIvPjxwYXRoIGQ9Ik0xMiA5LjVjLS41NTIgMC0xIC4zNy0xIC44MjZ2Ny4zNDhjMCAuNDU2LjQ0OC44MjYgMSAuODI2czEtLjM3IDEtLjgyNnYtNy4zNDhjMC0uNDU2LS40NDgtLjgyNi0xLS44MjZ6IiBmaWxsPSIjMDAwIiBmaWxsLXJ1bGU9Im5vbnplcm8iLz48L2c+PC9zdmc+")
|
||||
no-repeat 0.9375rem 50%;
|
||||
background-color: rgba(32, 32, 32, 0.8);
|
||||
background-size: 1.5rem;
|
||||
border-radius: var(--border-radius-default);
|
||||
padding: 0.9375rem 0.9375rem 0.9375rem 3.375rem;
|
||||
padding: 0.9375rem;
|
||||
color: #fff;
|
||||
margin: 0 0.5rem;
|
||||
display: flex;
|
||||
@ -58,11 +55,20 @@
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.Notification-button {
|
||||
.notification-icon {
|
||||
font-size: 1.75rem;
|
||||
margin-inline-end: 0.75rem;
|
||||
}
|
||||
|
||||
.notification-timer {
|
||||
margin-inline: 0.75rem;
|
||||
}
|
||||
|
||||
.notification-button {
|
||||
color: var(--color-primary);
|
||||
font-weight: 500;
|
||||
text-transform: none;
|
||||
margin: 0 0.5rem;
|
||||
margin-inline-start: 0.125rem;
|
||||
height: 2rem;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import type { FC, TeactNode } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
@ -8,37 +8,55 @@ import React, {
|
||||
import { getActions } from '../../global';
|
||||
|
||||
import type { CallbackAction } from '../../global/types';
|
||||
import type { TextPart } from '../../types';
|
||||
import type { IconName } from '../../types/icons';
|
||||
|
||||
import { ANIMATION_END_DELAY } from '../../config';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import captureEscKeyListener from '../../util/captureEscKeyListener';
|
||||
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useShowTransitionDeprecated from '../../hooks/useShowTransitionDeprecated';
|
||||
|
||||
import Icon from '../common/icons/Icon';
|
||||
import Button from './Button';
|
||||
import Portal from './Portal';
|
||||
import RoundTimer from './RoundTimer';
|
||||
|
||||
import './Notification.scss';
|
||||
|
||||
type OwnProps = {
|
||||
title?: TextPart[];
|
||||
title?: TeactNode;
|
||||
containerId?: string;
|
||||
message: TextPart[];
|
||||
message: TeactNode;
|
||||
duration?: number;
|
||||
onDismiss: () => void;
|
||||
action?: CallbackAction | CallbackAction[];
|
||||
actionText?: string;
|
||||
className?: string;
|
||||
icon?: IconName;
|
||||
shouldDisableClickDismiss?: boolean;
|
||||
dismissAction?: CallbackAction;
|
||||
shouldShowTimer?: boolean;
|
||||
cacheBreaker?: string;
|
||||
onDismiss: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
const DEFAULT_DURATION = 3000;
|
||||
const ANIMATION_DURATION = 150;
|
||||
|
||||
const Notification: FC<OwnProps> = ({
|
||||
title, className,
|
||||
message, duration = DEFAULT_DURATION, containerId, onDismiss,
|
||||
action, actionText,
|
||||
title,
|
||||
className,
|
||||
message,
|
||||
duration = DEFAULT_DURATION,
|
||||
containerId,
|
||||
icon,
|
||||
action,
|
||||
actionText,
|
||||
shouldDisableClickDismiss,
|
||||
dismissAction,
|
||||
shouldShowTimer,
|
||||
cacheBreaker,
|
||||
onDismiss,
|
||||
}) => {
|
||||
const actions = getActions();
|
||||
|
||||
@ -47,10 +65,15 @@ const Notification: FC<OwnProps> = ({
|
||||
const timerRef = useRef<number | undefined>(null);
|
||||
const { transitionClassNames } = useShowTransitionDeprecated(isOpen);
|
||||
|
||||
const closeAndDismiss = useCallback(() => {
|
||||
const closeAndDismiss = useLastCallback((force?: boolean) => {
|
||||
if (!force && shouldDisableClickDismiss) return;
|
||||
setIsOpen(false);
|
||||
setTimeout(onDismiss, ANIMATION_DURATION + ANIMATION_END_DELAY);
|
||||
}, [onDismiss]);
|
||||
if (dismissAction) {
|
||||
// @ts-ignore
|
||||
actions[dismissAction.action](dismissAction.payload);
|
||||
}
|
||||
});
|
||||
|
||||
const handleClick = useCallback(() => {
|
||||
if (action) {
|
||||
@ -68,7 +91,7 @@ const Notification: FC<OwnProps> = ({
|
||||
useEffect(() => (isOpen ? captureEscKeyListener(closeAndDismiss) : undefined), [isOpen, closeAndDismiss]);
|
||||
|
||||
useEffect(() => {
|
||||
timerRef.current = window.setTimeout(closeAndDismiss, duration);
|
||||
timerRef.current = window.setTimeout(() => closeAndDismiss(true), duration);
|
||||
|
||||
return () => {
|
||||
if (timerRef.current) {
|
||||
@ -76,18 +99,23 @@ const Notification: FC<OwnProps> = ({
|
||||
timerRef.current = undefined;
|
||||
}
|
||||
};
|
||||
}, [duration, closeAndDismiss]);
|
||||
}, [duration, cacheBreaker]); // Reset timer if `cacheBreaker` changes
|
||||
|
||||
const handleMouseEnter = useCallback(() => {
|
||||
const handleMouseEnter = useLastCallback(() => {
|
||||
if (shouldDisableClickDismiss) return;
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
timerRef.current = undefined;
|
||||
}
|
||||
}, []);
|
||||
});
|
||||
|
||||
const handleMouseLeave = useCallback(() => {
|
||||
const handleMouseLeave = useLastCallback(() => {
|
||||
if (shouldDisableClickDismiss) return;
|
||||
if (timerRef.current) {
|
||||
clearTimeout(timerRef.current);
|
||||
}
|
||||
timerRef.current = window.setTimeout(closeAndDismiss, duration);
|
||||
}, [duration, closeAndDismiss]);
|
||||
});
|
||||
|
||||
return (
|
||||
<Portal className="Notification-container" containerId={containerId}>
|
||||
@ -97,6 +125,7 @@ const Notification: FC<OwnProps> = ({
|
||||
onMouseEnter={handleMouseEnter}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
>
|
||||
<Icon name={icon || 'info-filled'} className="notification-icon" />
|
||||
<div className="content">
|
||||
{title && <div className="notification-title">{title}</div>}
|
||||
{message}
|
||||
@ -105,11 +134,14 @@ const Notification: FC<OwnProps> = ({
|
||||
<Button
|
||||
color="translucent-white"
|
||||
onClick={handleClick}
|
||||
className="Notification-button"
|
||||
className="notification-button"
|
||||
>
|
||||
{actionText}
|
||||
</Button>
|
||||
)}
|
||||
{shouldShowTimer && (
|
||||
<RoundTimer className="notification-timer" key={cacheBreaker} duration={Math.ceil(duration / 1000)} />
|
||||
)}
|
||||
</div>
|
||||
</Portal>
|
||||
);
|
||||
|
||||
24
src/components/ui/RoundTimer.module.scss
Normal file
24
src/components/ui/RoundTimer.module.scss
Normal file
@ -0,0 +1,24 @@
|
||||
.root {
|
||||
position: relative;
|
||||
color: var(--color-primary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.svg {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.circle {
|
||||
stroke: var(--color-primary);
|
||||
fill: transparent;
|
||||
stroke-width: 2;
|
||||
stroke-linecap: round;
|
||||
transition: stroke-dashoffset 1s linear, stroke 0.2s;
|
||||
|
||||
@starting-style {
|
||||
stroke-dashoffset: 0;
|
||||
}
|
||||
}
|
||||
60
src/components/ui/RoundTimer.tsx
Normal file
60
src/components/ui/RoundTimer.tsx
Normal file
@ -0,0 +1,60 @@
|
||||
import React, { memo, useEffect, useState } from '../../lib/teact/teact';
|
||||
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { formatCountdownShort } from '../../util/dates/dateFormat';
|
||||
|
||||
import useInterval from '../../hooks/schedulers/useInterval';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
|
||||
import AnimatedCounter from '../common/AnimatedCounter';
|
||||
|
||||
import styles from './RoundTimer.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
duration: number;
|
||||
className?: string;
|
||||
onEnd?: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
const UPDATE_FREQUENCY = 1000;
|
||||
const TIMER_RADIUS = 14;
|
||||
|
||||
const RoundTimer = ({ duration, className, onEnd }: OwnProps) => {
|
||||
const [timeLeft, setTimeLeft] = useState(duration);
|
||||
const lang = useOldLang();
|
||||
|
||||
useInterval(
|
||||
() => setTimeLeft((prev) => prev - 1),
|
||||
timeLeft > 0 ? UPDATE_FREQUENCY : undefined,
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (timeLeft <= 0) {
|
||||
onEnd?.();
|
||||
}
|
||||
}, [timeLeft, onEnd]);
|
||||
|
||||
useEffect(() => {
|
||||
setTimeLeft(duration);
|
||||
}, [duration]);
|
||||
|
||||
return (
|
||||
<div className={buildClassName(styles.root, className)}>
|
||||
<svg className={styles.svg} width="32px" height="32px">
|
||||
<circle
|
||||
cx="16"
|
||||
cy="16"
|
||||
r={TIMER_RADIUS}
|
||||
transform="rotate(-90, 16, 16)"
|
||||
pathLength="100"
|
||||
stroke-dasharray="100"
|
||||
stroke-dashoffset={100 - ((timeLeft - 1) / duration) * 100} // Show it one step further due to animation
|
||||
className={styles.circle}
|
||||
/>
|
||||
</svg>
|
||||
<AnimatedCounter className={styles.text} text={formatCountdownShort(lang, timeLeft * 1000)} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(RoundTimer);
|
||||
@ -295,6 +295,7 @@ export const COUNTRIES_WITH_12H_TIME_FORMAT = new Set(['AU', 'BD', 'CA', 'CO', '
|
||||
export const API_CHAT_TYPES = ['bots', 'channels', 'chats', 'users'] as const;
|
||||
|
||||
export const HEART_REACTION: ApiReactionEmoji = {
|
||||
type: 'emoji',
|
||||
emoticon: '❤',
|
||||
};
|
||||
|
||||
|
||||
@ -233,8 +233,6 @@ addActionHandler('openChat', (global, actions, payload): ActionReturnType => {
|
||||
} else if (isChatOnlySummary && !chat.isMin) {
|
||||
actions.requestChatUpdate({ chatId: id });
|
||||
}
|
||||
actions.closeStoryViewer({ tabId });
|
||||
actions.closeStarsBalanceModal({ tabId });
|
||||
});
|
||||
|
||||
addActionHandler('openSavedDialog', (global, actions, payload): ActionReturnType => {
|
||||
|
||||
@ -2094,6 +2094,11 @@ addActionHandler('loadFactChecks', async (global, actions, payload): Promise<voi
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('loadPaidReactionPrivacy', (): ActionReturnType => {
|
||||
callApi('fetchPaidReactionPrivacy');
|
||||
return undefined;
|
||||
});
|
||||
|
||||
addActionHandler('loadOutboxReadDate', async (global, actions, payload): Promise<void> => {
|
||||
const { chatId, messageId } = payload;
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { ApiReactionEmoji } from '../../../api/types';
|
||||
import type { ApiError, ApiReactionEmoji } from '../../../api/types';
|
||||
import type { ActionReturnType } from '../../types';
|
||||
import { ApiMediaFormat } from '../../../api/types';
|
||||
|
||||
@ -12,6 +12,7 @@ import * as mediaLoader from '../../../util/mediaLoader';
|
||||
import requestActionTimeout from '../../../util/requestActionTimeout';
|
||||
import { callApi } from '../../../api/gramjs';
|
||||
import {
|
||||
addPaidReaction,
|
||||
getDocumentMediaHash,
|
||||
getReactionKey,
|
||||
getUserReactions,
|
||||
@ -92,6 +93,7 @@ addActionHandler('loadAvailableEffects', async (global): Promise<void> => {
|
||||
for (const effect of effects) {
|
||||
if (effect.effectAnimationId) {
|
||||
const reaction: ApiReactionEmoji = {
|
||||
type: 'emoji',
|
||||
emoticon: effect.emoticon,
|
||||
};
|
||||
reactions.push(reaction);
|
||||
@ -240,6 +242,71 @@ addActionHandler('toggleReaction', async (global, actions, payload): Promise<voi
|
||||
}
|
||||
});
|
||||
|
||||
addActionHandler('addLocalPaidReaction', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
chatId, messageId, count, isPrivate, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
const chat = selectChat(global, chatId);
|
||||
const message = selectChatMessage(global, chatId, messageId);
|
||||
|
||||
if (!chat || !message) {
|
||||
return;
|
||||
}
|
||||
|
||||
const currentReactions = message.reactions?.results || [];
|
||||
const newReactions = addPaidReaction(currentReactions, count, isPrivate);
|
||||
global = updateChatMessage(global, message.chatId, message.id, {
|
||||
reactions: {
|
||||
...currentReactions,
|
||||
results: newReactions,
|
||||
},
|
||||
});
|
||||
setGlobal(global);
|
||||
|
||||
const messageKey = getMessageKey(message);
|
||||
if (selectPerformanceSettingsValue(global, 'reactionEffects')) {
|
||||
actions.startActiveReaction({
|
||||
containerId: messageKey,
|
||||
reaction: {
|
||||
type: 'paid',
|
||||
},
|
||||
tabId,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
addActionHandler('sendPaidReaction', async (global, actions, payload): Promise<void> => {
|
||||
const {
|
||||
chatId, messageId, forcedAmount, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
const chat = selectChat(global, chatId);
|
||||
const message = selectChatMessage(global, chatId, messageId);
|
||||
|
||||
if (!chat || !message) {
|
||||
return;
|
||||
}
|
||||
|
||||
const paidReaction = message.reactions?.results?.find((r) => r.reaction.type === 'paid');
|
||||
const count = forcedAmount || paidReaction?.localAmount || 0;
|
||||
if (!count) {
|
||||
return;
|
||||
}
|
||||
actions.resetLocalPaidReactions({ chatId, messageId });
|
||||
|
||||
try {
|
||||
await callApi('sendPaidReaction', {
|
||||
chat,
|
||||
messageId,
|
||||
count,
|
||||
isPrivate: paidReaction?.localIsPrivate,
|
||||
});
|
||||
} catch (error) {
|
||||
if ((error as ApiError).message === 'BALANCE_TOO_LOW') {
|
||||
actions.openStarsBalanceModal({ originReaction: { chatId, messageId, amount: count }, tabId });
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
addActionHandler('startActiveReaction', (global, actions, payload): ActionReturnType => {
|
||||
const { containerId, reaction, tabId = getCurrentTabId() } = payload;
|
||||
const tabState = selectTabState(global, tabId);
|
||||
|
||||
@ -19,6 +19,7 @@ import { getMessageKey, isLocalMessageId } from '../../../util/keys/messageKey';
|
||||
import { notifyAboutMessage } from '../../../util/notifications';
|
||||
import { onTickEnd } from '../../../util/schedulers';
|
||||
import {
|
||||
addPaidReaction,
|
||||
checkIfHasUnreadReactions, getIsSavedDialog, getMessageContent, getMessageText, isActionMessage,
|
||||
isMessageLocal, isUserId,
|
||||
} from '../../helpers';
|
||||
@ -788,6 +789,12 @@ function updateReactions<T extends GlobalState>(
|
||||
return global;
|
||||
}
|
||||
|
||||
const localPaidReaction = currentReactions?.results.find((r) => r.localAmount);
|
||||
// Save local count on update, but reset if we sent reaction
|
||||
if (localPaidReaction?.localAmount) {
|
||||
reactions.results = addPaidReaction(reactions.results, localPaidReaction.localAmount);
|
||||
}
|
||||
|
||||
global = updateChatMessage(global, chatId, id, { reactions });
|
||||
|
||||
if (!isOutgoing) {
|
||||
|
||||
@ -172,6 +172,19 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
actions.processPremiumFloodWait({
|
||||
isUpload: update.isUpload,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'updatePaidReactionPrivacy': {
|
||||
return {
|
||||
...global,
|
||||
settings: {
|
||||
...global.settings,
|
||||
paidReactionPrivacy: update.isPrivate,
|
||||
},
|
||||
};
|
||||
setGlobal(global);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -38,6 +38,11 @@ addActionHandler('processOpenChatOrThread', (global, actions, payload): ActionRe
|
||||
}
|
||||
actions.hideEffectInComposer({ tabId });
|
||||
|
||||
actions.closeStoryViewer({ tabId });
|
||||
actions.closeStarsBalanceModal({ tabId });
|
||||
actions.closeStarsBalanceModal({ tabId });
|
||||
actions.closeStarsTransactionModal({ tabId });
|
||||
|
||||
if (!currentMessageList || (
|
||||
currentMessageList.chatId !== chatId
|
||||
|| currentMessageList.threadId !== threadId
|
||||
|
||||
@ -948,6 +948,20 @@ addActionHandler('openPreviousReportAdModal', (global, actions, payload): Action
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('openPaidReactionModal', (global, actions, payload): ActionReturnType => {
|
||||
const { chatId, messageId, tabId = getCurrentTabId() } = payload;
|
||||
return updateTabState(global, {
|
||||
paidReactionModal: { chatId, messageId },
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('closePaidReactionModal', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
return updateTabState(global, {
|
||||
paidReactionModal: undefined,
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
function copyTextForMessages(global: GlobalState, chatId: string, messageIds: number[]) {
|
||||
const { type: messageListType, threadId } = selectCurrentMessageList(global) || {};
|
||||
const lang = langProvider.oldTranslate;
|
||||
|
||||
@ -305,10 +305,13 @@ addActionHandler('reorderStickerSets', (global, actions, payload): ActionReturnT
|
||||
|
||||
addActionHandler('showNotification', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId(), ...notification } = payload;
|
||||
notification.localId = generateUniqueId();
|
||||
const hasLocalId = notification.localId;
|
||||
notification.localId ||= generateUniqueId();
|
||||
|
||||
const newNotifications = [...selectTabState(global, tabId).notifications];
|
||||
const existingNotificationIndex = newNotifications.findIndex((n) => n.message === notification.message);
|
||||
const existingNotificationIndex = newNotifications.findIndex((n) => (
|
||||
hasLocalId ? n.localId === notification.localId : n.message === notification.message
|
||||
));
|
||||
if (existingNotificationIndex !== -1) {
|
||||
newNotifications.splice(existingNotificationIndex, 1);
|
||||
}
|
||||
@ -522,7 +525,7 @@ addActionHandler('setReactionEffect', (global, actions, payload): ActionReturnTy
|
||||
chatId, threadId, reaction, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
|
||||
const emoticon = reaction && 'emoticon' in reaction && reaction.emoticon;
|
||||
const emoticon = reaction?.type === 'emoji' && reaction.emoticon;
|
||||
if (!emoticon) return;
|
||||
|
||||
const effect = Object.values(global.availableEffectById)
|
||||
|
||||
@ -12,7 +12,10 @@ addActionHandler('closePaymentModal', (global, actions, payload): ActionReturnTy
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
const payment = selectTabState(global, tabId).payment;
|
||||
const status = payment.status || 'cancelled';
|
||||
const originPayment = selectTabState(global, tabId).starsBalanceModal?.originPayment;
|
||||
const starsBalanceModal = selectTabState(global, tabId).starsBalanceModal;
|
||||
const originPayment = starsBalanceModal?.originPayment;
|
||||
const originReaction = starsBalanceModal?.originReaction;
|
||||
|
||||
global = clearPayment(global, tabId);
|
||||
global = closeInvoice(global, tabId);
|
||||
global = updateTabState(global, {
|
||||
@ -20,7 +23,7 @@ addActionHandler('closePaymentModal', (global, actions, payload): ActionReturnTy
|
||||
...selectTabState(global, tabId).payment,
|
||||
status,
|
||||
},
|
||||
...(originPayment && {
|
||||
...((originPayment || originReaction) && {
|
||||
starsBalanceModal: undefined,
|
||||
}),
|
||||
}, tabId);
|
||||
@ -32,6 +35,16 @@ addActionHandler('closePaymentModal', (global, actions, payload): ActionReturnTy
|
||||
isStarPaymentModalOpen: true,
|
||||
}, tabId);
|
||||
}
|
||||
|
||||
// Send reaction
|
||||
if (originReaction) {
|
||||
actions.sendPaidReaction({
|
||||
chatId: originReaction.chatId,
|
||||
messageId: originReaction.messageId,
|
||||
forcedAmount: originReaction.amount,
|
||||
tabId,
|
||||
});
|
||||
}
|
||||
return global;
|
||||
});
|
||||
|
||||
@ -56,13 +69,17 @@ addActionHandler('closeGiftCodeModal', (global, actions, payload): ActionReturnT
|
||||
});
|
||||
|
||||
addActionHandler('openStarsBalanceModal', (global, actions, payload): ActionReturnType => {
|
||||
const { originPayment, tabId = getCurrentTabId() } = payload || {};
|
||||
const { originPayment, originReaction, tabId = getCurrentTabId() } = payload || {};
|
||||
|
||||
global = clearPayment(global, tabId);
|
||||
|
||||
// Always refresh status on opening
|
||||
actions.loadStarStatus();
|
||||
|
||||
return updateTabState(global, {
|
||||
starsBalanceModal: {
|
||||
originPayment,
|
||||
originReaction,
|
||||
},
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import type { ActionReturnType } from '../../types';
|
||||
|
||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||
import { getMessageKey } from '../../../util/keys/messageKey';
|
||||
import { addActionHandler } from '../../index';
|
||||
import { updateChatMessage } from '../../reducers';
|
||||
import { updateTabState } from '../../reducers/tabs';
|
||||
import { selectTabState } from '../../selectors';
|
||||
import { selectChatMessage, selectTabState } from '../../selectors';
|
||||
|
||||
addActionHandler('processOpenChatOrThread', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
@ -32,7 +34,7 @@ addActionHandler('openMessageReactionPicker', (global, actions, payload): Action
|
||||
messageId,
|
||||
position,
|
||||
tabId = getCurrentTabId(),
|
||||
} = payload!;
|
||||
} = payload;
|
||||
|
||||
return updateTabState(global, {
|
||||
reactionPicker: {
|
||||
@ -50,7 +52,7 @@ addActionHandler('openStoryReactionPicker', (global, actions, payload): ActionRe
|
||||
position,
|
||||
sendAsMessage,
|
||||
tabId = getCurrentTabId(),
|
||||
} = payload!;
|
||||
} = payload;
|
||||
|
||||
return updateTabState(global, {
|
||||
reactionPicker: {
|
||||
@ -67,7 +69,7 @@ addActionHandler('openEffectPicker', (global, actions, payload): ActionReturnTyp
|
||||
position,
|
||||
chatId,
|
||||
tabId = getCurrentTabId(),
|
||||
} = payload!;
|
||||
} = payload;
|
||||
|
||||
return updateTabState(global, {
|
||||
reactionPicker: {
|
||||
@ -93,3 +95,43 @@ addActionHandler('closeReactionPicker', (global, actions, payload): ActionReturn
|
||||
},
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('resetLocalPaidReactions', (global, actions, payload): ActionReturnType => {
|
||||
const { chatId, messageId } = payload;
|
||||
const message = selectChatMessage(global, chatId, messageId);
|
||||
if (!message) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { reactions } = message;
|
||||
|
||||
if (!reactions) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const updatedResults = reactions.results.map((reaction) => {
|
||||
if (reaction.localAmount) {
|
||||
if (!reaction.count) return undefined;
|
||||
return {
|
||||
...reaction,
|
||||
localAmount: undefined,
|
||||
};
|
||||
}
|
||||
return reaction;
|
||||
}).filter(Boolean);
|
||||
|
||||
Object.values(global.byTabId)
|
||||
.forEach(({ id: tabId }) => {
|
||||
actions.dismissNotification({
|
||||
localId: getMessageKey(message),
|
||||
tabId,
|
||||
});
|
||||
});
|
||||
|
||||
return updateChatMessage(global, chatId, messageId, {
|
||||
reactions: {
|
||||
...reactions,
|
||||
results: updatedResults,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
@ -245,6 +245,11 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
|
||||
if (!cached.topBotApps) {
|
||||
cached.topBotApps = initialState.topBotApps;
|
||||
}
|
||||
|
||||
if (!cached.reactions.defaultTags?.[0]?.type) {
|
||||
cached.reactions = initialState.reactions;
|
||||
}
|
||||
|
||||
if (!cached.users.commonChatsById) {
|
||||
cached.users.commonChatsById = initialState.users.commonChatsById;
|
||||
}
|
||||
@ -523,7 +528,8 @@ function reduceMessages<T extends GlobalState>(global: T): GlobalState['messages
|
||||
const cleanedById = Object.values(byId).reduce((acc, message) => {
|
||||
if (!message) return acc;
|
||||
|
||||
const cleanedMessage = omitLocalMedia(message);
|
||||
let cleanedMessage = omitLocalMedia(message);
|
||||
cleanedMessage = omitLocalPaidReactions(cleanedMessage);
|
||||
acc[message.id] = cleanedMessage;
|
||||
return acc;
|
||||
}, {} as Record<number, ApiMessage>);
|
||||
@ -540,6 +546,25 @@ function reduceMessages<T extends GlobalState>(global: T): GlobalState['messages
|
||||
};
|
||||
}
|
||||
|
||||
function omitLocalPaidReactions(message: ApiMessage): ApiMessage {
|
||||
if (!message.reactions?.results.length) return message;
|
||||
return {
|
||||
...message,
|
||||
reactions: {
|
||||
...message.reactions,
|
||||
results: message.reactions.results.map((reaction) => {
|
||||
if (reaction.localAmount) {
|
||||
return {
|
||||
...reaction,
|
||||
localAmount: undefined,
|
||||
};
|
||||
}
|
||||
return reaction;
|
||||
}),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function omitLocalMedia(message: ApiMessage): ApiMessage {
|
||||
const {
|
||||
photo, video, document, sticker,
|
||||
|
||||
@ -6,6 +6,7 @@ import type {
|
||||
ApiReactionCount,
|
||||
ApiReactionKey,
|
||||
ApiReactions,
|
||||
ApiReactionWithPaid,
|
||||
} from '../../api/types';
|
||||
import type { GlobalState } from '../types';
|
||||
|
||||
@ -20,18 +21,26 @@ export function checkIfHasUnreadReactions(global: GlobalState, reactions: ApiRea
|
||||
}
|
||||
|
||||
export function areReactionsEmpty(reactions: ApiReactions) {
|
||||
return !reactions.results.some(({ count }) => count > 0);
|
||||
return !reactions.results.some(({ count, localAmount }) => count || localAmount);
|
||||
}
|
||||
|
||||
export function getReactionKey(reaction: ApiReaction): ApiReactionKey {
|
||||
if ('emoticon' in reaction) {
|
||||
return `emoji-${reaction.emoticon}`;
|
||||
export function getReactionKey(reaction: ApiReactionWithPaid): ApiReactionKey {
|
||||
switch (reaction.type) {
|
||||
case 'emoji':
|
||||
return `emoji-${reaction.emoticon}`;
|
||||
case 'custom':
|
||||
return `document-${reaction.documentId}`;
|
||||
case 'paid':
|
||||
return 'paid';
|
||||
default: {
|
||||
// Legacy reactions
|
||||
const uniqueValue = (reaction as any).emoticon || (reaction as any).documentId;
|
||||
return `unsupported-${uniqueValue}`;
|
||||
}
|
||||
}
|
||||
|
||||
return `document-${reaction.documentId}`;
|
||||
}
|
||||
|
||||
export function isSameReaction(first?: ApiReaction, second?: ApiReaction) {
|
||||
export function isSameReaction(first?: ApiReactionWithPaid, second?: ApiReactionWithPaid) {
|
||||
if (first === second) {
|
||||
return true;
|
||||
}
|
||||
@ -43,9 +52,9 @@ export function isSameReaction(first?: ApiReaction, second?: ApiReaction) {
|
||||
return getReactionKey(first) === getReactionKey(second);
|
||||
}
|
||||
|
||||
export function canSendReaction(reaction: ApiReaction, chatReactions: ApiChatReactions) {
|
||||
export function canSendReaction(reaction: ApiReactionWithPaid, chatReactions: ApiChatReactions) {
|
||||
if (chatReactions.type === 'all') {
|
||||
return 'emoticon' in reaction || chatReactions.areCustomAllowed;
|
||||
return reaction.type === 'emoji' || chatReactions.areCustomAllowed;
|
||||
}
|
||||
|
||||
if (chatReactions.type === 'some') {
|
||||
@ -55,13 +64,17 @@ export function canSendReaction(reaction: ApiReaction, chatReactions: ApiChatRea
|
||||
return false;
|
||||
}
|
||||
|
||||
export function sortReactions<T extends ApiAvailableReaction | ApiReaction>(
|
||||
export function sortReactions<T extends ApiAvailableReaction | ApiReactionWithPaid>(
|
||||
reactions: T[],
|
||||
topReactions?: ApiReaction[],
|
||||
topReactions?: ApiReactionWithPaid[],
|
||||
): T[] {
|
||||
return reactions.slice().sort((left, right) => {
|
||||
const reactionOne = left ? ('reaction' in left ? left.reaction : left) as ApiReaction : undefined;
|
||||
const reactionTwo = right ? ('reaction' in right ? right.reaction : right) as ApiReaction : undefined;
|
||||
const reactionOne = left ? ('reaction' in left ? left.reaction : left) as ApiReactionWithPaid : undefined;
|
||||
const reactionTwo = right ? ('reaction' in right ? right.reaction : right) as ApiReactionWithPaid : undefined;
|
||||
|
||||
if (reactionOne?.type === 'paid') return -1;
|
||||
if (reactionTwo?.type === 'paid') return 1;
|
||||
|
||||
const indexOne = topReactions?.findIndex((reaction) => isSameReaction(reaction, reactionOne)) || 0;
|
||||
const indexTwo = topReactions?.findIndex((reaction) => isSameReaction(reaction, reactionTwo)) || 0;
|
||||
return (
|
||||
@ -73,7 +86,8 @@ export function sortReactions<T extends ApiAvailableReaction | ApiReaction>(
|
||||
export function getUserReactions(message: ApiMessage): ApiReaction[] {
|
||||
return message.reactions?.results?.filter((r): r is Required<ApiReactionCount> => isReactionChosen(r))
|
||||
.sort((a, b) => a.chosenOrder - b.chosenOrder)
|
||||
.map((r) => r.reaction) || [];
|
||||
.map((r) => r.reaction)
|
||||
.filter((r): r is ApiReaction => r.type !== 'paid') || [];
|
||||
}
|
||||
|
||||
export function isReactionChosen(reaction: ApiReactionCount) {
|
||||
@ -108,3 +122,37 @@ export function updateReactionCount(reactionCount: ApiReactionCount[], newReacti
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
export function addPaidReaction(
|
||||
reactionCount: ApiReactionCount[], count: number, isAnonymous?: boolean,
|
||||
): ApiReactionCount[] {
|
||||
const results: ApiReactionCount[] = [];
|
||||
const hasPaid = reactionCount.some((current) => current.reaction.type === 'paid');
|
||||
if (hasPaid) {
|
||||
reactionCount.forEach((current) => {
|
||||
if (current.reaction.type === 'paid') {
|
||||
results.push({
|
||||
...current,
|
||||
localAmount: (current.localAmount || 0) + count,
|
||||
chosenOrder: -1,
|
||||
localIsPrivate: isAnonymous !== undefined ? isAnonymous : current.localIsPrivate,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
results.push(current);
|
||||
});
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
return [
|
||||
{
|
||||
reaction: { type: 'paid' },
|
||||
count: 0,
|
||||
chosenOrder: -1,
|
||||
localAmount: count,
|
||||
},
|
||||
...reactionCount,
|
||||
];
|
||||
}
|
||||
|
||||
@ -57,6 +57,7 @@ import type {
|
||||
ApiQuickReply,
|
||||
ApiReaction,
|
||||
ApiReactionKey,
|
||||
ApiReactionWithPaid,
|
||||
ApiReceiptRegular,
|
||||
ApiReportReason,
|
||||
ApiSavedReactionTag,
|
||||
@ -422,7 +423,7 @@ export type TabState = {
|
||||
};
|
||||
|
||||
activeEmojiInteractions?: ActiveEmojiInteraction[];
|
||||
activeReactions: Record<string, ApiReaction[]>;
|
||||
activeReactions: Record<string, ApiReactionWithPaid[]>;
|
||||
|
||||
middleSearch: {
|
||||
byChatThreadKey: Record<string, MiddleSearchParams | undefined>;
|
||||
@ -824,6 +825,11 @@ export type TabState = {
|
||||
info: ApiCheckedGiftCode;
|
||||
};
|
||||
|
||||
paidReactionModal?: {
|
||||
chatId: string;
|
||||
messageId: number;
|
||||
};
|
||||
|
||||
inviteViaLinkModal?: {
|
||||
missingUsers: ApiMissingInvitedUser[];
|
||||
chatId: string;
|
||||
@ -841,6 +847,11 @@ export type TabState = {
|
||||
|
||||
starsBalanceModal?: {
|
||||
originPayment?: TabState['payment'];
|
||||
originReaction?: {
|
||||
chatId: string;
|
||||
messageId: number;
|
||||
amount: number;
|
||||
};
|
||||
};
|
||||
isStarPaymentModalOpen?: true;
|
||||
};
|
||||
@ -1172,6 +1183,7 @@ export type GlobalState = {
|
||||
privacy: Partial<Record<ApiPrivacyKey, ApiPrivacySettings>>;
|
||||
notifyExceptions?: Record<number, NotifyException>;
|
||||
lastPremiumBandwithNotificationDate?: number;
|
||||
paidReactionPrivacy?: boolean;
|
||||
};
|
||||
|
||||
push?: {
|
||||
@ -2294,6 +2306,11 @@ export interface ActionPayloads {
|
||||
};
|
||||
openStarsBalanceModal: {
|
||||
originPayment?: TabState['payment'];
|
||||
originReaction?: {
|
||||
chatId: string;
|
||||
messageId: number;
|
||||
amount: number;
|
||||
};
|
||||
} & WithTabId;
|
||||
closeStarsBalanceModal: WithTabId | undefined;
|
||||
|
||||
@ -2389,6 +2406,8 @@ export interface ActionPayloads {
|
||||
shouldIncludeGrouped?: boolean;
|
||||
} & WithTabId;
|
||||
|
||||
loadPaidReactionPrivacy: undefined;
|
||||
|
||||
sendPollVote: {
|
||||
chatId: string;
|
||||
messageId: number;
|
||||
@ -2454,6 +2473,23 @@ export interface ActionPayloads {
|
||||
shouldAddToRecent?: boolean;
|
||||
} & WithTabId;
|
||||
|
||||
sendPaidReaction: {
|
||||
chatId: string;
|
||||
messageId: number;
|
||||
forcedAmount?: number;
|
||||
isPrivate?: boolean;
|
||||
} & WithTabId;
|
||||
addLocalPaidReaction: {
|
||||
chatId: string;
|
||||
messageId: number;
|
||||
count: number;
|
||||
isPrivate?: boolean;
|
||||
} & WithTabId;
|
||||
resetLocalPaidReactions: {
|
||||
chatId: string;
|
||||
messageId: number;
|
||||
};
|
||||
|
||||
setDefaultReaction: {
|
||||
reaction: ApiReaction;
|
||||
};
|
||||
@ -2470,11 +2506,11 @@ export interface ActionPayloads {
|
||||
|
||||
startActiveReaction: {
|
||||
containerId: string;
|
||||
reaction: ApiReaction;
|
||||
reaction: ApiReactionWithPaid;
|
||||
} & WithTabId;
|
||||
stopActiveReaction: {
|
||||
containerId: string;
|
||||
reaction?: ApiReaction;
|
||||
reaction?: ApiReactionWithPaid;
|
||||
} & WithTabId;
|
||||
|
||||
openEffectPicker: {
|
||||
@ -3157,15 +3193,7 @@ export interface ActionPayloads {
|
||||
url?: string;
|
||||
} & WithTabId;
|
||||
closeUrlAuthModal: WithTabId | undefined;
|
||||
showNotification: {
|
||||
localId?: string;
|
||||
title?: string;
|
||||
message: string;
|
||||
className?: string;
|
||||
duration?: number;
|
||||
actionText?: string;
|
||||
action?: CallbackAction | CallbackAction[];
|
||||
} & WithTabId;
|
||||
showNotification: Omit<ApiNotification, 'localId'> & { localId?: string } & WithTabId;
|
||||
showAllowedMessageTypesNotification: {
|
||||
chatId: string;
|
||||
} & WithTabId;
|
||||
@ -3321,6 +3349,12 @@ export interface ActionPayloads {
|
||||
openStarsGiftingModal: WithTabId | undefined;
|
||||
closeStarsGiftingModal: WithTabId | undefined;
|
||||
|
||||
openPaidReactionModal: {
|
||||
chatId: string;
|
||||
messageId: number;
|
||||
} & WithTabId;
|
||||
closePaidReactionModal: WithTabId | undefined;
|
||||
|
||||
openDeleteMessageModal: ({
|
||||
message?: ApiMessage;
|
||||
isSchedule?: boolean;
|
||||
|
||||
@ -24,6 +24,7 @@ const useContextMenuHandlers = (
|
||||
shouldDisableOnLink?: boolean,
|
||||
shouldDisableOnLongTap?: boolean,
|
||||
getIsReady?: Signal<boolean>,
|
||||
shouldDisablePropagation?: boolean,
|
||||
) => {
|
||||
const [isContextMenuOpen, setIsContextMenuOpen] = useState(false);
|
||||
const [contextMenuAnchor, setContextMenuAnchor] = useState<IAnchorPosition | undefined>(undefined);
|
||||
@ -133,7 +134,7 @@ const useContextMenuHandlers = (
|
||||
if (isMenuDisabled) {
|
||||
return;
|
||||
}
|
||||
e.stopPropagation();
|
||||
if (shouldDisablePropagation) e.stopPropagation();
|
||||
clearLongPressTimer();
|
||||
|
||||
timer = window.setTimeout(() => emulateContextMenuEvent(e), LONG_TAP_DURATION_MS);
|
||||
@ -154,6 +155,7 @@ const useContextMenuHandlers = (
|
||||
};
|
||||
}, [
|
||||
contextMenuAnchor, isMenuDisabled, shouldDisableOnLongTap, elementRef, shouldDisableOnLink, getIsReady,
|
||||
shouldDisablePropagation,
|
||||
]);
|
||||
|
||||
return {
|
||||
|
||||
@ -1563,6 +1563,8 @@ messages.sendQuickReplyMessages#6c750de1 peer:InputPeer shortcut_id:int id:Vecto
|
||||
messages.getAvailableEffects#dea20a39 hash:int = messages.AvailableEffects;
|
||||
messages.getFactCheck#b9cdc5ee peer:InputPeer msg_id:Vector<int> = Vector<FactCheck>;
|
||||
messages.requestMainWebView#c9e01e7b flags:# compact:flags.7?true peer:InputPeer bot:InputUser start_param:flags.1?string theme_params:flags.0?DataJSON platform:string = WebViewResult;
|
||||
messages.sendPaidReaction#9dd6a67b flags:# peer:InputPeer msg_id:int count:int random_id:long private:flags.0?Bool = Updates;
|
||||
messages.getPaidReactionPrivacy#472455aa = Updates;
|
||||
updates.getState#edd4882a = updates.State;
|
||||
updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference;
|
||||
updates.getChannelDifference#3173d78 flags:# force:flags.0?true channel:InputChannel filter:ChannelMessagesFilter pts:int limit:int = updates.ChannelDifference;
|
||||
|
||||
@ -176,6 +176,7 @@
|
||||
"messages.sendQuickReplyMessages",
|
||||
"messages.getFactCheck",
|
||||
"messages.requestMainWebView",
|
||||
"messages.getPaidReactionPrivacy",
|
||||
"updates.getState",
|
||||
"updates.getDifference",
|
||||
"updates.getChannelDifference",
|
||||
@ -310,6 +311,7 @@
|
||||
"messages.getSavedReactionTags",
|
||||
"messages.updateSavedReactionTag",
|
||||
"messages.getDefaultTagReactions",
|
||||
"messages.sendPaidReaction",
|
||||
"help.getPremiumPromo",
|
||||
"channels.deactivateAllUsernames",
|
||||
"channels.toggleForum",
|
||||
|
||||
@ -192,6 +192,7 @@ $color-message-story-mention-to: #74bcff;
|
||||
|
||||
--color-deleted-account: #9eaab5;
|
||||
--color-archive: #9eaab5;
|
||||
--stars-gradient: linear-gradient(90deg, #FFAA00 0%, #FFCD3A 100%);
|
||||
|
||||
--color-heart: #ff3c32;
|
||||
|
||||
|
||||
@ -33,6 +33,7 @@ body {
|
||||
Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
|
||||
--font-family-monospace: "Cascadia Mono", "Roboto Mono", "Droid Sans Mono", 'SF Mono', "Menlo", "Ubuntu Mono",
|
||||
"Consolas", monospace;
|
||||
--font-family-rounded: -ui-rounded, "Roboto Round";
|
||||
|
||||
@media (max-width: 600px) {
|
||||
height: calc(var(--vh, 1vh) * 100);
|
||||
|
||||
@ -13,6 +13,7 @@ import type {
|
||||
ApiMessage,
|
||||
ApiPhoto,
|
||||
ApiReaction,
|
||||
ApiReactionWithPaid,
|
||||
ApiStickerSet,
|
||||
ApiUser,
|
||||
ApiVideo,
|
||||
@ -274,7 +275,7 @@ export enum SettingsScreens {
|
||||
export type StickerSetOrReactionsSetOrRecent = Pick<ApiStickerSet, (
|
||||
'id' | 'accessHash' | 'title' | 'count' | 'stickers' | 'isEmoji' | 'installedDate' | 'isArchived' |
|
||||
'hasThumbnail' | 'hasStaticThumb' | 'hasAnimatedThumb' | 'hasVideoThumb' | 'thumbCustomEmojiId'
|
||||
)> & { reactions?: ApiReaction[] };
|
||||
)> & { reactions?: ApiReactionWithPaid[] };
|
||||
|
||||
export enum LeftColumnContent {
|
||||
ChatList,
|
||||
|
||||
11
src/types/language.d.ts
vendored
11
src/types/language.d.ts
vendored
@ -1513,7 +1513,7 @@ export interface LangPair {
|
||||
'RemoveEffect': undefined;
|
||||
'ReplyInPrivateMessage': undefined;
|
||||
'ProfileOpenAppAbout': {
|
||||
'terms': string;
|
||||
'terms': string | number;
|
||||
};
|
||||
'ProfileOpenAppTerms': undefined;
|
||||
'ProfileBotOpenAppInfoLink': undefined;
|
||||
@ -1539,12 +1539,21 @@ export interface LangPair {
|
||||
'GiftStarsOutgoing': {
|
||||
'user': string | number;
|
||||
};
|
||||
'SendPaidReaction': {
|
||||
'amount': string | number;
|
||||
};
|
||||
'StarsReactionTerms': {
|
||||
'link': string | number;
|
||||
};
|
||||
'StarsReactionLinkText': undefined;
|
||||
'StarsReactionLink': undefined;
|
||||
'MiniAppsMoreTabs': {
|
||||
'botName': string | number;
|
||||
};
|
||||
'PrizeCredits': {
|
||||
'count': string | number;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
export type LangKey = keyof LangPair;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
const SITE_FONTS = ['400 1em Roboto', '500 1em Roboto'];
|
||||
const SITE_FONTS = ['400 1em Roboto', '500 1em Roboto', "500 1em 'Roboto Round'"];
|
||||
|
||||
export default function preloadFonts() {
|
||||
if ('fonts' in document) {
|
||||
|
||||
@ -401,10 +401,11 @@ async function getAvatar(chat: ApiPeer) {
|
||||
|
||||
function getReactionEmoji(reaction: ApiPeerReaction) {
|
||||
let emoji;
|
||||
if ('emoticon' in reaction.reaction) {
|
||||
if (reaction.reaction.type === 'emoji') {
|
||||
emoji = reaction.reaction.emoticon;
|
||||
}
|
||||
if ('documentId' in reaction.reaction) {
|
||||
|
||||
if (reaction.reaction.type === 'custom') {
|
||||
// eslint-disable-next-line eslint-multitab-tt/no-immediate-global
|
||||
emoji = getGlobal().customEmojis.byId[reaction.reaction.documentId]?.emoji;
|
||||
}
|
||||
@ -470,7 +471,7 @@ export async function notifyAboutMessage({
|
||||
if (isReaction && !activeReaction) return;
|
||||
|
||||
// If this is a custom emoji reaction we need to make sure it is loaded
|
||||
if (isReaction && activeReaction && 'documentId' in activeReaction.reaction) {
|
||||
if (isReaction && activeReaction && activeReaction.reaction.type === 'custom') {
|
||||
await loadCustomEmoji(activeReaction.reaction.documentId);
|
||||
}
|
||||
|
||||
|
||||
@ -11,7 +11,7 @@ export function formatInteger(value: number) {
|
||||
function formatFixedNumber(number: number) {
|
||||
const fixed = String(number.toFixed(1));
|
||||
if (fixed.substr(-2) === '.0') {
|
||||
return Math.round(number);
|
||||
return Math.floor(number);
|
||||
}
|
||||
|
||||
return number.toFixed(1).replace('.', ',');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user