[Refactoring] Action Message (#5578)
This commit is contained in:
parent
5b2c325279
commit
1b66f75853
432
src/api/gramjs/apiBuilders/messageActions.ts
Normal file
432
src/api/gramjs/apiBuilders/messageActions.ts
Normal file
@ -0,0 +1,432 @@
|
||||
import { Api as GramJs } from '../../../lib/gramjs';
|
||||
|
||||
import type { ApiPhoneCallDiscardReason } from '../../types';
|
||||
import type { ApiMessageAction } from '../../types/messageActions';
|
||||
|
||||
import { buildApiBotApp } from './bots';
|
||||
import { buildApiFormattedText, buildApiPhoto } from './common';
|
||||
import { buildApiStarGift } from './gifts';
|
||||
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
|
||||
|
||||
const UNSUPPORTED_ACTION: ApiMessageAction = {
|
||||
mediaType: 'action',
|
||||
type: 'unsupported',
|
||||
};
|
||||
|
||||
export function buildApiMessageAction(action: GramJs.TypeMessageAction): ApiMessageAction {
|
||||
if (action instanceof GramJs.MessageActionChatCreate) {
|
||||
const { title, users } = action;
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'chatCreate',
|
||||
title,
|
||||
userIds: users.map((u) => buildApiPeerId(u, 'user')),
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionChatEditTitle) {
|
||||
const { title } = action;
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'chatEditTitle',
|
||||
title,
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionChatEditPhoto) {
|
||||
const { photo } = action;
|
||||
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'chatEditPhoto',
|
||||
photo: photo instanceof GramJs.Photo ? buildApiPhoto(photo) : undefined,
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionChatDeletePhoto) {
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'chatDeletePhoto',
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionChatAddUser) {
|
||||
const { users } = action;
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'chatAddUser',
|
||||
userIds: users.map((u) => buildApiPeerId(u, 'user')),
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionChatDeleteUser) {
|
||||
const { userId } = action;
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'chatDeleteUser',
|
||||
userId: buildApiPeerId(userId, 'user'),
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionChatJoinedByLink) {
|
||||
const { inviterId } = action;
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'chatJoinedByLink',
|
||||
inviterId: buildApiPeerId(inviterId, 'user'),
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionChannelCreate) {
|
||||
const { title } = action;
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'channelCreate',
|
||||
title,
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionChatMigrateTo) {
|
||||
const { channelId } = action;
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'chatMigrateTo',
|
||||
channelId: buildApiPeerId(channelId, 'channel'),
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionChannelMigrateFrom) {
|
||||
const { title, chatId } = action;
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'channelMigrateFrom',
|
||||
title,
|
||||
chatId: buildApiPeerId(chatId, 'chat'),
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionPinMessage) {
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'pinMessage',
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionHistoryClear) {
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'historyClear',
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionGameScore) {
|
||||
const { gameId, score } = action;
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'gameScore',
|
||||
gameId: gameId.toString(),
|
||||
score,
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionPaymentSent) {
|
||||
const {
|
||||
recurringInit, recurringUsed, currency, totalAmount, invoiceSlug, subscriptionUntilDate,
|
||||
} = action;
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'paymentSent',
|
||||
isRecurringInit: recurringInit,
|
||||
isRecurringUsed: recurringUsed,
|
||||
currency,
|
||||
totalAmount: totalAmount.toJSNumber(),
|
||||
invoiceSlug,
|
||||
subscriptionUntilDate,
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionPhoneCall) {
|
||||
const {
|
||||
video, callId, reason, duration,
|
||||
} = action;
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'phoneCall',
|
||||
isVideo: video,
|
||||
callId: callId.toString(),
|
||||
reason: reason && buildApiPhoneCallDiscardReason(reason),
|
||||
duration,
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionScreenshotTaken) {
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'screenshotTaken',
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionCustomAction) {
|
||||
const { message } = action;
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'customAction',
|
||||
message,
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionBotAllowed) {
|
||||
const {
|
||||
attachMenu, fromRequest, domain, app,
|
||||
} = action;
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'botAllowed',
|
||||
isAttachMenu: attachMenu,
|
||||
isFromRequest: fromRequest,
|
||||
domain,
|
||||
app: app && buildApiBotApp(app),
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionBoostApply) {
|
||||
const { boosts } = action;
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'boostApply',
|
||||
boosts,
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionContactSignUp) {
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'contactSignUp',
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionGroupCall) {
|
||||
const { call, duration } = action;
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'groupCall',
|
||||
call: {
|
||||
id: call.id.toString(),
|
||||
accessHash: call.accessHash.toString(),
|
||||
},
|
||||
duration,
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionInviteToGroupCall) {
|
||||
const { call, users } = action;
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'inviteToGroupCall',
|
||||
call: {
|
||||
id: call.id.toString(),
|
||||
accessHash: call.accessHash.toString(),
|
||||
},
|
||||
userIds: users.map((u) => buildApiPeerId(u, 'user')),
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionGroupCallScheduled) {
|
||||
const { call, scheduleDate } = action;
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'groupCallScheduled',
|
||||
call: {
|
||||
id: call.id.toString(),
|
||||
accessHash: call.accessHash.toString(),
|
||||
},
|
||||
scheduleDate,
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionChatJoinedByRequest) {
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'chatJoinedByRequest',
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionWebViewDataSent) {
|
||||
const { text } = action;
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'webViewDataSent',
|
||||
text,
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionGiftPremium) {
|
||||
const {
|
||||
currency, amount, months, cryptoCurrency, cryptoAmount, message,
|
||||
} = action;
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'giftPremium',
|
||||
currency,
|
||||
amount: amount.toJSNumber(),
|
||||
months,
|
||||
cryptoCurrency,
|
||||
cryptoAmount: cryptoAmount?.toJSNumber(),
|
||||
message: message && buildApiFormattedText(message),
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionTopicCreate) {
|
||||
const { title, iconColor, iconEmojiId } = action;
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'topicCreate',
|
||||
title,
|
||||
iconColor,
|
||||
iconEmojiId: iconEmojiId?.toString(),
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionTopicEdit) {
|
||||
const {
|
||||
title, iconEmojiId, closed, hidden,
|
||||
} = action;
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'topicEdit',
|
||||
title,
|
||||
iconEmojiId: iconEmojiId?.toString(),
|
||||
isClosed: closed,
|
||||
isHidden: hidden,
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionSuggestProfilePhoto) {
|
||||
const { photo } = action;
|
||||
|
||||
if (!(photo instanceof GramJs.Photo)) return UNSUPPORTED_ACTION;
|
||||
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'suggestProfilePhoto',
|
||||
photo: buildApiPhoto(photo),
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionGiftCode) {
|
||||
const {
|
||||
viaGiveaway, unclaimed, boostPeer, months, slug, currency, amount, cryptoCurrency, cryptoAmount, message,
|
||||
} = action;
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'giftCode',
|
||||
isViaGiveaway: viaGiveaway,
|
||||
isUnclaimed: unclaimed,
|
||||
boostPeerId: boostPeer && getApiChatIdFromMtpPeer(boostPeer),
|
||||
months,
|
||||
slug,
|
||||
currency,
|
||||
amount: amount?.toJSNumber(),
|
||||
cryptoCurrency,
|
||||
cryptoAmount: cryptoAmount?.toJSNumber(),
|
||||
message: message && buildApiFormattedText(message),
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionGiveawayLaunch) {
|
||||
const { stars } = action;
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'giveawayLaunch',
|
||||
stars: stars?.toJSNumber(),
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionGiveawayResults) {
|
||||
const { stars, winnersCount, unclaimedCount } = action;
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'giveawayResults',
|
||||
isStars: stars,
|
||||
winnersCount,
|
||||
unclaimedCount,
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionPaymentRefunded) {
|
||||
const {
|
||||
peer, currency, totalAmount,
|
||||
} = action;
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'paymentRefunded',
|
||||
peerId: getApiChatIdFromMtpPeer(peer),
|
||||
currency,
|
||||
totalAmount: totalAmount.toJSNumber(),
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionGiftStars) {
|
||||
const {
|
||||
currency, amount, stars, cryptoCurrency, cryptoAmount, transactionId,
|
||||
} = action;
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'giftStars',
|
||||
currency,
|
||||
amount: amount.toJSNumber(),
|
||||
stars: stars.toJSNumber(),
|
||||
cryptoCurrency,
|
||||
cryptoAmount: cryptoAmount?.toJSNumber(),
|
||||
transactionId,
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionPrizeStars) {
|
||||
const {
|
||||
unclaimed, stars, transactionId, boostPeer, giveawayMsgId,
|
||||
} = action;
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'prizeStars',
|
||||
isUnclaimed: unclaimed,
|
||||
stars: stars.toJSNumber(),
|
||||
transactionId,
|
||||
boostPeerId: getApiChatIdFromMtpPeer(boostPeer),
|
||||
giveawayMsgId,
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionStarGift) {
|
||||
const {
|
||||
nameHidden, saved, converted, upgraded, refunded, canUpgrade, gift, message, convertStars, upgradeMsgId,
|
||||
upgradeStars, fromId, peer, savedId,
|
||||
} = action;
|
||||
|
||||
const starGift = buildApiStarGift(gift);
|
||||
if (starGift.type !== 'starGift') return UNSUPPORTED_ACTION;
|
||||
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'starGift',
|
||||
isNameHidden: nameHidden,
|
||||
isSaved: saved,
|
||||
isConverted: converted,
|
||||
isUpgraded: upgraded,
|
||||
isRefunded: refunded,
|
||||
canUpgrade,
|
||||
gift: starGift,
|
||||
message: message && buildApiFormattedText(message),
|
||||
starsToConvert: convertStars?.toJSNumber(),
|
||||
upgradeMsgId,
|
||||
alreadyPaidUpgradeStars: upgradeStars?.toJSNumber(),
|
||||
fromId: fromId && getApiChatIdFromMtpPeer(fromId),
|
||||
peerId: peer && getApiChatIdFromMtpPeer(peer),
|
||||
savedId: savedId && buildApiPeerId(savedId, 'user'),
|
||||
};
|
||||
}
|
||||
if (action instanceof GramJs.MessageActionStarGiftUnique) {
|
||||
const {
|
||||
upgrade, transferred, saved, refunded, gift, canExportAt, transferStars, fromId, peer, savedId,
|
||||
} = action;
|
||||
|
||||
const starGift = buildApiStarGift(gift);
|
||||
if (starGift.type !== 'starGiftUnique') return UNSUPPORTED_ACTION;
|
||||
|
||||
return {
|
||||
mediaType: 'action',
|
||||
type: 'starGiftUnique',
|
||||
isUpgrade: upgrade,
|
||||
isTransferred: transferred,
|
||||
isSaved: saved,
|
||||
isRefunded: refunded,
|
||||
gift: starGift,
|
||||
canExportAt,
|
||||
transferStars: transferStars?.toJSNumber(),
|
||||
fromId: fromId && getApiChatIdFromMtpPeer(fromId),
|
||||
peerId: peer && getApiChatIdFromMtpPeer(peer),
|
||||
savedId: savedId && buildApiPeerId(savedId, 'user'),
|
||||
};
|
||||
}
|
||||
|
||||
return UNSUPPORTED_ACTION;
|
||||
}
|
||||
|
||||
export function buildApiPhoneCallDiscardReason(reason: GramJs.TypePhoneCallDiscardReason): ApiPhoneCallDiscardReason {
|
||||
if (reason instanceof GramJs.PhoneCallDiscardReasonBusy) {
|
||||
return 'busy';
|
||||
}
|
||||
if (reason instanceof GramJs.PhoneCallDiscardReasonHangup) {
|
||||
return 'hangup';
|
||||
}
|
||||
if (reason instanceof GramJs.PhoneCallDiscardReasonMissed) {
|
||||
return 'missed';
|
||||
}
|
||||
|
||||
return 'disconnect';
|
||||
}
|
||||
@ -92,11 +92,23 @@ export function buildMessageMediaContent(
|
||||
|
||||
const isExpiredVoice = isExpiredVoiceMessage(media);
|
||||
if (isExpiredVoice) {
|
||||
return { isExpiredVoice };
|
||||
return {
|
||||
action: {
|
||||
mediaType: 'action',
|
||||
type: 'expired',
|
||||
isVoice: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
const isExpiredRoundVideo = isExpiredRoundVideoMessage(media);
|
||||
if (isExpiredRoundVideo) {
|
||||
return { isExpiredRoundVideo };
|
||||
return {
|
||||
action: {
|
||||
mediaType: 'action',
|
||||
type: 'expired',
|
||||
isRoundVideo: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const voice = buildVoice(media);
|
||||
@ -339,18 +351,18 @@ function buildAudio(media: GramJs.TypeMessageMedia): ApiAudio | undefined {
|
||||
};
|
||||
}
|
||||
|
||||
function isExpiredVoiceMessage(media: GramJs.TypeMessageMedia): MediaContent['isExpiredVoice'] {
|
||||
function isExpiredVoiceMessage(media: GramJs.TypeMessageMedia): boolean {
|
||||
if (!(media instanceof GramJs.MessageMediaDocument)) {
|
||||
return false;
|
||||
}
|
||||
return !media.document && media.voice;
|
||||
return Boolean(!media.document && media.voice);
|
||||
}
|
||||
|
||||
function isExpiredRoundVideoMessage(media: GramJs.TypeMessageMedia): MediaContent['isExpiredRoundVideo'] {
|
||||
function isExpiredRoundVideoMessage(media: GramJs.TypeMessageMedia): boolean {
|
||||
if (!(media instanceof GramJs.MessageMediaDocument)) {
|
||||
return false;
|
||||
}
|
||||
return !media.document && media.round;
|
||||
return Boolean(!media.document && media.round);
|
||||
}
|
||||
|
||||
function buildVoice(media: GramJs.TypeMessageMedia): ApiVoice | undefined {
|
||||
|
||||
@ -1,21 +1,15 @@
|
||||
import { Api as GramJs } from '../../../lib/gramjs';
|
||||
|
||||
import type {
|
||||
ApiAction,
|
||||
ApiAttachment,
|
||||
ApiChat,
|
||||
ApiContact,
|
||||
ApiDraft,
|
||||
ApiFactCheck,
|
||||
ApiFormattedText,
|
||||
ApiGroupCall,
|
||||
ApiInputMessageReplyInfo,
|
||||
ApiInputReplyInfo,
|
||||
ApiInputSavedStarGift,
|
||||
ApiKeyboardButton,
|
||||
ApiMessage,
|
||||
ApiMessageActionStarGift,
|
||||
ApiMessageActionStarGiftUnique,
|
||||
ApiMessageEntity,
|
||||
ApiMessageForwardInfo,
|
||||
ApiMessageReportResult,
|
||||
@ -27,15 +21,12 @@ import type {
|
||||
ApiReplyInfo,
|
||||
ApiReplyKeyboard,
|
||||
ApiSponsoredMessage,
|
||||
ApiStarGiftRegular,
|
||||
ApiStarGiftUnique,
|
||||
ApiSticker,
|
||||
ApiStory,
|
||||
ApiStorySkipped,
|
||||
ApiThreadInfo,
|
||||
ApiVideo,
|
||||
MediaContent,
|
||||
PhoneCallAction,
|
||||
} from '../../types';
|
||||
import {
|
||||
ApiMessageEntityTypes, MAIN_THREAD_ID,
|
||||
@ -45,7 +36,6 @@ import {
|
||||
DELETED_COMMENTS_CHANNEL_ID,
|
||||
SERVICE_NOTIFICATIONS_USER_ID,
|
||||
SPONSORED_MESSAGE_CACHE_MS,
|
||||
STARS_CURRENCY_CODE,
|
||||
SUPPORTED_AUDIO_CONTENT_TYPES,
|
||||
SUPPORTED_PHOTO_CONTENT_TYPES,
|
||||
SUPPORTED_VIDEO_CONTENT_TYPES,
|
||||
@ -60,12 +50,11 @@ import {
|
||||
type MediaRepairContext,
|
||||
} from '../helpers/localDb';
|
||||
import { resolveMessageApiChatId, serializeBytes } from '../helpers/misc';
|
||||
import { buildApiCallDiscardReason } from './calls';
|
||||
import {
|
||||
buildApiFormattedText,
|
||||
buildApiPhoto,
|
||||
} from './common';
|
||||
import { buildApiStarGift } from './gifts';
|
||||
import { buildApiMessageAction } from './messageActions';
|
||||
import { buildMessageContent, buildMessageMediaContent, buildMessageTextContent } from './messageContent';
|
||||
import { buildApiPeerColor, buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
|
||||
import { buildMessageReactions } from './reactions';
|
||||
@ -182,7 +171,6 @@ export function buildApiMessageWithChatId(
|
||||
mtpMessage: UniversalMessage,
|
||||
): ApiMessage {
|
||||
const fromId = mtpMessage.fromId ? getApiChatIdFromMtpPeer(mtpMessage.fromId) : undefined;
|
||||
const peerId = mtpMessage.peerId ? getApiChatIdFromMtpPeer(mtpMessage.peerId) : undefined;
|
||||
|
||||
const isChatWithSelf = !fromId && chatId === currentUserId;
|
||||
const forwardInfo = mtpMessage.fwdFrom && buildApiMessageForwardInfo(mtpMessage.fwdFrom, isChatWithSelf);
|
||||
@ -192,8 +180,7 @@ export function buildApiMessageWithChatId(
|
||||
const isOutgoing = !isChatWithSelf ? Boolean(mtpMessage.out && !mtpMessage.post)
|
||||
: isSavedOutgoing;
|
||||
const content = buildMessageContent(mtpMessage);
|
||||
const action = mtpMessage.action
|
||||
&& buildAction(mtpMessage.action, mtpMessage.id, fromId, peerId, Boolean(mtpMessage.post), isOutgoing);
|
||||
const action = mtpMessage.action && buildApiMessageAction(mtpMessage.action);
|
||||
if (action) {
|
||||
content.action = action;
|
||||
}
|
||||
@ -369,475 +356,6 @@ export function buildApiFactCheck(factCheck: GramJs.FactCheck): ApiFactCheck {
|
||||
};
|
||||
}
|
||||
|
||||
function buildApiMessageActionStarGift(
|
||||
action: GramJs.MessageActionStarGift, messageId: number,
|
||||
): ApiMessageActionStarGift {
|
||||
const {
|
||||
nameHidden, saved, converted, gift, message, convertStars, canUpgrade, upgraded, upgradeMsgId, upgradeStars,
|
||||
peer, savedId, fromId,
|
||||
} = action;
|
||||
|
||||
const inputSavedGift: ApiInputSavedStarGift = savedId && peer ? {
|
||||
type: 'chat',
|
||||
chatId: getApiChatIdFromMtpPeer(peer),
|
||||
savedId: savedId.toString(),
|
||||
} : {
|
||||
type: 'user',
|
||||
messageId,
|
||||
};
|
||||
|
||||
return {
|
||||
type: 'starGift',
|
||||
isNameHidden: Boolean(nameHidden),
|
||||
isSaved: Boolean(saved),
|
||||
isConverted: converted,
|
||||
fromId: fromId && getApiChatIdFromMtpPeer(fromId),
|
||||
gift: buildApiStarGift(gift) as ApiStarGiftRegular,
|
||||
message: message && buildApiFormattedText(message),
|
||||
starsToConvert: convertStars?.toJSNumber(),
|
||||
canUpgrade,
|
||||
isUpgraded: upgraded,
|
||||
upgradeMsgId,
|
||||
alreadyPaidUpgradeStars: upgradeStars?.toJSNumber(),
|
||||
peerId: peer && getApiChatIdFromMtpPeer(peer),
|
||||
savedId: savedId?.toString(),
|
||||
inputSavedGift,
|
||||
};
|
||||
}
|
||||
|
||||
function buildApiMessageActionStarGiftUnique(
|
||||
action: GramJs.MessageActionStarGiftUnique, messageId: number,
|
||||
): ApiMessageActionStarGiftUnique {
|
||||
const {
|
||||
gift, canExportAt, refunded, saved, transferStars, transferred, upgrade, fromId, peer, savedId,
|
||||
} = action;
|
||||
|
||||
const inputSavedGift: ApiInputSavedStarGift = savedId && peer ? {
|
||||
type: 'chat',
|
||||
chatId: getApiChatIdFromMtpPeer(peer),
|
||||
savedId: savedId.toString(),
|
||||
} : {
|
||||
type: 'user',
|
||||
messageId,
|
||||
};
|
||||
|
||||
return {
|
||||
type: 'starGiftUnique',
|
||||
gift: buildApiStarGift(gift) as ApiStarGiftUnique,
|
||||
canExportAt,
|
||||
isRefunded: refunded,
|
||||
isSaved: saved,
|
||||
transferStars: transferStars?.toJSNumber(),
|
||||
isTransferred: transferred,
|
||||
isUpgrade: upgrade,
|
||||
fromId: fromId && getApiChatIdFromMtpPeer(fromId),
|
||||
peerId: peer && getApiChatIdFromMtpPeer(peer),
|
||||
savedId: savedId?.toString(),
|
||||
inputSavedGift,
|
||||
};
|
||||
}
|
||||
|
||||
function buildAction(
|
||||
action: GramJs.TypeMessageAction,
|
||||
messageId: number,
|
||||
senderId: string | undefined,
|
||||
targetPeerId: string | undefined,
|
||||
isChannelPost: boolean,
|
||||
isOutgoing: boolean,
|
||||
): ApiAction | undefined {
|
||||
if (action instanceof GramJs.MessageActionEmpty) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
let phoneCall: PhoneCallAction | undefined;
|
||||
let call: Partial<ApiGroupCall> | undefined;
|
||||
let amount: number | undefined;
|
||||
let stars: number | undefined;
|
||||
let starGift: ApiMessageActionStarGift | ApiMessageActionStarGiftUnique | undefined;
|
||||
let currency: string | undefined;
|
||||
let giftCryptoInfo: {
|
||||
currency: string;
|
||||
amount: number;
|
||||
} | undefined;
|
||||
let text: string;
|
||||
const translationValues: string[] = [];
|
||||
let type: ApiAction['type'] = 'other';
|
||||
let photo: ApiPhoto | undefined;
|
||||
let score: number | undefined;
|
||||
let months: number | undefined;
|
||||
let topicEmojiIconId: string | undefined;
|
||||
let isTopicAction: boolean | undefined;
|
||||
let slug: string | undefined;
|
||||
let isGiveaway: boolean | undefined;
|
||||
let isUnclaimed: boolean | undefined;
|
||||
let pluralValue: number | undefined;
|
||||
let transactionId: string | undefined;
|
||||
let message: ApiFormattedText | undefined;
|
||||
|
||||
let targetUserIds = 'users' in action
|
||||
? action.users && action.users.map((id) => buildApiPeerId(id, 'user'))
|
||||
: ('userId' in action && [buildApiPeerId(action.userId, 'user')]) || [];
|
||||
|
||||
let targetChatId;
|
||||
if (action instanceof GramJs.MessageActionChatCreate) {
|
||||
text = 'Notification.CreatedChatWithTitle';
|
||||
translationValues.push('%action_origin%', action.title);
|
||||
type = 'chatCreate';
|
||||
} else if (action instanceof GramJs.MessageActionChatEditTitle) {
|
||||
if (isChannelPost) {
|
||||
text = 'Channel.MessageTitleUpdated';
|
||||
translationValues.push(action.title);
|
||||
} else {
|
||||
text = 'Notification.ChangedGroupName';
|
||||
translationValues.push('%action_origin%', action.title);
|
||||
}
|
||||
} else if (action instanceof GramJs.MessageActionChatEditPhoto) {
|
||||
if (isChannelPost) {
|
||||
text = 'Channel.MessagePhotoUpdated';
|
||||
} else {
|
||||
text = 'Notification.ChangedGroupPhoto';
|
||||
translationValues.push('%action_origin%');
|
||||
}
|
||||
type = 'updateProfilePhoto';
|
||||
} else if (action instanceof GramJs.MessageActionChatDeletePhoto) {
|
||||
if (isChannelPost) {
|
||||
text = 'Channel.MessagePhotoRemoved';
|
||||
} else {
|
||||
text = 'Group.MessagePhotoRemoved';
|
||||
}
|
||||
} else if (action instanceof GramJs.MessageActionChatAddUser) {
|
||||
if (!senderId || targetUserIds.includes(senderId)) {
|
||||
text = 'Notification.JoinedChat';
|
||||
translationValues.push('%target_user%');
|
||||
} else {
|
||||
text = 'Notification.Invited';
|
||||
translationValues.push('%action_origin%', '%target_user%');
|
||||
}
|
||||
} else if (action instanceof GramJs.MessageActionChatDeleteUser) {
|
||||
if (!senderId || targetUserIds.includes(senderId)) {
|
||||
text = 'Notification.LeftChat';
|
||||
translationValues.push('%target_user%');
|
||||
} else {
|
||||
text = 'Notification.Kicked';
|
||||
translationValues.push('%action_origin%', '%target_user%');
|
||||
}
|
||||
} else if (action instanceof GramJs.MessageActionChatJoinedByLink) {
|
||||
text = 'Notification.JoinedGroupByLink';
|
||||
translationValues.push('%action_origin%');
|
||||
} else if (action instanceof GramJs.MessageActionChannelCreate) {
|
||||
text = 'Notification.CreatedChannel';
|
||||
} else if (action instanceof GramJs.MessageActionChatMigrateTo) {
|
||||
targetChatId = getApiChatIdFromMtpPeer(action);
|
||||
text = 'Migrated to %target_chat%';
|
||||
translationValues.push('%target_chat%');
|
||||
} else if (action instanceof GramJs.MessageActionChannelMigrateFrom) {
|
||||
targetChatId = getApiChatIdFromMtpPeer(action);
|
||||
text = 'Migrated from %target_chat%';
|
||||
translationValues.push('%target_chat%');
|
||||
} else if (action instanceof GramJs.MessageActionPinMessage) {
|
||||
text = 'Chat.Service.Group.UpdatedPinnedMessage1';
|
||||
translationValues.push('%action_origin%', '%message%');
|
||||
} else if (action instanceof GramJs.MessageActionHistoryClear) {
|
||||
text = 'HistoryCleared';
|
||||
type = 'historyClear';
|
||||
} else if (action instanceof GramJs.MessageActionPhoneCall) {
|
||||
const withDuration = Boolean(action.duration);
|
||||
text = [
|
||||
withDuration ? 'ChatList.Service' : 'Chat',
|
||||
action.video ? 'VideoCall' : 'Call',
|
||||
isOutgoing ? (withDuration ? 'outgoing' : 'Outgoing') : (withDuration ? 'incoming' : 'Incoming'),
|
||||
].join('.');
|
||||
|
||||
if (withDuration) {
|
||||
const mins = Math.max(Math.round(action.duration! / 60), 1);
|
||||
translationValues.push(`${mins} min${mins > 1 ? 's' : ''}`);
|
||||
}
|
||||
|
||||
phoneCall = {
|
||||
isOutgoing,
|
||||
isVideo: action.video,
|
||||
duration: action.duration,
|
||||
reason: buildApiCallDiscardReason(action.reason),
|
||||
};
|
||||
} else if (action instanceof GramJs.MessageActionInviteToGroupCall) {
|
||||
text = 'Notification.VoiceChatInvitation';
|
||||
call = {
|
||||
id: action.call.id.toString(),
|
||||
accessHash: action.call.accessHash.toString(),
|
||||
};
|
||||
translationValues.push('%action_origin%', '%target_user%');
|
||||
} else if (action instanceof GramJs.MessageActionContactSignUp) {
|
||||
text = 'Notification.Joined';
|
||||
translationValues.push('%action_origin%');
|
||||
type = 'contactSignUp';
|
||||
} else if (action instanceof GramJs.MessageActionPaymentSent) {
|
||||
amount = Number(action.totalAmount);
|
||||
currency = action.currency;
|
||||
text = 'PaymentSuccessfullyPaid';
|
||||
type = 'receipt';
|
||||
if (targetPeerId) {
|
||||
targetUserIds.push(targetPeerId);
|
||||
}
|
||||
translationValues.push('%payment_amount%', '%target_user%', '%product%');
|
||||
} else if (action instanceof GramJs.MessageActionGroupCall) {
|
||||
if (action.duration) {
|
||||
const mins = Math.max(Math.round(action.duration / 60), 1);
|
||||
text = 'Notification.VoiceChatEnded';
|
||||
translationValues.push(`${mins} min${mins > 1 ? 's' : ''}`);
|
||||
} else {
|
||||
text = 'Notification.VoiceChatStartedChannel';
|
||||
call = {
|
||||
id: action.call.id.toString(),
|
||||
accessHash: action.call.accessHash.toString(),
|
||||
};
|
||||
}
|
||||
} else if (action instanceof GramJs.MessageActionBotAllowed) {
|
||||
if (action.domain) {
|
||||
text = 'ActionBotAllowed';
|
||||
translationValues.push(action.domain);
|
||||
} else if (action.fromRequest) {
|
||||
text = 'lng_action_webapp_bot_allowed';
|
||||
} else {
|
||||
text = 'ActionAttachMenuBotAllowed';
|
||||
}
|
||||
} else if (action instanceof GramJs.MessageActionCustomAction) {
|
||||
text = action.message;
|
||||
} else if (action instanceof GramJs.MessageActionChatJoinedByRequest) {
|
||||
text = 'ChatService.UserJoinedGroupByRequest';
|
||||
translationValues.push('%action_origin%');
|
||||
} else if (action instanceof GramJs.MessageActionGameScore) {
|
||||
text = senderId === currentUserId ? 'ActionYouScoredInGame' : 'ActionUserScoredInGame';
|
||||
translationValues.push('%score%');
|
||||
score = action.score;
|
||||
} else if (action instanceof GramJs.MessageActionWebViewDataSent) {
|
||||
text = 'Notification.WebAppSentData';
|
||||
translationValues.push(action.text);
|
||||
} else if (action instanceof GramJs.MessageActionGiftPremium) {
|
||||
type = 'giftPremium';
|
||||
text = isOutgoing ? 'ActionGiftOutbound' : 'ActionGiftInbound';
|
||||
if (isOutgoing) {
|
||||
translationValues.push('%gift_payment_amount%');
|
||||
} else {
|
||||
translationValues.push('%action_origin%', '%gift_payment_amount%');
|
||||
}
|
||||
if (action.message) {
|
||||
message = buildApiFormattedText(action.message);
|
||||
}
|
||||
if (targetPeerId) {
|
||||
targetUserIds.push(targetPeerId);
|
||||
}
|
||||
currency = action.currency;
|
||||
if (action.cryptoCurrency) {
|
||||
giftCryptoInfo = {
|
||||
currency: action.cryptoCurrency,
|
||||
amount: action.cryptoAmount!.toJSNumber(),
|
||||
};
|
||||
}
|
||||
amount = action.amount.toJSNumber();
|
||||
months = action.months;
|
||||
} else if (action instanceof GramJs.MessageActionTopicCreate) {
|
||||
text = 'TopicWasCreatedAction';
|
||||
type = 'topicCreate';
|
||||
translationValues.push(action.title);
|
||||
} else if (action instanceof GramJs.MessageActionTopicEdit) {
|
||||
if (action.closed !== undefined) {
|
||||
text = action.closed ? 'TopicWasClosedAction' : 'TopicWasReopenedAction';
|
||||
translationValues.push('%action_origin%', '%action_topic%');
|
||||
} else if (action.hidden !== undefined) {
|
||||
text = action.hidden ? 'TopicHidden2' : 'TopicShown';
|
||||
} else if (action.title) {
|
||||
text = 'TopicRenamedTo';
|
||||
translationValues.push('%action_origin%', action.title);
|
||||
} else if (action.iconEmojiId) {
|
||||
text = 'TopicWasIconChangedToAction';
|
||||
translationValues.push('%action_origin%', '%action_topic_icon%');
|
||||
topicEmojiIconId = action.iconEmojiId.toString();
|
||||
} else {
|
||||
text = 'ChatList.UnsupportedMessage';
|
||||
}
|
||||
isTopicAction = true;
|
||||
} else if (action instanceof GramJs.MessageActionSuggestProfilePhoto) {
|
||||
const isVideo = action.photo instanceof GramJs.Photo && action.photo.videoSizes?.length;
|
||||
text = senderId === currentUserId
|
||||
? (isVideo ? 'ActionSuggestVideoFromYouDescription' : 'ActionSuggestPhotoFromYouDescription')
|
||||
: (isVideo ? 'ActionSuggestVideoToYouDescription' : 'ActionSuggestPhotoToYouDescription');
|
||||
type = 'suggestProfilePhoto';
|
||||
translationValues.push('%target_user%');
|
||||
|
||||
if (targetPeerId) targetUserIds.push(targetPeerId);
|
||||
} else if (action instanceof GramJs.MessageActionGiveawayLaunch) {
|
||||
text = 'BoostingGiveawayJustStarted';
|
||||
translationValues.push('%action_origin%');
|
||||
} else if (action instanceof GramJs.MessageActionGiftCode) {
|
||||
type = 'giftCode';
|
||||
text = isOutgoing ? 'ActionGiftOutbound' : 'BoostingReceivedGiftNoName';
|
||||
slug = action.slug;
|
||||
months = action.months;
|
||||
amount = action.amount?.toJSNumber();
|
||||
isGiveaway = Boolean(action.viaGiveaway);
|
||||
isUnclaimed = Boolean(action.unclaimed);
|
||||
if (isOutgoing) {
|
||||
translationValues.push('%gift_payment_amount%');
|
||||
}
|
||||
if (action.message) {
|
||||
message = buildApiFormattedText(action.message);
|
||||
}
|
||||
|
||||
currency = action.currency;
|
||||
if (action.cryptoCurrency) {
|
||||
giftCryptoInfo = {
|
||||
currency: action.cryptoCurrency,
|
||||
amount: action.cryptoAmount!.toJSNumber(),
|
||||
};
|
||||
}
|
||||
if (action.boostPeer) {
|
||||
targetChatId = getApiChatIdFromMtpPeer(action.boostPeer);
|
||||
}
|
||||
if (targetPeerId) {
|
||||
targetUserIds.push(targetPeerId);
|
||||
}
|
||||
} else if (action instanceof GramJs.MessageActionGiveawayResults) {
|
||||
if (!action.winnersCount) {
|
||||
text = 'lng_action_giveaway_results_none';
|
||||
} else if (action.unclaimedCount) {
|
||||
text = 'lng_action_giveaway_results_some';
|
||||
} else {
|
||||
text = 'BoostingGiveawayServiceWinnersSelected';
|
||||
translationValues.push('%amount%');
|
||||
amount = action.winnersCount;
|
||||
pluralValue = action.winnersCount;
|
||||
}
|
||||
} else if (action instanceof GramJs.MessageActionPrizeStars) {
|
||||
type = 'prizeStars';
|
||||
isUnclaimed = Boolean(action.unclaimed);
|
||||
if (action.boostPeer) {
|
||||
targetChatId = getApiChatIdFromMtpPeer(action.boostPeer);
|
||||
}
|
||||
text = 'Notification.StarsPrize';
|
||||
stars = action.stars.toJSNumber();
|
||||
transactionId = action.transactionId;
|
||||
} else if (action instanceof GramJs.MessageActionBoostApply) {
|
||||
type = 'chatBoost';
|
||||
if (action.boosts === 1) {
|
||||
text = senderId === currentUserId ? 'BoostingBoostsGroupByYouServiceMsg' : 'BoostingBoostsGroupByUserServiceMsg';
|
||||
translationValues.push('%action_origin%');
|
||||
} else {
|
||||
text = senderId === currentUserId ? 'BoostingBoostsGroupByYouServiceMsgCount'
|
||||
: 'BoostingBoostsGroupByUserServiceMsgCount';
|
||||
translationValues.push(action.boosts.toString());
|
||||
if (senderId !== currentUserId) {
|
||||
translationValues.unshift('%action_origin%');
|
||||
}
|
||||
pluralValue = action.boosts;
|
||||
}
|
||||
} else if (action instanceof GramJs.MessageActionPaymentRefunded) {
|
||||
text = 'ActionRefunded';
|
||||
amount = Number(action.totalAmount);
|
||||
currency = action.currency;
|
||||
} else if (action instanceof GramJs.MessageActionRequestedPeer) {
|
||||
text = 'ActionRequestedPeer';
|
||||
if (action.peers) {
|
||||
targetUserIds = action.peers?.map((peer) => getApiChatIdFromMtpPeer(peer));
|
||||
}
|
||||
if (targetPeerId) {
|
||||
translationValues.unshift('%action_origin%');
|
||||
}
|
||||
} else if (action instanceof GramJs.MessageActionGiftStars) {
|
||||
type = 'giftStars';
|
||||
text = isOutgoing ? 'ActionGiftOutbound' : targetPeerId ? 'ActionGiftInbound' : 'BoostingReceivedGiftNoName';
|
||||
if (isOutgoing) {
|
||||
translationValues.push('%gift_payment_amount%');
|
||||
} else {
|
||||
translationValues.push('%action_origin%', '%gift_payment_amount%');
|
||||
}
|
||||
if (targetPeerId) {
|
||||
targetUserIds.push(targetPeerId);
|
||||
targetChatId = targetPeerId;
|
||||
}
|
||||
|
||||
if (action.cryptoCurrency) {
|
||||
giftCryptoInfo = {
|
||||
currency: action.cryptoCurrency,
|
||||
amount: action.cryptoAmount!.toJSNumber(),
|
||||
};
|
||||
}
|
||||
|
||||
currency = action.currency;
|
||||
amount = action.amount.toJSNumber();
|
||||
stars = action.stars.toJSNumber();
|
||||
transactionId = action.transactionId;
|
||||
} else if (action instanceof GramJs.MessageActionStarGift && action.gift instanceof GramJs.StarGift) {
|
||||
type = 'starGift';
|
||||
starGift = buildApiMessageActionStarGift(action, messageId);
|
||||
if (isOutgoing) {
|
||||
text = 'ActionGiftOutbound';
|
||||
translationValues.push('%gift_payment_amount%');
|
||||
} else {
|
||||
text = 'ActionGiftInbound';
|
||||
translationValues.push('%action_origin%', '%gift_payment_amount%');
|
||||
}
|
||||
|
||||
if (targetPeerId) {
|
||||
targetUserIds.push(targetPeerId);
|
||||
targetChatId = targetPeerId;
|
||||
}
|
||||
|
||||
amount = action.gift.stars.toJSNumber();
|
||||
currency = STARS_CURRENCY_CODE;
|
||||
} else if (action instanceof GramJs.MessageActionStarGiftUnique && action.gift instanceof GramJs.StarGiftUnique) {
|
||||
type = 'starGiftUnique';
|
||||
if (isOutgoing) {
|
||||
text = action.upgrade ? 'Notification.StarsGift.UpgradeYou' : 'ActionUniqueGiftTransferOutbound';
|
||||
} else {
|
||||
text = action.upgrade ? 'Notification.StarsGift.Upgrade' : 'ActionUniqueGiftTransferInbound';
|
||||
translationValues.push('%action_origin_chat%');
|
||||
}
|
||||
|
||||
starGift = buildApiMessageActionStarGiftUnique(action, messageId);
|
||||
|
||||
if (action.peer) {
|
||||
targetChatId = getApiChatIdFromMtpPeer(action.peer);
|
||||
} else if (targetPeerId) {
|
||||
targetUserIds.push(targetPeerId);
|
||||
targetChatId = targetPeerId;
|
||||
}
|
||||
} else {
|
||||
text = 'ChatList.UnsupportedMessage';
|
||||
}
|
||||
|
||||
if ('photo' in action && action.photo instanceof GramJs.Photo) {
|
||||
addPhotoToLocalDb(action.photo);
|
||||
photo = buildApiPhoto(action.photo);
|
||||
}
|
||||
|
||||
return {
|
||||
mediaType: 'action',
|
||||
text,
|
||||
type,
|
||||
targetUserIds,
|
||||
targetChatId,
|
||||
photo,
|
||||
amount,
|
||||
stars,
|
||||
starGift,
|
||||
currency,
|
||||
giftCryptoInfo,
|
||||
isGiveaway,
|
||||
slug,
|
||||
translationValues,
|
||||
call,
|
||||
phoneCall,
|
||||
score,
|
||||
months,
|
||||
topicEmojiIconId,
|
||||
isTopicAction,
|
||||
isUnclaimed,
|
||||
pluralValue,
|
||||
transactionId,
|
||||
message,
|
||||
};
|
||||
}
|
||||
|
||||
function buildReplyButtons(message: UniversalMessage, shouldSkipBuyButton?: boolean): ApiReplyKeyboard | undefined {
|
||||
const { replyMarkup, media } = message;
|
||||
|
||||
|
||||
@ -633,7 +633,7 @@ async function getFullChannelInfo(
|
||||
const memberInfo = memberInfoRequest?.member;
|
||||
const joinInfo = memberInfo?.joinedDate ? {
|
||||
joinedDate: memberInfo.joinedDate,
|
||||
inviter: memberInfo.inviterId,
|
||||
inviterId: memberInfo.inviterId,
|
||||
isViaRequest: memberInfo.isViaRequest,
|
||||
} : undefined;
|
||||
|
||||
|
||||
@ -289,7 +289,8 @@ export async function fetchProfilePhotos({
|
||||
|
||||
return {
|
||||
count: totalCount,
|
||||
photos: messages.map((message) => message.content.action!.photo).filter(Boolean),
|
||||
photos: messages.map((message) => message.content.action?.type === 'chatEditPhoto' && message.content.action.photo)
|
||||
.filter(Boolean),
|
||||
nextOffsetId,
|
||||
};
|
||||
}
|
||||
|
||||
@ -31,13 +31,6 @@ export interface ApiGroupCall {
|
||||
isSpeakerDisabled?: boolean;
|
||||
}
|
||||
|
||||
export interface PhoneCallAction {
|
||||
isOutgoing: boolean;
|
||||
isVideo?: boolean;
|
||||
duration?: number;
|
||||
reason?: 'missed' | 'disconnect' | 'hangup' | 'busy';
|
||||
}
|
||||
|
||||
export interface ApiPhoneCall {
|
||||
state?: 'active' | 'waiting' | 'discarded' | 'requested' | 'accepted' | 'requesting';
|
||||
isConnected?: boolean;
|
||||
@ -73,3 +66,5 @@ export interface ApiPhoneCall {
|
||||
screencastState?: VideoState;
|
||||
isBatteryLow?: boolean;
|
||||
}
|
||||
|
||||
export type ApiPhoneCallDiscardReason = 'missed' | 'disconnect' | 'hangup' | 'busy';
|
||||
|
||||
@ -122,7 +122,7 @@ export interface ApiChatFullInfo {
|
||||
};
|
||||
joinInfo?: {
|
||||
joinedDate: number;
|
||||
inviter?: string;
|
||||
inviterId?: string;
|
||||
isViaRequest?: boolean;
|
||||
};
|
||||
linkedChatId?: string;
|
||||
|
||||
287
src/api/types/messageActions.ts
Normal file
287
src/api/types/messageActions.ts
Normal file
@ -0,0 +1,287 @@
|
||||
import type { ApiGroupCall, ApiPhoneCallDiscardReason } from './calls';
|
||||
import type { ApiBotApp, ApiFormattedText, ApiPhoto } from './messages';
|
||||
import type { ApiStarGiftRegular, ApiStarGiftUnique } from './payments';
|
||||
|
||||
interface ActionMediaType {
|
||||
mediaType: 'action';
|
||||
}
|
||||
|
||||
export interface ApiMessageActionChatCreate extends ActionMediaType {
|
||||
type: 'chatCreate';
|
||||
title: string;
|
||||
userIds: string[];
|
||||
}
|
||||
|
||||
export interface ApiMessageActionChatEditTitle extends ActionMediaType {
|
||||
type: 'chatEditTitle';
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface ApiMessageActionChatEditPhoto extends ActionMediaType {
|
||||
type: 'chatEditPhoto';
|
||||
photo?: ApiPhoto;
|
||||
}
|
||||
|
||||
export interface ApiMessageActionChatDeletePhoto extends ActionMediaType {
|
||||
type: 'chatDeletePhoto';
|
||||
}
|
||||
|
||||
export interface ApiMessageActionChatAddUser extends ActionMediaType {
|
||||
type: 'chatAddUser';
|
||||
userIds: string[];
|
||||
}
|
||||
|
||||
export interface ApiMessageActionChatDeleteUser extends ActionMediaType {
|
||||
type: 'chatDeleteUser';
|
||||
userId: string;
|
||||
}
|
||||
|
||||
export interface ApiMessageActionChatJoinedByLink extends ActionMediaType {
|
||||
type: 'chatJoinedByLink';
|
||||
inviterId: string;
|
||||
}
|
||||
|
||||
export interface ApiMessageActionChannelCreate extends ActionMediaType {
|
||||
type: 'channelCreate';
|
||||
title: string;
|
||||
}
|
||||
|
||||
export interface ApiMessageActionChatMigrateTo extends ActionMediaType {
|
||||
type: 'chatMigrateTo';
|
||||
channelId: string;
|
||||
}
|
||||
|
||||
export interface ApiMessageActionChannelMigrateFrom extends ActionMediaType {
|
||||
type: 'channelMigrateFrom';
|
||||
title: string;
|
||||
chatId: string;
|
||||
}
|
||||
|
||||
export interface ApiMessageActionPinMessage extends ActionMediaType {
|
||||
type: 'pinMessage';
|
||||
}
|
||||
|
||||
export interface ApiMessageActionHistoryClear extends ActionMediaType {
|
||||
type: 'historyClear';
|
||||
}
|
||||
|
||||
export interface ApiMessageActionGameScore extends ActionMediaType {
|
||||
type: 'gameScore';
|
||||
gameId: string;
|
||||
score: number;
|
||||
}
|
||||
|
||||
export interface ApiMessageActionPaymentSent extends ActionMediaType {
|
||||
type: 'paymentSent';
|
||||
isRecurringInit?: true;
|
||||
isRecurringUsed?: true;
|
||||
currency: string;
|
||||
totalAmount: number;
|
||||
invoiceSlug?: string;
|
||||
subscriptionUntilDate?: number;
|
||||
}
|
||||
|
||||
export interface ApiMessageActionPhoneCall extends ActionMediaType {
|
||||
type: 'phoneCall';
|
||||
isVideo?: true;
|
||||
callId: string;
|
||||
reason?: ApiPhoneCallDiscardReason;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
export interface ApiMessageActionScreenshotTaken extends ActionMediaType {
|
||||
type: 'screenshotTaken';
|
||||
}
|
||||
|
||||
export interface ApiMessageActionCustomAction extends ActionMediaType {
|
||||
type: 'customAction';
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface ApiMessageActionBotAllowed extends ActionMediaType {
|
||||
type: 'botAllowed';
|
||||
isAttachMenu?: true;
|
||||
isFromRequest?: true;
|
||||
domain?: string;
|
||||
app?: ApiBotApp;
|
||||
}
|
||||
|
||||
export interface ApiMessageActionContactSignUp extends ActionMediaType {
|
||||
type: 'contactSignUp';
|
||||
}
|
||||
|
||||
export interface ApiMessageActionGroupCall extends ActionMediaType {
|
||||
type: 'groupCall';
|
||||
call: Pick<ApiGroupCall, 'id' | 'accessHash'>;
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
export interface ApiMessageActionInviteToGroupCall extends ActionMediaType {
|
||||
type: 'inviteToGroupCall';
|
||||
call: Pick<ApiGroupCall, 'id' | 'accessHash'>;
|
||||
userIds: string[];
|
||||
}
|
||||
|
||||
export interface ApiMessageActionGroupCallScheduled extends ActionMediaType {
|
||||
type: 'groupCallScheduled';
|
||||
call: Pick<ApiGroupCall, 'id' | 'accessHash'>;
|
||||
scheduleDate: number;
|
||||
}
|
||||
|
||||
export interface ApiMessageActionChatJoinedByRequest extends ActionMediaType {
|
||||
type: 'chatJoinedByRequest';
|
||||
}
|
||||
|
||||
export interface ApiMessageActionWebViewDataSent extends ActionMediaType {
|
||||
type: 'webViewDataSent';
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface ApiMessageActionGiftPremium extends ActionMediaType {
|
||||
type: 'giftPremium';
|
||||
currency: string;
|
||||
amount: number;
|
||||
months: number;
|
||||
cryptoCurrency?: string;
|
||||
cryptoAmount?: number;
|
||||
message?: ApiFormattedText;
|
||||
}
|
||||
|
||||
export interface ApiMessageActionTopicCreate extends ActionMediaType {
|
||||
type: 'topicCreate';
|
||||
title: string;
|
||||
iconColor: number;
|
||||
iconEmojiId?: string;
|
||||
}
|
||||
|
||||
export interface ApiMessageActionTopicEdit extends ActionMediaType {
|
||||
type: 'topicEdit';
|
||||
title?: string;
|
||||
iconEmojiId?: string;
|
||||
isClosed?: boolean;
|
||||
isHidden?: boolean;
|
||||
}
|
||||
|
||||
export interface ApiMessageActionSuggestProfilePhoto extends ActionMediaType {
|
||||
type: 'suggestProfilePhoto';
|
||||
photo: ApiPhoto;
|
||||
}
|
||||
|
||||
export interface ApiMessageActionGiftCode extends ActionMediaType {
|
||||
type: 'giftCode';
|
||||
isViaGiveaway?: true;
|
||||
isUnclaimed?: true;
|
||||
boostPeerId?: string;
|
||||
months: number;
|
||||
slug: string;
|
||||
currency?: string;
|
||||
amount?: number;
|
||||
cryptoCurrency?: string;
|
||||
cryptoAmount?: number;
|
||||
message?: ApiFormattedText;
|
||||
}
|
||||
|
||||
export interface ApiMessageActionGiveawayLaunch extends ActionMediaType {
|
||||
type: 'giveawayLaunch';
|
||||
stars?: number;
|
||||
}
|
||||
|
||||
export interface ApiMessageActionGiveawayResults extends ActionMediaType {
|
||||
type: 'giveawayResults';
|
||||
isStars?: true;
|
||||
winnersCount: number;
|
||||
unclaimedCount: number;
|
||||
}
|
||||
|
||||
export interface ApiMessageActionBoostApply extends ActionMediaType {
|
||||
type: 'boostApply';
|
||||
boosts: number;
|
||||
}
|
||||
|
||||
export interface ApiMessageActionPaymentRefunded extends ActionMediaType {
|
||||
type: 'paymentRefunded';
|
||||
peerId: string;
|
||||
currency: string;
|
||||
totalAmount: number;
|
||||
}
|
||||
|
||||
export interface ApiMessageActionGiftStars extends ActionMediaType {
|
||||
type: 'giftStars';
|
||||
currency: string;
|
||||
amount: number;
|
||||
stars: number;
|
||||
cryptoCurrency?: string;
|
||||
cryptoAmount?: number;
|
||||
transactionId?: string;
|
||||
}
|
||||
|
||||
export interface ApiMessageActionPrizeStars extends ActionMediaType {
|
||||
type: 'prizeStars';
|
||||
isUnclaimed?: true;
|
||||
stars: number;
|
||||
transactionId: string;
|
||||
boostPeerId: string;
|
||||
giveawayMsgId: number;
|
||||
}
|
||||
|
||||
export interface ApiMessageActionStarGift extends ActionMediaType {
|
||||
type: 'starGift';
|
||||
isNameHidden?: true;
|
||||
isSaved?: true;
|
||||
isConverted?: true;
|
||||
isUpgraded?: true;
|
||||
isRefunded?: true;
|
||||
canUpgrade?: true;
|
||||
gift: ApiStarGiftRegular;
|
||||
message?: ApiFormattedText;
|
||||
starsToConvert?: number;
|
||||
upgradeMsgId?: number;
|
||||
alreadyPaidUpgradeStars?: number;
|
||||
fromId?: string;
|
||||
peerId?: string;
|
||||
savedId?: string;
|
||||
}
|
||||
|
||||
export interface ApiMessageActionStarGiftUnique extends ActionMediaType {
|
||||
type: 'starGiftUnique';
|
||||
isUpgrade?: true;
|
||||
isTransferred?: true;
|
||||
isSaved?: true;
|
||||
isRefunded?: true;
|
||||
gift: ApiStarGiftUnique;
|
||||
canExportAt?: number;
|
||||
transferStars?: number;
|
||||
fromId?: string;
|
||||
peerId?: string;
|
||||
savedId?: string;
|
||||
}
|
||||
|
||||
export interface ApiMessageActionChannelJoined extends ActionMediaType {
|
||||
type: 'channelJoined';
|
||||
isViaRequest?: true;
|
||||
inviterId?: string;
|
||||
}
|
||||
|
||||
export interface ApiMessageActionExpiredContent extends ActionMediaType {
|
||||
type: 'expired';
|
||||
isVoice?: true;
|
||||
isRoundVideo?: true;
|
||||
}
|
||||
|
||||
export interface ApiMessageActionUnsupported extends ActionMediaType {
|
||||
type: 'unsupported';
|
||||
}
|
||||
|
||||
export type ApiMessageAction = ApiMessageActionUnsupported | ApiMessageActionChatCreate | ApiMessageActionChatEditTitle
|
||||
| ApiMessageActionChatEditPhoto | ApiMessageActionChatDeletePhoto | ApiMessageActionChatAddUser
|
||||
| ApiMessageActionChatDeleteUser | ApiMessageActionChatJoinedByLink | ApiMessageActionChannelCreate
|
||||
| ApiMessageActionChatMigrateTo | ApiMessageActionChannelMigrateFrom | ApiMessageActionPinMessage
|
||||
| ApiMessageActionHistoryClear | ApiMessageActionGameScore | ApiMessageActionPaymentSent | ApiMessageActionPhoneCall
|
||||
| ApiMessageActionScreenshotTaken | ApiMessageActionCustomAction | ApiMessageActionBotAllowed
|
||||
| ApiMessageActionBoostApply | ApiMessageActionContactSignUp | ApiMessageActionExpiredContent
|
||||
| ApiMessageActionGroupCall | ApiMessageActionInviteToGroupCall | ApiMessageActionGroupCallScheduled
|
||||
| ApiMessageActionChatJoinedByRequest | ApiMessageActionWebViewDataSent | ApiMessageActionGiftPremium
|
||||
| ApiMessageActionTopicCreate | ApiMessageActionTopicEdit | ApiMessageActionSuggestProfilePhoto
|
||||
| ApiMessageActionChannelJoined | ApiMessageActionGiftCode | ApiMessageActionGiveawayLaunch
|
||||
| ApiMessageActionGiveawayResults | ApiMessageActionPaymentRefunded | ApiMessageActionGiftStars
|
||||
| ApiMessageActionPrizeStars | ApiMessageActionStarGift | ApiMessageActionStarGiftUnique;
|
||||
@ -1,11 +1,9 @@
|
||||
import type { ThreadId, WebPageMediaSize } from '../../types';
|
||||
import type { ApiWebDocument } from './bots';
|
||||
import type { ApiGroupCall, PhoneCallAction } from './calls';
|
||||
import type { ApiPeerColor } from './chats';
|
||||
import type { ApiMessageAction } from './messageActions';
|
||||
import type {
|
||||
ApiInputSavedStarGift,
|
||||
ApiLabeledPrice,
|
||||
ApiStarGiftRegular,
|
||||
ApiStarGiftUnique,
|
||||
} from './payments';
|
||||
import type {
|
||||
@ -328,85 +326,6 @@ export type ApiNewPoll = {
|
||||
};
|
||||
};
|
||||
|
||||
export interface ApiMessageActionStarGift {
|
||||
type: 'starGift';
|
||||
isNameHidden: boolean;
|
||||
isSaved: boolean;
|
||||
isConverted?: true;
|
||||
gift: ApiStarGiftRegular;
|
||||
message?: ApiFormattedText;
|
||||
starsToConvert?: number;
|
||||
canUpgrade?: true;
|
||||
isUpgraded?: true;
|
||||
upgradeMsgId?: number;
|
||||
alreadyPaidUpgradeStars?: number;
|
||||
fromId?: string;
|
||||
peerId?: string;
|
||||
savedId?: string;
|
||||
inputSavedGift?: ApiInputSavedStarGift;
|
||||
}
|
||||
|
||||
export interface ApiMessageActionStarGiftUnique {
|
||||
type: 'starGiftUnique';
|
||||
isUpgrade?: true;
|
||||
isTransferred?: true;
|
||||
isSaved?: true;
|
||||
isRefunded?: true;
|
||||
gift: ApiStarGiftUnique;
|
||||
canExportAt?: number;
|
||||
transferStars?: number;
|
||||
fromId?: string;
|
||||
peerId?: string;
|
||||
savedId?: string;
|
||||
inputSavedGift?: ApiInputSavedStarGift;
|
||||
}
|
||||
|
||||
export interface ApiAction {
|
||||
mediaType: 'action';
|
||||
text: string;
|
||||
targetUserIds?: string[];
|
||||
targetChatId?: string;
|
||||
type:
|
||||
| 'historyClear'
|
||||
| 'contactSignUp'
|
||||
| 'chatCreate'
|
||||
| 'topicCreate'
|
||||
| 'suggestProfilePhoto'
|
||||
| 'updateProfilePhoto'
|
||||
| 'joinedChannel'
|
||||
| 'chatBoost'
|
||||
| 'receipt'
|
||||
| 'giftStars'
|
||||
| 'giftPremium'
|
||||
| 'giftCode'
|
||||
| 'prizeStars'
|
||||
| 'starGift'
|
||||
| 'starGiftUnique'
|
||||
| 'other';
|
||||
photo?: ApiPhoto;
|
||||
amount?: number;
|
||||
stars?: number;
|
||||
transactionId?: string;
|
||||
currency?: string;
|
||||
giftCryptoInfo?: {
|
||||
currency: string;
|
||||
amount: number;
|
||||
};
|
||||
starGift?: ApiMessageActionStarGift | ApiMessageActionStarGiftUnique;
|
||||
translationValues: string[];
|
||||
call?: Partial<ApiGroupCall>;
|
||||
phoneCall?: PhoneCallAction;
|
||||
score?: number;
|
||||
months?: number;
|
||||
topicEmojiIconId?: string;
|
||||
isTopicAction?: boolean;
|
||||
slug?: string;
|
||||
isGiveaway?: boolean;
|
||||
isUnclaimed?: boolean;
|
||||
pluralValue?: number;
|
||||
message?: ApiFormattedText;
|
||||
}
|
||||
|
||||
export interface ApiWebPage {
|
||||
mediaType: 'webpage';
|
||||
id: number;
|
||||
@ -571,7 +490,7 @@ export type MediaContent = {
|
||||
sticker?: ApiSticker;
|
||||
contact?: ApiContact;
|
||||
pollId?: string;
|
||||
action?: ApiAction;
|
||||
action?: ApiMessageAction;
|
||||
webPage?: ApiWebPage;
|
||||
audio?: ApiAudio;
|
||||
voice?: ApiVoice;
|
||||
@ -582,8 +501,6 @@ export type MediaContent = {
|
||||
giveaway?: ApiGiveaway;
|
||||
giveawayResults?: ApiGiveawayResults;
|
||||
paidMedia?: ApiPaidMedia;
|
||||
isExpiredVoice?: boolean;
|
||||
isExpiredRoundVideo?: boolean;
|
||||
ttlSeconds?: number;
|
||||
};
|
||||
export type MediaContainer = {
|
||||
|
||||
@ -14,8 +14,6 @@
|
||||
"DeleteChatUser" = "Delete chat";
|
||||
"AccDescrGroup" = "Group";
|
||||
"AccDescrChannel" = "Channel";
|
||||
"ChatServiceGroupUpdatedPinnedMessage1" = "{user} pinned \"{message}\"";
|
||||
"MessagePinnedGenericMessage" = "{user} pinned a message";
|
||||
"Nothing" = "Nothing";
|
||||
"UserTyping" = "{user} is typing";
|
||||
"SendActionRecordVideo" = "recording a video";
|
||||
@ -154,6 +152,7 @@
|
||||
"SentAppCode" = "We've sent the code to the **Telegram** app on your other device.";
|
||||
"LoginJustSentSms" = "We've sent you a code via SMS. Please enter it above.";
|
||||
"Code" = "Code";
|
||||
"Open" = "Open";
|
||||
"LoginHeaderPassword" = "Enter Password";
|
||||
"LoginEnterPasswordDescription" = "You have Two-Step Verification enabled, so your account is protected with an additional password.";
|
||||
"StartText" = "Please confirm your country code\nand enter your phone number.";
|
||||
@ -613,6 +612,8 @@
|
||||
"PreviewDraggingAddItems_one" = "Add Item";
|
||||
"PreviewDraggingAddItems_other" = "Add Items";
|
||||
"Caption" = "Caption";
|
||||
"CropperTitle" = "Drag to reposition";
|
||||
"CropperApply" = "Crop Image";
|
||||
"AttachmentMenuPhotoOrVideo" = "Photo or Video";
|
||||
"AttachDocument" = "File";
|
||||
"Poll" = "Poll";
|
||||
@ -676,7 +677,7 @@
|
||||
"ErrorPasswordFlood" = "Too many attempts, please try again later";
|
||||
"ErrorPhoneBanned" = "This phone number is banned";
|
||||
"ErrorFloodTime" = "Too many attempts, please try again in {time}";
|
||||
"ErrorPasswordFresh" = "The password was modified less than 24 hours ago, try again in {time}";
|
||||
"ErrorPasswordFresh" = "The password was modified recently, try again in {time}";
|
||||
"ErrorUnexpected" = "Unexpected error";
|
||||
"ErrorUnexpectedMessage" = "Unexpected error: {error}";
|
||||
"ErrorEmailUnconfirmed" = "Email not confirmed";
|
||||
@ -1223,15 +1224,6 @@
|
||||
"EditProfileNoFirstName" = "Please enter your first name";
|
||||
"AriaEditProfilePhoto" = "Edit avatar";
|
||||
"LaunchConfetti" = "Launch confetti!";
|
||||
"SettingsAnimations" = "Animation Level";
|
||||
"SettingsAnimationsDescription" = "Choose the desired animations amount";
|
||||
"SettingsAnimationsLow" = "Solid and Steady";
|
||||
"SettingsAnimationsMedium" = "Nice and Fast";
|
||||
"SettingsAnimationsHigh" = "Lots of Stuff";
|
||||
"Settings12HourFormat" = "12-hour";
|
||||
"Settings24HourFormat" = "24-hour";
|
||||
"SettingsSendEnterDescription" = "New line by Shift + Enter";
|
||||
"SettingsSendCtrlEnterDescription" = "New line by Enter";
|
||||
"AriaMoreButton" = "More Actions";
|
||||
"RecoveryEmailCode" = "Recovery Email Code";
|
||||
"NotificationsWeb" = "Web Notifications";
|
||||
@ -1340,7 +1332,6 @@
|
||||
"HideCaption" = "Hide Caption";
|
||||
"ChangeRecipient" = "Change Recipient";
|
||||
"DragToSortAria" = "Drag to sort";
|
||||
"SettingsTimeFormat" = "Time format";
|
||||
"MenuReportBug" = "Report a Bug";
|
||||
"MenuBetaChangelog" = "Beta Changelog";
|
||||
"MenuSwitchToK" = "Switch to K Version";
|
||||
@ -1378,6 +1369,7 @@
|
||||
"GiftMessagePlaceholder" = "Enter Message (Optional)";
|
||||
"GiftHideMyName" = "Hide My Name";
|
||||
"GiftHideNameDescription" = "You can hide your name and message from visitors to {receiver}'s profile. {receiver} will still see your name and message.";
|
||||
"GiftHideNameDescriptionSelf" = "Hide my name and message from visitors to my profile.";
|
||||
"GiftHideNameDescriptionChannel" = "You can hide your name and message from all visitors of this channel except its admins.";
|
||||
"GiftSend" = "Send a Gift for {amount}";
|
||||
"GiftUnique" = "{title} #{number}";
|
||||
@ -1422,7 +1414,6 @@
|
||||
"GiftInfoSoldOutDescription" = "This gift has been sold out";
|
||||
"GiftInfoSenderHidden" = "Only you can see the sender's name and message.";
|
||||
"GiftInfoOwner" = "Owner";
|
||||
"GiftInfoAvailability" = "Availability";
|
||||
"GiftInfoIssued" = "{issued}/{total} issued";
|
||||
"GiftInfoCollectible" = "Collectible #{number}";
|
||||
"GiftAttributeModel" = "Model";
|
||||
@ -1488,21 +1479,6 @@
|
||||
"MiniAppsMoreTabs_other" = "{botName} & {count} Others";
|
||||
"PrizeCredits2_one" = "Your prize is {count} Star.";
|
||||
"PrizeCredits2_other" = "Your prize is {count} Stars.";
|
||||
"ActionStarGiftPeerTitle" = "{peer} sent you a Gift for {count} Stars";
|
||||
"ActionStarGiftOutTitle" = "You have sent a gift for {count} Stars";
|
||||
"ActionStarGiftPeerOutDescription_one" = "{peer} can display this gift on their profile or convert it to {count} Star.";
|
||||
"ActionStarGiftPeerOutDescription_other" = "{peer} can display this gift on their profile or convert it to {count} Stars.";
|
||||
"ActionStarGiftDescription2_one" = "Add this gift to your profile or convert it to {count} Star.";
|
||||
"ActionStarGiftDescription2_other" = "Add this gift to your profile or convert it to {count} Stars.";
|
||||
"ActionStarGiftDisplaying" = "You kept this gift on your profile.";
|
||||
"ActionStarGiftChannelDisplaying" = "This gift is displayed to visitors of your channel.";
|
||||
"ActionStarGiftPeerOutDescriptionUpgrade" = "{peer} can turn this gift to a unique collectible.";
|
||||
"ActionStarGiftDescriptionUpgrade" = "Tap “Unpack” to turn this gift to a unique collectible.";
|
||||
"ActionStarGiftUpgraded" = "This gift was upgraded.";
|
||||
"ActionStarGiftUnpack" = "Unpack";
|
||||
"GiftTo" = "Gift to";
|
||||
"GiftFrom" = "Gift from";
|
||||
"ReceivedGift" = "Received Gift";
|
||||
"SentGift" = "Sent Gift";
|
||||
"StarsSubscribeText_one" = "Do you want to subscribe to **{chat}** for **{amount} Star** per month?";
|
||||
"StarsSubscribeText_other" = "Do you want to subscribe to **{chat}** for **{amount} Stars** per month?";
|
||||
@ -1590,7 +1566,7 @@
|
||||
"ProfileTabSimilarChannels" = "Similar Channels";
|
||||
"ProfileTabSimilarBots" = "Similar Bots";
|
||||
"ActionUnsupportedTitle" = "Action not supported yet";
|
||||
"ActionUnsupportedDescription" = "Please, use one of our apps to complete this action.";
|
||||
"ActionUnsupportedDescription" = "Please use one of our apps to complete this action.";
|
||||
"LocationPermissionText" = "**{name}** requests access to set your **location**. You will be able to revoke this access in the profile page of **{name}**.";
|
||||
"UnlockMoreSimilarBots" = "Show More Apps";
|
||||
"MoreSimilarBotsText" = "Subscribe to **Telegram Premium** to unlock up to {count} similar apps.";
|
||||
@ -1623,6 +1599,197 @@
|
||||
"CheckPasswordTitle" = "Enter Password";
|
||||
"CheckPasswordPlaceholder" = "Password";
|
||||
"CheckPasswordDescription" = "Please enter your password to continue.";
|
||||
"ActionFallbackUser" = "User";
|
||||
"ActionFallbackChat" = "Chat";
|
||||
"ActionFallbackChannel" = "Channel";
|
||||
"ActionFallbackSomeone" = "Someone";
|
||||
"ActionUnsupported" = "Unsupported message";
|
||||
"ActionPinnedText" = "{from} pinned \"{text}\"";
|
||||
"ActionPinnedTextYou" = "You pinned \"{text}\"";
|
||||
"ActionPinnedNotFound" = "{from} pinned a message";
|
||||
"ActionPinnedNotFoundYou" = "You pinned a message";
|
||||
"ActionPinnedMedia" = "{from} pinned {media}";
|
||||
"ActionPinnedMediaYou" = "You pinned {media}";
|
||||
"ActionPinnedMediaPhoto" = "a photo";
|
||||
"ActionPinnedMediaVideo" = "a video";
|
||||
"ActionPinnedMediaAudio" = "an audio file";
|
||||
"ActionPinnedMediaVoice" = "a voice message";
|
||||
"ActionPinnedMediaVideoMessage" = "a video message";
|
||||
"ActionPinnedMediaFile" = "a file";
|
||||
"ActionPinnedMediaGif" = "a GIF";
|
||||
"ActionPinnedMediaContact" = "a contact information";
|
||||
"ActionPinnedMediaLocation" = "a location mark";
|
||||
"ActionPinnedMediaSticker" = "a sticker";
|
||||
"ActionPinnedMediaInvoice" = "an invoice";
|
||||
"ActionPinnedMediaGame" = "the game «{game}»";
|
||||
"ActionPinnedMediaStory" = "a story";
|
||||
"ActionPinnedMediaAlbum" = "an album";
|
||||
"ActionPinnedMediaPoll" = "a poll";
|
||||
"ActionPinnedMediaGiveaway" = "a giveaway";
|
||||
"ActionPinnedMediaGiveawayResults" = "giveaway results";
|
||||
"ActionGroupCallStartedGroup" = "{from} started a video chat";
|
||||
"ActionGroupCallStartedChannel" = "Live stream started";
|
||||
"ActionGroupCallScheduledGroup" = "{from} scheduled a video chat for {date}";
|
||||
"ActionGroupCallScheduledChannel" = "Live stream scheduled for {date}";
|
||||
"ActionGroupCallFinishedChannel" = "Live stream finished ({duration})";
|
||||
"ActionGroupCallFinishedGroup" = "{from} ended the video chat ({duration})";
|
||||
"ActionExpiredVoice" = "Expired voice message";
|
||||
"ActionExpiredVideo" = "Expired video message";
|
||||
"ActionAddUser" = "{from} added {user}";
|
||||
"ActionAddUserYou" = "You added {user}";
|
||||
"ActionAddUsersMany" = "{from} added {users}";
|
||||
"ActionAddUsersManyYou" = "You added {users}";
|
||||
"ActionAddYou" = "{from} added you to this channel";
|
||||
"ActionChannelJoinedYou" = "You joined this channel";
|
||||
"ActionChannelJoinedByRequestChannelYou" = "Your request to join the channel was approved";
|
||||
"ActionAddYouGroup" = "{from} added you to this group";
|
||||
"ActionKickUser" = "{from} removed {user}";
|
||||
"ActionKickUserYou" = "You removed {user}";
|
||||
"ActionUserLeft" = "{from} left the group";
|
||||
"ActionUserLeftYou" = "You left the group";
|
||||
"ActionUserJoined" = "{from} joined the group";
|
||||
"ActionUserJoinedYou" = "You joined the group";
|
||||
"ActionUserJoinedByLink" = "{from} joined the group via invite link";
|
||||
"ActionJoinedByRequest" = "{from} was accepted to the group";
|
||||
"ActionJoinedByRequestYou" = "Your request to join the group was approved";
|
||||
"ActionVideoInvited" = "{from} invited {user} to the video chat";
|
||||
"ActionVideoInvitedYou" = "You invited {user} to the video chat";
|
||||
"ActionVideoInvitedMany" = "{from} invited {users} to the video chat";
|
||||
"ActionVideoInvitedManyYou" = "You invited {users} to the video chat";
|
||||
"ActionUserRegistered" = "{from} joined Telegram";
|
||||
"ActionRemovedPhoto" = "{from} removed group photo";
|
||||
"ActionRemovedPhotoYou" = "You removed group photo";
|
||||
"ActionRemovedPhotoChannel" = "Channel photo removed";
|
||||
"ActionChangedPhoto" = "{from} updated group photo";
|
||||
"ActionChangedPhotoYou" = "You updated group photo";
|
||||
"ActionChangedPhotoChannel" = "Channel photo updated";
|
||||
"ActionChangedTitle" = "{from} changed group name to «{title}»";
|
||||
"ActionChangedTitleYou" = "You changed group name to «{title}»";
|
||||
"ActionChangedTitleChannel" = "Channel name was changed to «{title}»";
|
||||
"ActionCreatedChat" = "{from} created the group «{title}»";
|
||||
"ActionCreatedChannel" = "Channel created";
|
||||
"ActionGameScore_one" = "{from} scored {count} in {game}";
|
||||
"ActionGameScore_other" = "{from} scored {count} in {game}";
|
||||
"ActionGameScoreYou_one" = "You scored {count} in {game}";
|
||||
"ActionGameScoreYou_other" = "You scored {count} in {game}";
|
||||
"ActionGameScoreNoGame_one" = "{from} scored {count}";
|
||||
"ActionGameScoreNoGame_other" = "{from} scored {count}";
|
||||
"ActionGameScoreNoGameYou_one" = "You scored {count}";
|
||||
"ActionGameScoreNoGameYou_other" = "You scored {count}";
|
||||
"ActionPaymentDone" = "You successfully transferred {amount} to {user}";
|
||||
"ActionPaymentDoneFor" = "You successfully transferred {amount} to {user} for {invoice}";
|
||||
"ActionPaymentInitRecurringFor" = "You successfully transferred {amount} to {user} for {invoice} and allowed future recurring payments";
|
||||
"ActionPaymentInitRecurring" = "You successfully transferred {amount} to {user} and allowed future recurring payments";
|
||||
"ActionPaymentUsedRecurring" = "You were charged {amount} via recurring payment";
|
||||
"ActionScreenshotTaken" = "{from} took a screenshot!";
|
||||
"ActionScreenshotTakenYou" = "You took a screenshot!";
|
||||
"ActionBotAllowedFromDomain" = "You allowed this bot to message you when you logged in on {domain}.";
|
||||
"ActionBotAllowedFromApp" = "You allowed this bot to message you when you opened {app}.";
|
||||
"ActionBotAppPlaceholder" = "App";
|
||||
"ActionGiftTextUnknown" = "You've received a gift";
|
||||
"ActionGiftTextCost" = "{from} sent you a gift for {cost}";
|
||||
"ActionGiftTextCostYou" = "You've sent a gift for {cost}";
|
||||
"ActionGiftTextCostAnonymous" = "Someone sent you a gift for {cost}";
|
||||
"ActionCostCrypto" = "{price} ({cryptoPrice})";
|
||||
"ActionWebviewDataDone" = "Data from the \"{text}\" button was transferred to the bot.";
|
||||
"ActionGiftUniqueReceived" = "{user} sent you a unique collectible item";
|
||||
"ActionGiftUniqueSent" = "You sent a unique collectible item";
|
||||
"ActionStarGiftReceived" = "{user} sent you a gift for {cost}";
|
||||
"ActionStarGiftSent" = "You sent a gift for {cost}";
|
||||
"ActionStarGiftUpgradedUser" = "{user} turned the gift from you into a unique collectible";
|
||||
"ActionStarGiftUpgradedChannel" = "{user} turned this gift to {channel} into a unique collectible";
|
||||
"ActionStarGiftUpgradedChannelYou" = "You turned this gift to {channel} into a unique collectible";
|
||||
"ActionStarGiftUpgradedMine" = "You turned the gift from {user} into a unique collectible";
|
||||
"ActionStarGiftUpgradedSelf" = "You turned this gift into a unique collectible";
|
||||
"ActionStarGiftTransferred" = "{user} transferred you a gift";
|
||||
"ActionStarGiftTransferredChannel" = "{user} transferred a gift to {channel}";
|
||||
"ActionStarGiftTransferredChannelYou" = "You transferred a gift to {channel}";
|
||||
"ActionStarGiftTransferredMine" = "You transferred a gift to {user}";
|
||||
"ActionStarGiftTransferredSelf" = "You transferred a unique collectible";
|
||||
"ActionStarGiftTransferredUnknown" = "Someone transferred you a gift";
|
||||
"ActionStarGiftTransferredUnknownChannel" = "Someone transferred a gift to {channel}";
|
||||
"ActionStarGiftReceivedAnonymous" = "Someone sent you a gift for {cost}";
|
||||
"ActionStarGiftSentChannel" = "{user} sent a gift to {channel} for {cost}";
|
||||
"ActionStarGiftSentChannelYou" = "You sent a gift to {channel} for {cost}";
|
||||
"ActionStarGiftSelfBought" = "You bought a gift for {cost}";
|
||||
"ActionStarGiftTo" = "Gift to {peer}";
|
||||
"ActionStarGiftFrom" = "Gift from {peer}";
|
||||
"ActionStarGiftConvertText" = "{peer} can add this gift to their profile or convert it to {amount}.";
|
||||
"ActionStarGiftConvertTextYou" = "Add this gift to your profile or convert it to {amount}.";
|
||||
"ActionStarGiftNoConvertText" = "{peer} can add this gift to their profile.";
|
||||
"ActionStarGiftNoConvertTextYou" = "You can add this gift to your profile.";
|
||||
"ActionStarGiftConvertedText" = "{peer} converted this gift to {amount}.";
|
||||
"ActionStarGiftConvertedTextYou" = "You converted this gift to {amount}.";
|
||||
"ActionStarGiftChannelText" = "Add this gift to your channel's profile or convert it to {amount}.";
|
||||
"ActionStarGiftDisplaying" = "You kept this gift on your profile.";
|
||||
"ActionStarGiftChannelDisplaying" = "This gift is displayed to visitors of your channel.";
|
||||
"ActionStarGiftUpgradeText" = "{peer} can turn this gift to a unique collectible.";
|
||||
"ActionStarGiftUpgradeTextYou" = "Tap “Unpack” to turn this gift to a unique collectible.";
|
||||
"ActionStarGiftUpgraded" = "This gift was upgraded.";
|
||||
"ActionStarGiftUnpack" = "Unpack";
|
||||
"ActionStarGiftLimitedRibbon" = "1 of {total}";
|
||||
"ActionStarGiftUniqueRibbon" = "gift";
|
||||
"ActionStarGiftUniqueModel" = "Model";
|
||||
"ActionStarGiftUniqueBackdrop" = "Backdrop";
|
||||
"ActionStarGiftUniqueSymbol" = "Symbol";
|
||||
"ActionStarGiftSelf" = "Saved Gift";
|
||||
"ActionSuggestedPhotoYou" = "You suggested this photo for {user}'s Telegram profile.";
|
||||
"ActionSuggestedPhoto" = "{user} suggests this photo for your Telegram profile.";
|
||||
"ActionSuggestedPhotoButton" = "View Photo";
|
||||
"ActionSuggestedVideoTitle" = "Suggested Video";
|
||||
"ActionSuggestedVideoText" = "Do you want to use this photo for your profile?";
|
||||
"ActionSuggestedPhotoUpdatedTitle" = "Photo Updated";
|
||||
"ActionSuggestedPhotoUpdatedDescription" = "You can change it in Settings";
|
||||
"ActionAttachMenuBotAllowed" = "You allowed this bot to message you when you added it to your attachment menu.";
|
||||
"ActionWebappBotAllowed" = "You allowed this bot to message you in its web-app.";
|
||||
"ActionTopicClosedInside" = "Topic closed";
|
||||
"ActionTopicReopenedInside" = "Topic reopened";
|
||||
"ActionTopicHiddenInside" = "Topic hidden";
|
||||
"ActionTopicUnhiddenInside" = "Topic unhidden";
|
||||
"ActionTopicCreated" = "The topic \"{topic}\" was created";
|
||||
"ActionTopicClosed" = "{from} closed {topic}";
|
||||
"ActionTopicReopened" = "{from} reopened {topic}";
|
||||
"ActionTopicHidden" = "{topic} was hidden";
|
||||
"ActionTopicUnhidden" = "{topic} was unhidden";
|
||||
"ActionTopicPlaceholder" = "topic";
|
||||
"ActionTopicRenamed" = "{from} renamed the {link} to \"{title}\"";
|
||||
"ActionTopicIconChanged" = "{from} changed the {link} icon to {emoji}";
|
||||
"ActionTopicIconRemoved" = "{from} removed the {link} icon";
|
||||
"ActionTopicIconAndRenamed" = "{from} changed the {link} name and icon to \"{topic}\"";
|
||||
"ActionGiveawayStartedGroup" = "{from} just started a giveaway of Telegram Premium subscriptions to its members.";
|
||||
"ActionGiveawayStarted" = "{from} just started a giveaway of Telegram Premium subscriptions for its followers.";
|
||||
"ActionGiveawayStarsStartedGroup" = "{from} just started a giveaway of {amount} to its members.";
|
||||
"ActionGiveawayStarsStarted" = "{from} just started a giveaway of {amount} to its followers.";
|
||||
"ActionGiveawayResults_one" = "{count} winner of the giveaway was randomly selected by Telegram and received private messages with giftcodes.";
|
||||
"ActionGiveawayResults_other" = "{count} winners of the giveaway were randomly selected by Telegram and received private messages with giftcodes.";
|
||||
"ActionGiveawayResultsSome" = "Some winners of the giveaway were randomly selected by Telegram and received private messages with giftcodes.";
|
||||
"ActionGiveawayResultsStars_one" = "{count} winner of the giveaway was randomly selected by Telegram and received their prize.";
|
||||
"ActionGiveawayResultsStars_other" = "{count} winners of the giveaway were randomly selected by Telegram and received their prize.";
|
||||
"ActionGiveawayResultsStarsSome" = "Some winners of the giveaway were randomly selected by Telegram and received their prize.";
|
||||
"ActionGiveawayResultsNone" = "No winners of the giveaway could be selected.";
|
||||
"ActionOpenGiftButton" = "Open Gift";
|
||||
"ActionViewButton" = "View";
|
||||
"ActionGiveawayResultTitle" = "Congratulations!";
|
||||
"ActionGiveawayResultPremiumText_one" = "You won a prize in a giveaway organized by {channel}.\n\nYour prize is a **Telegram Premium** subscription for **{months}** month.";
|
||||
"ActionGiveawayResultPremiumText_other" = "You won a prize in a giveaway organized by {channel}.\n\nYour prize is a **Telegram Premium** subscription for **{months}** months.";
|
||||
"ActionGiftCodePremiumText_one" = "You've received a gift from {channel}.\n\nYour gift is a **Telegram Premium** subscription for {months} month.";
|
||||
"ActionGiftCodePremiumText_other" = "You've received a gift from {channel}.\n\nYour gift is a **Telegram Premium** subscription for {months} months.";
|
||||
"ActionGiveawayResultStarsText_one" = "You won a prize in a giveaway organized by {channel}.\n\nYour prize is **{amount}** Star.";
|
||||
"ActionGiveawayResultStarsText_other" = "You won a prize in a giveaway organized by {channel}.\n\nYour prize is **{amount}** Stars.";
|
||||
"ActionGiftPremiumTitle_one" = "{months} Month Premium";
|
||||
"ActionGiftPremiumTitle_other" = "{months} Months Premium";
|
||||
"ActionGiftPremiumText" = "Subscription for exclusive Telegram features.";
|
||||
"ActionGiftStarsTitle_one" = "{amount} Star";
|
||||
"ActionGiftStarsTitle_other" = "{amount} Stars";
|
||||
"ActionGiftStarsText" = "Use Stars to unlock content and services on Telegram.";
|
||||
"ActionBoostApplyYou_one" = "You boosted the group";
|
||||
"ActionBoostApplyYou_other" = "You boosted the group {count} times";
|
||||
"ActionBoostApply_one" = "{from} boosted the group";
|
||||
"ActionBoostApply_other" = "{from} boosted the group {count} times";
|
||||
"ActionPaymentRefunded" = "{peer} refunded {amount}";
|
||||
"ActionMigratedFrom" = "Migrated from {chat}";
|
||||
"ActionMigratedTo" = "Migrated to {chat}";
|
||||
"ActionHistoryCleared" = "History was cleared";
|
||||
"UniqueStatusWearTitle" = "Wear {gift}";
|
||||
"UniqueStatusBenefitsDescription" = "and get these benefits:";
|
||||
"UniqueStatusBadgeBenefitTitle" = "Radiant Badge";
|
||||
|
||||
@ -88,7 +88,7 @@ const CustomEmoji: FC<OwnProps> = ({
|
||||
const [shouldPlay, setShouldPlay] = useState(true);
|
||||
|
||||
const hasCustomColor = customEmoji?.shouldUseTextColor;
|
||||
const customColor = useDynamicColorListener(containerRef, !hasCustomColor);
|
||||
const customColor = useDynamicColorListener(containerRef, undefined, !hasCustomColor);
|
||||
|
||||
const handleVideoEnded = useLastCallback((e) => {
|
||||
if (!loopLimit) return;
|
||||
|
||||
@ -5,12 +5,12 @@ import type {
|
||||
ApiFormattedText, ApiMessage, ApiPoll, ApiTypeStory,
|
||||
} from '../../api/types';
|
||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
import { ApiMessageEntityTypes } from '../../api/types';
|
||||
|
||||
import {
|
||||
extractMessageText,
|
||||
getMessagePollId,
|
||||
groupStatetefulContent,
|
||||
groupStatefulContent,
|
||||
isActionMessage,
|
||||
} from '../../global/helpers';
|
||||
import {
|
||||
getMessageSummaryDescription,
|
||||
@ -24,6 +24,7 @@ import renderText from './helpers/renderText';
|
||||
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
|
||||
import ActionMessageText from '../middle/message/ActionMessageText';
|
||||
import MessageText from './MessageText';
|
||||
|
||||
type OwnProps = {
|
||||
@ -59,14 +60,13 @@ function MessageSummary({
|
||||
observeIntersectionForPlaying,
|
||||
}: OwnProps & StateProps) {
|
||||
const lang = useOldLang();
|
||||
const { text, entities } = extractMessageText(message, inChatList) || {};
|
||||
const hasSpoilers = entities?.some((e) => e.type === ApiMessageEntityTypes.Spoiler);
|
||||
const hasCustomEmoji = entities?.some((e) => e.type === ApiMessageEntityTypes.CustomEmoji);
|
||||
const extractedText = extractMessageText(message, inChatList);
|
||||
const hasPoll = Boolean(getMessagePollId(message));
|
||||
const isAction = isActionMessage(message);
|
||||
|
||||
const statefulContent = groupStatetefulContent({ poll, story });
|
||||
const statefulContent = groupStatefulContent({ poll, story });
|
||||
|
||||
if ((!text || (!hasSpoilers && !hasCustomEmoji)) && !hasPoll) {
|
||||
if (!extractedText && !hasPoll && !isAction) {
|
||||
const summaryText = translatedText?.text
|
||||
|| getMessageSummaryText(lang, message, statefulContent, noEmoji, truncateLength);
|
||||
const trimmedText = trimText(summaryText, truncateLength);
|
||||
@ -83,12 +83,16 @@ function MessageSummary({
|
||||
}
|
||||
|
||||
function renderMessageText() {
|
||||
if (isAction) {
|
||||
return <ActionMessageText message={message} asPreview />;
|
||||
}
|
||||
|
||||
return (
|
||||
<MessageText
|
||||
messageOrStory={message}
|
||||
translatedText={translatedText}
|
||||
highlight={highlight}
|
||||
isSimple
|
||||
asPreview
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
withTranslucentThumbs={withTranslucentThumbs}
|
||||
|
||||
@ -20,7 +20,7 @@ interface OwnProps {
|
||||
isForAnimation?: boolean;
|
||||
emojiSize?: number;
|
||||
highlight?: string;
|
||||
isSimple?: boolean;
|
||||
asPreview?: boolean;
|
||||
truncateLength?: number;
|
||||
isProtected?: boolean;
|
||||
observeIntersectionForLoading?: ObserveFn;
|
||||
@ -42,7 +42,7 @@ function MessageText({
|
||||
isForAnimation,
|
||||
emojiSize,
|
||||
highlight,
|
||||
isSimple,
|
||||
asPreview,
|
||||
truncateLength,
|
||||
isProtected,
|
||||
observeIntersectionForLoading,
|
||||
@ -98,7 +98,7 @@ function MessageText({
|
||||
emojiSize,
|
||||
shouldRenderAsHtml,
|
||||
containerId,
|
||||
isSimple,
|
||||
asPreview,
|
||||
isProtected,
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
|
||||
28
src/components/common/MiniTable.module.scss
Normal file
28
src/components/common/MiniTable.module.scss
Normal file
@ -0,0 +1,28 @@
|
||||
.root {
|
||||
display: grid;
|
||||
grid-template-columns: min-content 1fr;
|
||||
justify-content: center;
|
||||
gap: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.5rem;
|
||||
|
||||
text-align: initial;
|
||||
|
||||
position: relative;
|
||||
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.key {
|
||||
justify-self: flex-end;
|
||||
font-weight: var(--font-weight-normal);
|
||||
}
|
||||
|
||||
.value {
|
||||
justify-self: flex-start;
|
||||
width: 100%; // Grid ellipsis hack
|
||||
|
||||
font-weight: var(--font-weight-semibold);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
36
src/components/common/MiniTable.tsx
Normal file
36
src/components/common/MiniTable.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import React, { memo, type TeactNode } from '../../lib/teact/teact';
|
||||
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
|
||||
import styles from './MiniTable.module.scss';
|
||||
|
||||
export type TableEntry = [TeactNode, TeactNode];
|
||||
|
||||
type OwnProps = {
|
||||
data: TableEntry[];
|
||||
className?: string;
|
||||
style?: string;
|
||||
valueClassName?: string;
|
||||
keyClassName?: string;
|
||||
};
|
||||
|
||||
const MiniTable = ({
|
||||
data,
|
||||
style,
|
||||
className,
|
||||
valueClassName,
|
||||
keyClassName,
|
||||
}: OwnProps) => {
|
||||
return (
|
||||
<div className={buildClassName(styles.root, className)} style={style}>
|
||||
{data.map(([key, value]) => (
|
||||
<>
|
||||
<div className={buildClassName(styles.key, keyClassName)}>{key}</div>
|
||||
<div className={buildClassName(styles.value, valueClassName)}>{value}</div>
|
||||
</>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(MiniTable);
|
||||
@ -103,7 +103,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
||||
const menuRef = useRef<HTMLDivElement>(null);
|
||||
const lang = useOldLang();
|
||||
const hasCustomColor = sticker.shouldUseTextColor;
|
||||
const customColor = useDynamicColorListener(ref, !hasCustomColor);
|
||||
const customColor = useDynamicColorListener(ref, undefined, !hasCustomColor);
|
||||
|
||||
const {
|
||||
id, stickerSetInfo,
|
||||
|
||||
@ -124,7 +124,7 @@ const StickerView: FC<OwnProps> = ({
|
||||
const [isPlayerReady, markPlayerReady] = useFlag();
|
||||
const isFullMediaReady = shouldRenderFullMedia && (isStatic || isPlayerReady);
|
||||
|
||||
const thumbDataUri = useThumbnail(sticker);
|
||||
const thumbDataUri = useThumbnail(sticker.thumbnail);
|
||||
const thumbData = cachedPreview || previewMediaData || thumbDataUri;
|
||||
const isThumbOpaque = sharedCanvasRef && !withTranslucentThumb;
|
||||
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, { memo } from '../../lib/teact/teact';
|
||||
|
||||
import type { ThreadId } from '../../types';
|
||||
|
||||
import { GENERAL_TOPIC_ID } from '../../config';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { getTopicDefaultIcon } from '../../util/forumColors';
|
||||
@ -14,7 +16,7 @@ import styles from './TopicDefaultIcon.module.scss';
|
||||
type OwnProps = {
|
||||
className?: string;
|
||||
letterClassName?: string;
|
||||
topicId: number;
|
||||
topicId: ThreadId;
|
||||
iconColor?: number;
|
||||
title: string;
|
||||
onClick?: NoneToVoidFunction;
|
||||
|
||||
@ -53,7 +53,7 @@ const WebLink: FC<OwnProps> = ({
|
||||
linkData = {
|
||||
siteName: domain.replace(/^www./, ''),
|
||||
url: url.includes('://') ? url : url.includes('@') ? `mailto:${url}` : `http://${url}`,
|
||||
formattedDescription: getMessageText(message) !== url
|
||||
formattedDescription: getMessageText(message)?.text !== url
|
||||
? renderMessageSummary(lang, message, undefined, undefined, MAX_TEXT_LENGTH)
|
||||
: undefined,
|
||||
} as ApiWebPageWithFormatted;
|
||||
|
||||
@ -15,7 +15,6 @@ import {
|
||||
getMessageMediaHash,
|
||||
getMessageRoundVideo,
|
||||
getPeerTitle,
|
||||
isActionMessage,
|
||||
isChatChannel,
|
||||
isChatGroup,
|
||||
isMessageTranslatable,
|
||||
@ -35,7 +34,6 @@ import useOldLang from '../../../hooks/useOldLang';
|
||||
import useThumbnail from '../../../hooks/useThumbnail';
|
||||
import useMessageTranslation from '../../middle/message/hooks/useMessageTranslation';
|
||||
|
||||
import ActionMessage from '../../middle/ActionMessage';
|
||||
import RippleEffect from '../../ui/RippleEffect';
|
||||
import Icon from '../icons/Icon';
|
||||
import MediaSpoiler from '../MediaSpoiler';
|
||||
@ -133,7 +131,7 @@ const EmbeddedMessage: FC<OwnProps> = ({
|
||||
return renderTextWithEntities({
|
||||
text: replyInfo.quoteText.text,
|
||||
entities: replyInfo.quoteText.entities,
|
||||
noLineBreaks: isInComposer,
|
||||
asPreview: true,
|
||||
emojiSize: EMOJI_SIZE,
|
||||
});
|
||||
}
|
||||
@ -142,17 +140,6 @@ const EmbeddedMessage: FC<OwnProps> = ({
|
||||
return customText || renderMediaContentType(containedMedia) || NBSP;
|
||||
}
|
||||
|
||||
if (isActionMessage(message)) {
|
||||
return (
|
||||
<ActionMessage
|
||||
message={message}
|
||||
isEmbedded
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<MessageSummary
|
||||
message={message}
|
||||
|
||||
@ -1,392 +0,0 @@
|
||||
import React from '../../../lib/teact/teact';
|
||||
|
||||
import type {
|
||||
ApiChat, ApiGroupCall, ApiMessage, ApiTopic, ApiUser,
|
||||
} from '../../../api/types';
|
||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
import type { OldLangFn } from '../../../hooks/useOldLang';
|
||||
import type { TextPart } from '../../../types';
|
||||
|
||||
import { SERVICE_NOTIFICATIONS_USER_ID, STARS_CURRENCY_CODE } from '../../../config';
|
||||
import {
|
||||
getChatTitle,
|
||||
getExpiredMessageDescription,
|
||||
getUserFullName,
|
||||
isExpiredMessage,
|
||||
} from '../../../global/helpers';
|
||||
import { getMessageSummaryText } from '../../../global/helpers/messageSummary';
|
||||
import { formatCurrencyAsString } from '../../../util/formatCurrency';
|
||||
import trimText from '../../../util/trimText';
|
||||
import renderText from './renderText';
|
||||
|
||||
import ChatLink from '../ChatLink';
|
||||
import CustomEmoji from '../CustomEmoji';
|
||||
import GroupCallLink from '../GroupCallLink';
|
||||
import MessageLink from '../MessageLink';
|
||||
import MessageSummary from '../MessageSummary';
|
||||
import TopicDefaultIcon from '../TopicDefaultIcon';
|
||||
import UserLink from '../UserLink';
|
||||
|
||||
interface RenderOptions {
|
||||
asPlainText?: boolean;
|
||||
isEmbedded?: boolean;
|
||||
}
|
||||
|
||||
const MAX_LENGTH = 32;
|
||||
const NBSP = '\u00A0';
|
||||
|
||||
export function renderActionMessageText(
|
||||
oldLang: OldLangFn,
|
||||
message: ApiMessage,
|
||||
actionOriginUser?: ApiUser,
|
||||
actionOriginChat?: ApiChat,
|
||||
targetUsers?: ApiUser[],
|
||||
targetMessage?: ApiMessage,
|
||||
targetChatId?: string,
|
||||
topic?: ApiTopic,
|
||||
options: RenderOptions = {},
|
||||
observeIntersectionForLoading?: ObserveFn,
|
||||
observeIntersectionForPlaying?: ObserveFn,
|
||||
) {
|
||||
if (isExpiredMessage(message)) {
|
||||
return getExpiredMessageDescription(oldLang, message);
|
||||
}
|
||||
|
||||
if (!message.content?.action) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const {
|
||||
text, translationValues, amount, currency, call, score, topicEmojiIconId, giftCryptoInfo, pluralValue,
|
||||
} = message.content.action;
|
||||
|
||||
const noLinks = options.asPlainText || options.isEmbedded;
|
||||
|
||||
const content: TextPart[] = [];
|
||||
const translationKey = text === 'Chat.Service.Group.UpdatedPinnedMessage1' && !targetMessage
|
||||
? 'Message.PinnedGenericMessage'
|
||||
: text;
|
||||
|
||||
let unprocessed = oldLang(
|
||||
translationKey, translationValues?.length ? translationValues : undefined, undefined, pluralValue,
|
||||
);
|
||||
if (translationKey.includes('ScoredInGame')) { // Translation hack for games
|
||||
unprocessed = unprocessed.replace('un1', '%action_origin%').replace('un2', '%message%');
|
||||
}
|
||||
if (translationKey === 'ActionGiftOutbound') { // Translation hack for Premium Gift
|
||||
unprocessed = unprocessed.replace('un2', '%gift_payment_amount%').replace(/\*\*/g, '');
|
||||
}
|
||||
if (translationKey === 'ActionGiftInbound') { // Translation hack for Premium Gift
|
||||
unprocessed = unprocessed
|
||||
.replace('un1', '%action_origin%')
|
||||
.replace('un2', '%gift_payment_amount%')
|
||||
.replace(/\*\*/g, '');
|
||||
}
|
||||
if (translationKey === 'ActionRefunded') {
|
||||
unprocessed = unprocessed
|
||||
.replace('un1', '%action_origin%')
|
||||
.replace('%1$s', '%gift_payment_amount%');
|
||||
}
|
||||
if (translationKey === 'ActionRequestedPeer') {
|
||||
unprocessed = unprocessed
|
||||
.replace('un1', '%star_target_user%')
|
||||
.replace('un2', '%action_origin%')
|
||||
.replace(/\*\*/g, '');
|
||||
}
|
||||
if (translationKey.startsWith('Notification.StarsGift.Upgrade')) {
|
||||
unprocessed = unprocessed
|
||||
.replace('%@', '%action_origin_chat%');
|
||||
}
|
||||
if (translationKey.startsWith('ActionUniqueGiftTransfer')) {
|
||||
unprocessed = unprocessed
|
||||
.replace('un1', '%action_origin%')
|
||||
.replace(/\*\*/g, '');
|
||||
}
|
||||
if (translationKey === 'BoostingReceivedPrizeFrom') {
|
||||
unprocessed = unprocessed
|
||||
.replace('**%s**', '%target_chat%')
|
||||
.replace(/\*\*/g, '');
|
||||
}
|
||||
let processed: TextPart[];
|
||||
|
||||
if (unprocessed.includes('%star_target_user%')) {
|
||||
processed = processPlaceholder(
|
||||
unprocessed,
|
||||
'%star_target_user%',
|
||||
targetUsers
|
||||
? targetUsers.map((user) => renderUserContent(user, noLinks)).filter(Boolean)
|
||||
: 'User',
|
||||
);
|
||||
|
||||
unprocessed = processed.pop() as string;
|
||||
content.push(...processed);
|
||||
}
|
||||
|
||||
processed = processPlaceholder(
|
||||
unprocessed,
|
||||
'%action_origin%',
|
||||
actionOriginUser ? (
|
||||
actionOriginUser.id === SERVICE_NOTIFICATIONS_USER_ID
|
||||
? oldLang('StarsTransactionUnknown')
|
||||
: renderUserContent(actionOriginUser, noLinks) || NBSP
|
||||
) : actionOriginChat ? (
|
||||
renderChatContent(oldLang, actionOriginChat, noLinks) || NBSP
|
||||
) : 'User',
|
||||
'',
|
||||
);
|
||||
|
||||
unprocessed = processed.pop() as string;
|
||||
content.push(...processed);
|
||||
|
||||
processed = processPlaceholder(
|
||||
unprocessed,
|
||||
'%action_origin_chat%',
|
||||
actionOriginChat ? (
|
||||
renderChatContent(oldLang, actionOriginChat, noLinks) || NBSP
|
||||
) : 'Chat',
|
||||
'',
|
||||
);
|
||||
|
||||
unprocessed = processed.pop() as string;
|
||||
content.push(...processed);
|
||||
|
||||
if (unprocessed.includes('%payment_amount%')) {
|
||||
processed = processPlaceholder(
|
||||
unprocessed,
|
||||
'%payment_amount%',
|
||||
formatCurrencyAsString(amount!, currency!, oldLang.code),
|
||||
);
|
||||
unprocessed = processed.pop() as string;
|
||||
content.push(...processed);
|
||||
}
|
||||
|
||||
if (unprocessed.includes('%action_topic%')) {
|
||||
const topicEmoji = topic?.iconEmojiId
|
||||
? <CustomEmoji documentId={topic.iconEmojiId} isSelectable />
|
||||
: '';
|
||||
const topicString = topic ? `${topic.title}` : 'a topic';
|
||||
processed = processPlaceholder(
|
||||
unprocessed,
|
||||
'%action_topic%',
|
||||
[topicEmoji, topicString],
|
||||
'',
|
||||
);
|
||||
unprocessed = processed.pop() as string;
|
||||
content.push(...processed);
|
||||
}
|
||||
|
||||
if (unprocessed.includes('%action_topic_icon%')) {
|
||||
const topicIcon = topicEmojiIconId || topic?.iconEmojiId;
|
||||
const hasIcon = topicIcon && topicIcon !== '0';
|
||||
processed = processPlaceholder(
|
||||
unprocessed,
|
||||
'%action_topic_icon%',
|
||||
hasIcon ? <CustomEmoji documentId={topicIcon!} isSelectable />
|
||||
: topic ? <TopicDefaultIcon topicId={topic!.id} title={topic!.title} /> : '...',
|
||||
);
|
||||
unprocessed = processed.pop() as string;
|
||||
content.push(...processed);
|
||||
}
|
||||
|
||||
if (unprocessed.includes('%gift_payment_amount%')) {
|
||||
let priceText;
|
||||
|
||||
if (currency && currency === STARS_CURRENCY_CODE) {
|
||||
priceText = oldLang('ActionGiftStarsTitle', amount!);
|
||||
} else {
|
||||
const price = formatCurrencyAsString(amount!, currency!, oldLang.code);
|
||||
|
||||
if (giftCryptoInfo) {
|
||||
const cryptoPrice = formatCurrencyAsString(giftCryptoInfo.amount, giftCryptoInfo.currency, oldLang.code);
|
||||
priceText = `${cryptoPrice} (${price})`;
|
||||
} else {
|
||||
priceText = price;
|
||||
}
|
||||
}
|
||||
|
||||
processed = processPlaceholder(
|
||||
unprocessed,
|
||||
'%gift_payment_amount%',
|
||||
priceText,
|
||||
);
|
||||
unprocessed = processed.pop() as string;
|
||||
content.push(...processed);
|
||||
}
|
||||
|
||||
if (unprocessed.includes('%amount%')) {
|
||||
processed = processPlaceholder(
|
||||
unprocessed,
|
||||
'%amount%',
|
||||
amount,
|
||||
);
|
||||
unprocessed = processed.pop() as string;
|
||||
content.push(...processed);
|
||||
}
|
||||
|
||||
if (unprocessed.includes('%score%')) {
|
||||
processed = processPlaceholder(
|
||||
unprocessed,
|
||||
'%score%',
|
||||
score!.toString(),
|
||||
);
|
||||
unprocessed = processed.pop() as string;
|
||||
content.push(...processed);
|
||||
}
|
||||
|
||||
processed = processPlaceholder(
|
||||
unprocessed,
|
||||
'%target_user%',
|
||||
targetUsers
|
||||
? targetUsers.map((user) => renderUserContent(user, noLinks)).filter(Boolean)
|
||||
: 'User',
|
||||
'',
|
||||
);
|
||||
|
||||
unprocessed = processed.pop() as string;
|
||||
content.push(...processed);
|
||||
|
||||
processed = processPlaceholder(
|
||||
unprocessed,
|
||||
'%message%',
|
||||
targetMessage
|
||||
? renderMessageContent(
|
||||
oldLang, targetMessage, options, observeIntersectionForLoading, observeIntersectionForPlaying,
|
||||
)
|
||||
: 'a message',
|
||||
);
|
||||
unprocessed = processed.pop() as string;
|
||||
content.push(...processed);
|
||||
|
||||
processed = processPlaceholder(
|
||||
unprocessed,
|
||||
'%product%',
|
||||
targetMessage
|
||||
? renderProductContent(targetMessage)
|
||||
: 'a product',
|
||||
);
|
||||
unprocessed = processed.pop() as string;
|
||||
content.push(...processed);
|
||||
|
||||
processed = processPlaceholder(
|
||||
unprocessed,
|
||||
'%target_chat%',
|
||||
targetChatId
|
||||
? renderMigratedContent(targetChatId, noLinks)
|
||||
: 'another chat',
|
||||
'',
|
||||
);
|
||||
processed.forEach((part) => {
|
||||
content.push(...renderText(part));
|
||||
});
|
||||
|
||||
if (options.asPlainText) {
|
||||
return content.join('').trim();
|
||||
}
|
||||
|
||||
if (call) {
|
||||
return renderGroupCallContent(call, content);
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
function renderProductContent(message: ApiMessage) {
|
||||
return message.content && message.content.invoice
|
||||
? message.content.invoice.title
|
||||
: 'a product';
|
||||
}
|
||||
|
||||
function renderMessageContent(
|
||||
lang: OldLangFn,
|
||||
message: ApiMessage,
|
||||
options: RenderOptions = {},
|
||||
observeIntersectionForLoading?: ObserveFn,
|
||||
observeIntersectionForPlaying?: ObserveFn,
|
||||
) {
|
||||
const { asPlainText, isEmbedded } = options;
|
||||
|
||||
if (asPlainText) {
|
||||
return getMessageSummaryText(lang, message, undefined, undefined, MAX_LENGTH);
|
||||
}
|
||||
|
||||
const messageSummary = (
|
||||
<MessageSummary
|
||||
message={message}
|
||||
truncateLength={MAX_LENGTH}
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
withTranslucentThumbs
|
||||
/>
|
||||
);
|
||||
|
||||
if (isEmbedded) {
|
||||
return messageSummary;
|
||||
}
|
||||
|
||||
return (
|
||||
<MessageLink className="action-link" message={message}>{messageSummary}</MessageLink>
|
||||
);
|
||||
}
|
||||
|
||||
function renderGroupCallContent(groupCall: Partial<ApiGroupCall>, text: TextPart[]): string | TextPart | undefined {
|
||||
return (
|
||||
<GroupCallLink groupCall={groupCall}>
|
||||
{text}
|
||||
</GroupCallLink>
|
||||
);
|
||||
}
|
||||
|
||||
function renderUserContent(sender: ApiUser, noLinks?: boolean): string | TextPart | undefined {
|
||||
const text = trimText(getUserFullName(sender), MAX_LENGTH);
|
||||
|
||||
if (noLinks) {
|
||||
return renderText(text!);
|
||||
}
|
||||
|
||||
return <UserLink className="action-link" sender={sender}>{sender && renderText(text!)}</UserLink>;
|
||||
}
|
||||
|
||||
function renderChatContent(lang: OldLangFn, chat: ApiChat, noLinks?: boolean): string | TextPart | undefined {
|
||||
const text = trimText(getChatTitle(lang, chat), MAX_LENGTH);
|
||||
|
||||
if (noLinks) {
|
||||
return renderText(text!);
|
||||
}
|
||||
|
||||
return <ChatLink className="action-link" chatId={chat.id}>{chat && renderText(text!)}</ChatLink>;
|
||||
}
|
||||
|
||||
function renderMigratedContent(chatId: string, noLinks?: boolean): string | TextPart | undefined {
|
||||
const text = 'another chat';
|
||||
|
||||
if (noLinks) {
|
||||
return text;
|
||||
}
|
||||
|
||||
return <ChatLink className="action-link underlined-link" chatId={chatId}>{text}</ChatLink>;
|
||||
}
|
||||
|
||||
function processPlaceholder(
|
||||
text: string, placeholder: string, replaceValue?: TextPart | TextPart[], separator = ',',
|
||||
): TextPart[] {
|
||||
const placeholderPosition = text.indexOf(placeholder);
|
||||
if (placeholderPosition < 0 || !replaceValue) {
|
||||
return [text];
|
||||
}
|
||||
|
||||
const content: TextPart[] = [];
|
||||
content.push(text.substring(0, placeholderPosition));
|
||||
if (Array.isArray(replaceValue)) {
|
||||
replaceValue.forEach((value, index) => {
|
||||
content.push(value);
|
||||
if (index + 1 < replaceValue.length) {
|
||||
content.push(`${separator} `);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
content.push(replaceValue);
|
||||
}
|
||||
content.push(text.substring(placeholderPosition + placeholder.length));
|
||||
|
||||
return content.flat();
|
||||
}
|
||||
@ -24,7 +24,7 @@ export function renderMessageText({
|
||||
message,
|
||||
highlight,
|
||||
emojiSize,
|
||||
isSimple,
|
||||
asPreview,
|
||||
truncateLength,
|
||||
isProtected,
|
||||
forcePlayback,
|
||||
@ -34,7 +34,7 @@ export function renderMessageText({
|
||||
message: ApiMessage | ApiSponsoredMessage;
|
||||
highlight?: string;
|
||||
emojiSize?: number;
|
||||
isSimple?: boolean;
|
||||
asPreview?: boolean;
|
||||
truncateLength?: number;
|
||||
isProtected?: boolean;
|
||||
forcePlayback?: boolean;
|
||||
@ -44,7 +44,7 @@ export function renderMessageText({
|
||||
const { text, entities } = message.content.text || {};
|
||||
|
||||
if (!text) {
|
||||
const contentNotSupportedText = getMessageText(message);
|
||||
const contentNotSupportedText = getMessageText(message)?.text;
|
||||
return contentNotSupportedText ? [trimText(contentNotSupportedText, truncateLength)] : undefined;
|
||||
}
|
||||
|
||||
@ -57,7 +57,7 @@ export function renderMessageText({
|
||||
emojiSize,
|
||||
shouldRenderAsHtml,
|
||||
containerId: `${isForMediaViewer ? 'mv-' : ''}${messageKey}`,
|
||||
isSimple,
|
||||
asPreview,
|
||||
isProtected,
|
||||
forcePlayback,
|
||||
});
|
||||
@ -92,7 +92,7 @@ export function renderMessageSummary(
|
||||
const emojiWithSpace = emoji ? `${emoji} ` : '';
|
||||
|
||||
const text = renderMessageText({
|
||||
message, highlight, isSimple: true, truncateLength,
|
||||
message, highlight, asPreview: true, truncateLength,
|
||||
});
|
||||
const description = getMessageSummaryDescription(lang, message, statefulContent, text);
|
||||
|
||||
|
||||
@ -36,9 +36,8 @@ export function renderTextWithEntities({
|
||||
emojiSize,
|
||||
shouldRenderAsHtml,
|
||||
containerId,
|
||||
isSimple,
|
||||
asPreview,
|
||||
isProtected,
|
||||
noLineBreaks,
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
withTranslucentThumbs,
|
||||
@ -56,9 +55,8 @@ export function renderTextWithEntities({
|
||||
emojiSize?: number;
|
||||
shouldRenderAsHtml?: boolean;
|
||||
containerId?: string;
|
||||
isSimple?: boolean;
|
||||
asPreview?: boolean;
|
||||
isProtected?: boolean;
|
||||
noLineBreaks?: boolean;
|
||||
observeIntersectionForLoading?: ObserveFn;
|
||||
observeIntersectionForPlaying?: ObserveFn;
|
||||
withTranslucentThumbs?: boolean;
|
||||
@ -77,8 +75,7 @@ export function renderTextWithEntities({
|
||||
focusedQuote,
|
||||
emojiSize,
|
||||
shouldRenderAsHtml,
|
||||
isSimple,
|
||||
noLineBreaks,
|
||||
asPreview,
|
||||
});
|
||||
}
|
||||
|
||||
@ -113,8 +110,7 @@ export function renderTextWithEntities({
|
||||
focusedQuote,
|
||||
emojiSize,
|
||||
shouldRenderAsHtml,
|
||||
isSimple,
|
||||
noLineBreaks,
|
||||
asPreview,
|
||||
}) as TextPart[]);
|
||||
}
|
||||
}
|
||||
@ -164,8 +160,7 @@ export function renderTextWithEntities({
|
||||
highlight,
|
||||
focusedQuote,
|
||||
containerId,
|
||||
isSimple,
|
||||
noLineBreaks,
|
||||
asPreview,
|
||||
isProtected,
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
@ -199,8 +194,7 @@ export function renderTextWithEntities({
|
||||
focusedQuote,
|
||||
emojiSize,
|
||||
shouldRenderAsHtml,
|
||||
isSimple,
|
||||
noLineBreaks,
|
||||
asPreview,
|
||||
}) as TextPart[]);
|
||||
}
|
||||
}
|
||||
@ -254,16 +248,14 @@ function renderMessagePart({
|
||||
focusedQuote,
|
||||
emojiSize,
|
||||
shouldRenderAsHtml,
|
||||
isSimple,
|
||||
noLineBreaks,
|
||||
asPreview,
|
||||
} : {
|
||||
content: TextPart | TextPart[];
|
||||
highlight?: string;
|
||||
focusedQuote?: string;
|
||||
emojiSize?: number;
|
||||
shouldRenderAsHtml?: boolean;
|
||||
isSimple?: boolean;
|
||||
noLineBreaks?: boolean;
|
||||
asPreview?: boolean;
|
||||
}) {
|
||||
if (Array.isArray(content)) {
|
||||
const result: TextPart[] = [];
|
||||
@ -275,8 +267,7 @@ function renderMessagePart({
|
||||
focusedQuote,
|
||||
emojiSize,
|
||||
shouldRenderAsHtml,
|
||||
isSimple,
|
||||
noLineBreaks,
|
||||
asPreview,
|
||||
}));
|
||||
});
|
||||
|
||||
@ -291,7 +282,7 @@ function renderMessagePart({
|
||||
|
||||
const filters: TextFilter[] = [emojiFilter];
|
||||
const params: RenderTextParams = {};
|
||||
if (!isSimple && !noLineBreaks) {
|
||||
if (!asPreview) {
|
||||
filters.push('br');
|
||||
}
|
||||
|
||||
@ -380,8 +371,7 @@ function processEntity({
|
||||
highlight,
|
||||
focusedQuote,
|
||||
containerId,
|
||||
isSimple,
|
||||
noLineBreaks,
|
||||
asPreview,
|
||||
isProtected,
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
@ -400,8 +390,7 @@ function processEntity({
|
||||
highlight?: string;
|
||||
focusedQuote?: string;
|
||||
containerId?: string;
|
||||
isSimple?: boolean;
|
||||
noLineBreaks?: boolean;
|
||||
asPreview?: boolean;
|
||||
isProtected?: boolean;
|
||||
observeIntersectionForLoading?: ObserveFn;
|
||||
observeIntersectionForPlaying?: ObserveFn;
|
||||
@ -423,8 +412,7 @@ function processEntity({
|
||||
highlight,
|
||||
focusedQuote,
|
||||
emojiSize,
|
||||
isSimple,
|
||||
noLineBreaks,
|
||||
asPreview,
|
||||
});
|
||||
}
|
||||
|
||||
@ -432,7 +420,7 @@ function processEntity({
|
||||
return renderNestedMessagePart();
|
||||
}
|
||||
|
||||
if (isSimple) {
|
||||
if (asPreview) {
|
||||
const text = renderNestedMessagePart();
|
||||
if (entity.type === ApiMessageEntityTypes.Spoiler) {
|
||||
return <Spoiler>{text}</Spoiler>;
|
||||
|
||||
@ -20,13 +20,11 @@ import { MAIN_THREAD_ID } from '../../../api/types';
|
||||
import { StoryViewerOrigin } from '../../../types';
|
||||
|
||||
import {
|
||||
getMessageAction,
|
||||
groupStatetefulContent,
|
||||
groupStatefulContent,
|
||||
isUserId,
|
||||
isUserOnline,
|
||||
selectIsChatMuted,
|
||||
} from '../../../global/helpers';
|
||||
import { getMessageReplyInfo } from '../../../global/helpers/replies';
|
||||
import {
|
||||
selectCanAnimateInterface,
|
||||
selectChat,
|
||||
@ -101,9 +99,6 @@ type StateProps = {
|
||||
isMuted?: boolean;
|
||||
user?: ApiUser;
|
||||
userStatus?: ApiUserStatus;
|
||||
actionTargetUserIds?: string[];
|
||||
actionTargetMessage?: ApiMessage;
|
||||
actionTargetChatId?: string;
|
||||
lastMessageSender?: ApiPeer;
|
||||
lastMessageOutgoingStatus?: ApiMessageOutgoingStatus;
|
||||
draft?: ApiDraft;
|
||||
@ -135,11 +130,8 @@ const Chat: FC<OwnProps & StateProps> = ({
|
||||
isMuted,
|
||||
user,
|
||||
userStatus,
|
||||
actionTargetUserIds,
|
||||
lastMessageSender,
|
||||
lastMessageOutgoingStatus,
|
||||
actionTargetMessage,
|
||||
actionTargetChatId,
|
||||
offsetTop,
|
||||
draft,
|
||||
withInterfaceAnimations,
|
||||
@ -191,10 +183,7 @@ const Chat: FC<OwnProps & StateProps> = ({
|
||||
lastMessage,
|
||||
typingStatus,
|
||||
draft,
|
||||
statefulMediaContent: groupStatetefulContent({ story: lastMessageStory }),
|
||||
actionTargetMessage,
|
||||
actionTargetUserIds,
|
||||
actionTargetChatId,
|
||||
statefulMediaContent: groupStatefulContent({ story: lastMessageStory }),
|
||||
lastMessageTopic,
|
||||
lastMessageSender,
|
||||
observeIntersection,
|
||||
@ -458,12 +447,6 @@ export default memo(withGlobal<OwnProps>(
|
||||
const savedDialogSender = isSavedDialog && forwardInfo?.fromId ? selectPeer(global, forwardInfo.fromId) : undefined;
|
||||
const messageSender = lastMessage ? selectSender(global, lastMessage) : undefined;
|
||||
const lastMessageSender = savedDialogSender || messageSender;
|
||||
const replyToMessageId = lastMessage && getMessageReplyInfo(lastMessage)?.replyToMsgId;
|
||||
const lastMessageAction = lastMessage ? getMessageAction(lastMessage) : undefined;
|
||||
const actionTargetMessage = lastMessageAction && replyToMessageId
|
||||
? selectChatMessage(global, chat.id, replyToMessageId)
|
||||
: undefined;
|
||||
const { targetUserIds: actionTargetUserIds, targetChatId: actionTargetChatId } = lastMessageAction || {};
|
||||
|
||||
const {
|
||||
chatId: currentChatId,
|
||||
@ -489,9 +472,6 @@ export default memo(withGlobal<OwnProps>(
|
||||
chat,
|
||||
isMuted: selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global)),
|
||||
lastMessageSender,
|
||||
actionTargetUserIds,
|
||||
actionTargetChatId,
|
||||
actionTargetMessage,
|
||||
draft: selectDraft(global, chatId, MAIN_THREAD_ID),
|
||||
isSelected,
|
||||
isSelectedForum,
|
||||
|
||||
@ -9,8 +9,7 @@ import type {
|
||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
import type { ChatAnimationTypes } from './hooks';
|
||||
|
||||
import { getMessageAction, groupStatetefulContent } from '../../../global/helpers';
|
||||
import { getMessageReplyInfo } from '../../../global/helpers/replies';
|
||||
import { groupStatefulContent } from '../../../global/helpers';
|
||||
import {
|
||||
selectCanAnimateInterface,
|
||||
selectCanDeleteTopic,
|
||||
@ -62,10 +61,7 @@ type StateProps = {
|
||||
lastMessage?: ApiMessage;
|
||||
lastMessageStory?: ApiTypeStory;
|
||||
lastMessageOutgoingStatus?: ApiMessageOutgoingStatus;
|
||||
actionTargetMessage?: ApiMessage;
|
||||
actionTargetUserIds?: string[];
|
||||
lastMessageSender?: ApiPeer;
|
||||
actionTargetChatId?: string;
|
||||
typingStatus?: ApiTypingStatus;
|
||||
draft?: ApiDraft;
|
||||
canScrollDown?: boolean;
|
||||
@ -86,9 +82,6 @@ const Topic: FC<OwnProps & StateProps> = ({
|
||||
lastMessageOutgoingStatus,
|
||||
observeIntersection,
|
||||
canDelete,
|
||||
actionTargetMessage,
|
||||
actionTargetUserIds,
|
||||
actionTargetChatId,
|
||||
lastMessageSender,
|
||||
animationType,
|
||||
withInterfaceAnimations,
|
||||
@ -136,16 +129,13 @@ const Topic: FC<OwnProps & StateProps> = ({
|
||||
chatId,
|
||||
lastMessage,
|
||||
draft,
|
||||
actionTargetMessage,
|
||||
actionTargetUserIds,
|
||||
actionTargetChatId,
|
||||
lastMessageSender,
|
||||
lastMessageTopic: topic,
|
||||
observeIntersection,
|
||||
isTopic: true,
|
||||
typingStatus,
|
||||
topics,
|
||||
statefulMediaContent: groupStatetefulContent({ story: lastMessageStory }),
|
||||
statefulMediaContent: groupStatefulContent({ story: lastMessageStory }),
|
||||
|
||||
animationType,
|
||||
withInterfaceAnimations,
|
||||
@ -245,13 +235,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
|
||||
const lastMessage = selectChatMessage(global, chatId, topic.lastMessageId);
|
||||
const { isOutgoing } = lastMessage || {};
|
||||
const replyToMessageId = lastMessage && getMessageReplyInfo(lastMessage)?.replyToMsgId;
|
||||
const lastMessageSender = lastMessage && selectSender(global, lastMessage);
|
||||
const lastMessageAction = lastMessage ? getMessageAction(lastMessage) : undefined;
|
||||
const actionTargetMessage = lastMessageAction && replyToMessageId
|
||||
? selectChatMessage(global, chatId, replyToMessageId)
|
||||
: undefined;
|
||||
const { targetUserIds: actionTargetUserIds, targetChatId: actionTargetChatId } = lastMessageAction || {};
|
||||
const typingStatus = selectThreadParam(global, chatId, topic.id, 'typingStatus');
|
||||
const draft = selectDraft(global, chatId, topic.id);
|
||||
const threadInfo = selectThreadInfo(global, chatId, topic.id);
|
||||
@ -266,9 +250,6 @@ export default memo(withGlobal<OwnProps>(
|
||||
return {
|
||||
chat,
|
||||
lastMessage,
|
||||
actionTargetUserIds,
|
||||
actionTargetChatId,
|
||||
actionTargetMessage,
|
||||
lastMessageSender,
|
||||
typingStatus,
|
||||
canDelete: selectCanDeleteTopic(global, chatId, topic.id),
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import React, {
|
||||
useCallback, useLayoutEffect, useMemo, useRef,
|
||||
useCallback, useLayoutEffect, useRef,
|
||||
} from '../../../../lib/teact/teact';
|
||||
import { getGlobal } from '../../../../global';
|
||||
|
||||
import type {
|
||||
ApiChat, ApiDraft, ApiMessage, ApiPeer, ApiTopic, ApiTypingStatus,
|
||||
@ -12,7 +11,6 @@ import type { ObserveFn } from '../../../../hooks/useIntersectionObserver';
|
||||
import { ANIMATION_END_DELAY, CHAT_HEIGHT_PX } from '../../../../config';
|
||||
import { requestMutation } from '../../../../lib/fasterdom/fasterdom';
|
||||
import {
|
||||
getExpiredMessageDescription,
|
||||
getMessageIsSpoiler,
|
||||
getMessageMediaHash,
|
||||
getMessageMediaThumbDataUri,
|
||||
@ -20,18 +18,12 @@ import {
|
||||
getMessageSenderName,
|
||||
getMessageSticker,
|
||||
getMessageVideo,
|
||||
isActionMessage,
|
||||
isExpiredMessage,
|
||||
} from '../../../../global/helpers';
|
||||
import { isApiPeerChat } from '../../../../global/helpers/peers';
|
||||
import { getMessageReplyInfo } from '../../../../global/helpers/replies';
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
import { renderActionMessageText } from '../../../common/helpers/renderActionMessageText';
|
||||
import renderText from '../../../common/helpers/renderText';
|
||||
import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities';
|
||||
import { ChatAnimationTypes } from './useChatAnimationType';
|
||||
|
||||
import useEnsureMessage from '../../../../hooks/useEnsureMessage';
|
||||
import useEnsureStory from '../../../../hooks/useEnsureStory';
|
||||
import useMedia from '../../../../hooks/useMedia';
|
||||
import useOldLang from '../../../../hooks/useOldLang';
|
||||
@ -51,11 +43,8 @@ export default function useChatListEntry({
|
||||
chatId,
|
||||
typingStatus,
|
||||
draft,
|
||||
actionTargetMessage,
|
||||
actionTargetUserIds,
|
||||
lastMessageTopic,
|
||||
lastMessageSender,
|
||||
actionTargetChatId,
|
||||
observeIntersection,
|
||||
animationType,
|
||||
orderDiff,
|
||||
@ -71,11 +60,8 @@ export default function useChatListEntry({
|
||||
chatId: string;
|
||||
typingStatus?: ApiTypingStatus;
|
||||
draft?: ApiDraft;
|
||||
actionTargetMessage?: ApiMessage;
|
||||
actionTargetUserIds?: string[];
|
||||
lastMessageTopic?: ApiTopic;
|
||||
lastMessageSender?: ApiPeer;
|
||||
actionTargetChatId?: string;
|
||||
observeIntersection?: ObserveFn;
|
||||
isTopic?: boolean;
|
||||
isSavedDialog?: boolean;
|
||||
@ -89,11 +75,6 @@ export default function useChatListEntry({
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const isAction = lastMessage && isActionMessage(lastMessage);
|
||||
|
||||
const replyToMessageId = lastMessage && getMessageReplyInfo(lastMessage)?.replyToMsgId;
|
||||
useEnsureMessage(chatId, isAction ? replyToMessageId : undefined, actionTargetMessage);
|
||||
|
||||
const storyData = lastMessage?.content.storyData;
|
||||
const shouldTryLoadingStory = statefulMediaContent && !statefulMediaContent.story;
|
||||
|
||||
@ -106,16 +87,6 @@ export default function useChatListEntry({
|
||||
const mediaBlobUrl = useMedia(mediaHasPreview ? getMessageMediaHash(mediaContent, 'micro') : undefined);
|
||||
const isRoundVideo = Boolean(lastMessage && getMessageRoundVideo(lastMessage));
|
||||
|
||||
const actionTargetUsers = useMemo(() => {
|
||||
if (!actionTargetUserIds) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// No need for expensive global updates on users, so we avoid them
|
||||
const usersById = getGlobal().users.byId;
|
||||
return actionTargetUserIds.map((userId) => usersById[userId]).filter(Boolean);
|
||||
}, [actionTargetUserIds]);
|
||||
|
||||
const renderLastMessageOrTyping = useCallback(() => {
|
||||
if (!isSavedDialog && !isPreview
|
||||
&& typingStatus && lastMessage && typingStatus.timestamp > lastMessage.date * 1000) {
|
||||
@ -135,7 +106,7 @@ export default function useChatListEntry({
|
||||
{renderTextWithEntities({
|
||||
text: draft.text?.text || '',
|
||||
entities: draft.text?.entities,
|
||||
isSimple: true,
|
||||
asPreview: true,
|
||||
withTranslucentThumbs: true,
|
||||
})}
|
||||
</p>
|
||||
@ -146,34 +117,6 @@ export default function useChatListEntry({
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (isExpiredMessage(lastMessage)) {
|
||||
return (
|
||||
<p className="last-message shared-canvas-container" dir={oldLang.isRtl ? 'auto' : 'ltr'}>
|
||||
{getExpiredMessageDescription(oldLang, lastMessage)}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
if (isAction) {
|
||||
return (
|
||||
<p className="last-message shared-canvas-container" dir={oldLang.isRtl ? 'auto' : 'ltr'}>
|
||||
{renderActionMessageText(
|
||||
oldLang,
|
||||
lastMessage,
|
||||
lastMessageSender && !isApiPeerChat(lastMessageSender) ? lastMessageSender : undefined,
|
||||
lastMessageSender && isApiPeerChat(lastMessageSender) ? lastMessageSender : chat,
|
||||
actionTargetUsers,
|
||||
actionTargetMessage,
|
||||
actionTargetChatId,
|
||||
lastMessageTopic,
|
||||
{ isEmbedded: true },
|
||||
undefined,
|
||||
undefined,
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
const senderName = getMessageSenderName(oldLang, chatId, lastMessageSender);
|
||||
|
||||
return (
|
||||
@ -190,9 +133,8 @@ export default function useChatListEntry({
|
||||
</p>
|
||||
);
|
||||
}, [
|
||||
actionTargetChatId, actionTargetMessage, actionTargetUsers, chat, chatId, draft, isAction,
|
||||
isRoundVideo, isTopic, oldLang, lastMessage, lastMessageSender, lastMessageTopic, mediaBlobUrl, mediaThumbnail,
|
||||
observeIntersection, typingStatus, isSavedDialog, isPreview,
|
||||
chat, chatId, draft, isRoundVideo, isTopic, oldLang, lastMessage, lastMessageSender, lastMessageTopic,
|
||||
mediaBlobUrl, mediaThumbnail, observeIntersection, typingStatus, isSavedDialog, isPreview,
|
||||
]);
|
||||
|
||||
function renderSubtitle() {
|
||||
|
||||
@ -20,6 +20,7 @@ import { formatCurrency } from '../../../util/formatCurrency';
|
||||
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 usePreviousDeprecated from '../../../hooks/usePreviousDeprecated';
|
||||
@ -125,7 +126,8 @@ const PremiumFeatureModal: FC<OwnProps> = ({
|
||||
onBack,
|
||||
onClickSubscribe,
|
||||
}) => {
|
||||
const lang = useOldLang();
|
||||
const oldLang = useOldLang();
|
||||
const lang = useLang();
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const scrollContainerRef = useRef<HTMLDivElement>(null);
|
||||
const [currentSlideIndex, setCurrentSlideIndex] = useState(PREMIUM_FEATURE_SECTIONS.indexOf(initialSection));
|
||||
@ -148,7 +150,9 @@ const PremiumFeatureModal: FC<OwnProps> = ({
|
||||
const { amount, months, currency } = subscriptionOption;
|
||||
const perMonthPrice = Math.floor(amount / months);
|
||||
|
||||
return isPremium ? lang('OK') : lang('SubscribeToPremium', formatCurrency(perMonthPrice, currency, lang.code));
|
||||
return isPremium
|
||||
? lang('OK')
|
||||
: lang('SubscribeToPremium', { price: formatCurrency(lang, perMonthPrice, currency) }, { withNodes: true });
|
||||
}, [isPremium, lang, subscriptionOption]);
|
||||
|
||||
const handleClick = useLastCallback(() => {
|
||||
@ -220,7 +224,7 @@ const PremiumFeatureModal: FC<OwnProps> = ({
|
||||
className={buildClassName(styles.backButton, currentSlideIndex !== 0 && styles.whiteBackButton)}
|
||||
color={currentSlideIndex === 0 ? 'translucent' : 'translucent-white'}
|
||||
onClick={onBack}
|
||||
ariaLabel={lang('Back')}
|
||||
ariaLabel={oldLang('Back')}
|
||||
>
|
||||
<Icon name="arrow-left" />
|
||||
</Button>
|
||||
@ -234,7 +238,7 @@ const PremiumFeatureModal: FC<OwnProps> = ({
|
||||
return (
|
||||
<div className={buildClassName(styles.slide, styles.limits)}>
|
||||
<h2 className={buildClassName(styles.header, isScrolledToTop && styles.noHeaderBorder)}>
|
||||
{lang(PREMIUM_FEATURE_TITLES.double_limits)}
|
||||
{oldLang(PREMIUM_FEATURE_TITLES.double_limits)}
|
||||
</h2>
|
||||
<div className={buildClassName(styles.limitsContent, 'custom-scroll')} onScroll={handleLimitsScroll}>
|
||||
{PREMIUM_LIMITS_ORDER.map((limit, i) => {
|
||||
@ -242,8 +246,8 @@ const PremiumFeatureModal: FC<OwnProps> = ({
|
||||
const premiumLimit = limits?.[limit][1].toString();
|
||||
return (
|
||||
<PremiumLimitPreview
|
||||
title={lang(LIMITS_TITLES[limit])}
|
||||
description={lang(LIMITS_DESCRIPTIONS[limit], premiumLimit)}
|
||||
title={oldLang(LIMITS_TITLES[limit])}
|
||||
description={oldLang(LIMITS_DESCRIPTIONS[limit], premiumLimit)}
|
||||
leftValue={defaultLimit}
|
||||
rightValue={premiumLimit}
|
||||
colorStepProgress={i / (PREMIUM_LIMITS_ORDER.length - 1)}
|
||||
@ -262,10 +266,10 @@ const PremiumFeatureModal: FC<OwnProps> = ({
|
||||
<PremiumFeaturePreviewStickers isActive={currentSlideIndex === index} />
|
||||
</div>
|
||||
<h1 className={styles.title}>
|
||||
{lang(PREMIUM_FEATURE_TITLES.premium_stickers)}
|
||||
{oldLang(PREMIUM_FEATURE_TITLES.premium_stickers)}
|
||||
</h1>
|
||||
<div className={styles.description}>
|
||||
{renderText(lang(PREMIUM_FEATURE_DESCRIPTIONS.premium_stickers), ['br'])}
|
||||
{renderText(oldLang(PREMIUM_FEATURE_DESCRIPTIONS.premium_stickers), ['br'])}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -294,10 +298,10 @@ const PremiumFeatureModal: FC<OwnProps> = ({
|
||||
/>
|
||||
</div>
|
||||
<h1 className={styles.title}>
|
||||
{lang(PREMIUM_FEATURE_TITLES[promo.videoSections[i]!])}
|
||||
{oldLang(PREMIUM_FEATURE_TITLES[promo.videoSections[i]!])}
|
||||
</h1>
|
||||
<div className={styles.description}>
|
||||
{renderText(lang(PREMIUM_FEATURE_DESCRIPTIONS[promo.videoSections[i]!]), ['br'])}
|
||||
{renderText(oldLang(PREMIUM_FEATURE_DESCRIPTIONS[promo.videoSections[i]!]), ['br'])}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -22,6 +22,7 @@ import { REM } from '../../common/helpers/mediaDimensions';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities';
|
||||
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
import useSyncEffect from '../../../hooks/useSyncEffect';
|
||||
@ -137,7 +138,8 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
|
||||
closePremiumModal, openInvoice, requestConfetti, openTelegramLink, loadStickers, openStickerSet,
|
||||
} = getActions();
|
||||
|
||||
const lang = useOldLang();
|
||||
const oldLang = useOldLang();
|
||||
const lang = useLang();
|
||||
const [isHeaderHidden, setHeaderHidden] = useState(true);
|
||||
const [currentSection, setCurrentSection] = useState<ApiPremiumSection | undefined>(initialSection);
|
||||
const [selectedSubscriptionOption, setSubscriptionOption] = useState<ApiPremiumSubscriptionOption>();
|
||||
@ -262,11 +264,11 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
|
||||
const { amount, months, currency } = selectedSubscriptionOption;
|
||||
const perMonthPrice = Math.floor(amount / months);
|
||||
return formatCurrency(
|
||||
lang,
|
||||
perMonthPrice,
|
||||
currency,
|
||||
lang.code,
|
||||
);
|
||||
}, [selectedSubscriptionOption, lang.code]);
|
||||
}, [selectedSubscriptionOption, lang]);
|
||||
|
||||
if (!promo || (fromUserStatusEmoji && !fromUserStatusSet)) return undefined;
|
||||
|
||||
@ -274,14 +276,14 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
|
||||
if (isGift) {
|
||||
return renderText(
|
||||
fromUser?.id === currentUserId
|
||||
? lang('TelegramPremiumUserGiftedPremiumOutboundDialogTitle', [getUserFullName(toUser), monthsAmount])
|
||||
: lang('TelegramPremiumUserGiftedPremiumDialogTitle', [getUserFullName(fromUser), monthsAmount]),
|
||||
? oldLang('TelegramPremiumUserGiftedPremiumOutboundDialogTitle', [getUserFullName(toUser), monthsAmount])
|
||||
: oldLang('TelegramPremiumUserGiftedPremiumDialogTitle', [getUserFullName(fromUser), monthsAmount]),
|
||||
['simple_markdown', 'emoji'],
|
||||
);
|
||||
}
|
||||
|
||||
if (fromUserStatusSet && fromUser) {
|
||||
const template = lang('lng_premium_emoji_status_title').replace('{user}', getUserFullName(fromUser)!);
|
||||
const template = oldLang('lng_premium_emoji_status_title').replace('{user}', getUserFullName(fromUser)!);
|
||||
const [first, second] = template.split('{link}');
|
||||
|
||||
const emoji = fromUserStatusSet.thumbCustomEmojiId ? (
|
||||
@ -297,8 +299,8 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
|
||||
|
||||
return renderText(
|
||||
fromUser
|
||||
? lang('TelegramPremiumUserDialogTitle', getUserFullName(fromUser))
|
||||
: lang(isPremium ? 'TelegramPremiumSubscribedTitle' : 'TelegramPremium'),
|
||||
? oldLang('TelegramPremiumUserDialogTitle', getUserFullName(fromUser))
|
||||
: oldLang(isPremium ? 'TelegramPremiumSubscribedTitle' : 'TelegramPremium'),
|
||||
['simple_markdown', 'emoji'],
|
||||
);
|
||||
}
|
||||
@ -306,17 +308,17 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
|
||||
function getHeaderDescription() {
|
||||
if (isGift) {
|
||||
return fromUser?.id === currentUserId
|
||||
? lang('TelegramPremiumUserGiftedPremiumOutboundDialogSubtitle', getUserFullName(toUser))
|
||||
: lang('TelegramPremiumUserGiftedPremiumDialogSubtitle');
|
||||
? oldLang('TelegramPremiumUserGiftedPremiumOutboundDialogSubtitle', getUserFullName(toUser))
|
||||
: oldLang('TelegramPremiumUserGiftedPremiumDialogSubtitle');
|
||||
}
|
||||
|
||||
if (fromUserStatusSet) {
|
||||
return lang('TelegramPremiumUserStatusDialogSubtitle');
|
||||
return oldLang('TelegramPremiumUserStatusDialogSubtitle');
|
||||
}
|
||||
|
||||
return fromUser
|
||||
? lang('TelegramPremiumUserDialogSubtitle')
|
||||
: lang(isPremium ? 'TelegramPremiumSubscribedSubtitle' : 'TelegramPremiumSubtitle');
|
||||
? oldLang('TelegramPremiumUserDialogSubtitle')
|
||||
: oldLang(isPremium ? 'TelegramPremiumSubscribedSubtitle' : 'TelegramPremiumSubtitle');
|
||||
}
|
||||
|
||||
function renderFooterText() {
|
||||
@ -325,7 +327,7 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={styles.footerText} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
<div className={styles.footerText} dir={oldLang.isRtl ? 'rtl' : undefined}>
|
||||
{renderTextWithEntities({
|
||||
text: promo.statusText,
|
||||
entities: promo.statusEntities,
|
||||
@ -369,7 +371,7 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
|
||||
color="translucent"
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => closePremiumModal()}
|
||||
ariaLabel={lang('Close')}
|
||||
ariaLabel={oldLang('Close')}
|
||||
>
|
||||
<Icon name="close" />
|
||||
</Button>
|
||||
@ -393,7 +395,7 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
|
||||
{!isPremium && !isGift && renderSubscriptionOptions()}
|
||||
<div className={buildClassName(styles.header, isHeaderHidden && styles.hiddenHeader)}>
|
||||
<h2 className={styles.premiumHeaderText}>
|
||||
{lang('TelegramPremium')}
|
||||
{oldLang('TelegramPremium')}
|
||||
</h2>
|
||||
</div>
|
||||
<div className={buildClassName(styles.list, isPremium && styles.noButton)}>
|
||||
@ -401,11 +403,11 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
|
||||
return (
|
||||
<PremiumFeatureItem
|
||||
key={section}
|
||||
title={lang(PREMIUM_FEATURE_TITLES[section])}
|
||||
title={oldLang(PREMIUM_FEATURE_TITLES[section])}
|
||||
text={section === 'double_limits'
|
||||
? lang(PREMIUM_FEATURE_DESCRIPTIONS[section],
|
||||
? oldLang(PREMIUM_FEATURE_DESCRIPTIONS[section],
|
||||
[limitChannels, limitFolders, limitPins, limitLinks, LIMIT_ACCOUNTS])
|
||||
: lang(PREMIUM_FEATURE_DESCRIPTIONS[section])}
|
||||
: oldLang(PREMIUM_FEATURE_DESCRIPTIONS[section])}
|
||||
icon={PREMIUM_FEATURE_COLOR_ICONS[section]}
|
||||
index={index}
|
||||
count={filteredSections.length}
|
||||
@ -416,13 +418,13 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
|
||||
})}
|
||||
<div
|
||||
className={buildClassName(styles.footerText, styles.primaryFooterText)}
|
||||
dir={lang.isRtl ? 'rtl' : undefined}
|
||||
dir={oldLang.isRtl ? 'rtl' : undefined}
|
||||
>
|
||||
<p>
|
||||
{renderText(lang('AboutPremiumDescription'), ['simple_markdown'])}
|
||||
{renderText(oldLang('AboutPremiumDescription'), ['simple_markdown'])}
|
||||
</p>
|
||||
<p>
|
||||
{renderText(lang('AboutPremiumDescription2'), ['simple_markdown'])}
|
||||
{renderText(oldLang('AboutPremiumDescription2'), ['simple_markdown'])}
|
||||
</p>
|
||||
</div>
|
||||
{renderFooterText()}
|
||||
@ -430,7 +432,7 @@ const PremiumMainModal: FC<OwnProps & StateProps> = ({
|
||||
{!isPremium && selectedSubscriptionOption && (
|
||||
<div className={styles.footer}>
|
||||
<Button className={styles.button} isShiny withPremiumGradient onClick={handleClick}>
|
||||
{lang('SubscribeToPremium', subscribeButtonText)}
|
||||
{oldLang('SubscribeToPremium', subscribeButtonText)}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@ -5,7 +5,7 @@ import React, { memo, useCallback, useMemo } from '../../../lib/teact/teact';
|
||||
import type { ApiPremiumGiftCodeOption, ApiPremiumGiftOption } from '../../../api/types';
|
||||
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { formatCurrency } from '../../../util/formatCurrency';
|
||||
import { formatCurrencyAsString } from '../../../util/formatCurrency';
|
||||
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
@ -24,7 +24,7 @@ const PremiumSubscriptionOption: FC<OwnProps> = ({
|
||||
option, checked, fullMonthlyAmount,
|
||||
onChange, className, isGiveaway,
|
||||
}) => {
|
||||
const lang = useOldLang();
|
||||
const oldLang = useOldLang();
|
||||
|
||||
const {
|
||||
months, amount, currency,
|
||||
@ -52,7 +52,7 @@ const PremiumSubscriptionOption: FC<OwnProps> = ({
|
||||
(checked && !isGiveaway) && styles.active,
|
||||
className,
|
||||
)}
|
||||
dir={lang.isRtl ? 'rtl' : undefined}
|
||||
dir={oldLang.isRtl ? 'rtl' : undefined}
|
||||
>
|
||||
<input
|
||||
className={styles.input}
|
||||
@ -67,18 +67,18 @@ const PremiumSubscriptionOption: FC<OwnProps> = ({
|
||||
{Boolean(discount) && (
|
||||
<span
|
||||
className={buildClassName(styles.giveawayDiscount, styles.discount)}
|
||||
title={lang('GiftDiscount')}
|
||||
title={oldLang('GiftDiscount')}
|
||||
> −{discount}%
|
||||
</span>
|
||||
)}
|
||||
{lang('Months', months)}
|
||||
{oldLang('Months', months)}
|
||||
</div>
|
||||
<div className={styles.perMonth}>
|
||||
{(isGiveaway || isUserCountPlural) ? `${formatCurrency(amount, currency, lang.code)} x ${users!}`
|
||||
: lang('PricePerMonth', formatCurrency(perMonth, currency, lang.code))}
|
||||
{(isGiveaway || isUserCountPlural) ? `${formatCurrencyAsString(amount, currency, oldLang.code)} x ${users!}`
|
||||
: oldLang('PricePerMonth', formatCurrencyAsString(perMonth, currency, oldLang.code))}
|
||||
</div>
|
||||
<div className={styles.amount}>
|
||||
{formatCurrency(amount, currency, lang.code)}
|
||||
{formatCurrencyAsString(amount, currency, oldLang.code)}
|
||||
</div>
|
||||
</div>
|
||||
</label>
|
||||
|
||||
@ -99,9 +99,9 @@ export default function getViewableMedia(params?: MediaViewerItem): ViewableMedi
|
||||
action, document, photo, video, webPage, paidMedia,
|
||||
} = getMessageContent(params.message);
|
||||
|
||||
if (action?.photo) {
|
||||
if (action?.type === 'chatEditPhoto' || action?.type === 'suggestProfilePhoto') {
|
||||
return {
|
||||
media: action.photo,
|
||||
media: action.photo!,
|
||||
isSingle: true,
|
||||
};
|
||||
}
|
||||
|
||||
@ -330,6 +330,7 @@ function getNodes(origin: MediaViewerOrigin, message?: ApiMessage, index?: numbe
|
||||
mediaSelector = '.avatar-media';
|
||||
break;
|
||||
|
||||
case MediaViewerOrigin.ChannelAvatar:
|
||||
case MediaViewerOrigin.SuggestedAvatar:
|
||||
containerSelector = `.Transition_slide-active > .MessageList #${getMessageHtmlId(message!.id, index)}`;
|
||||
mediaSelector = '.Avatar img';
|
||||
@ -370,6 +371,7 @@ function applyShape(ghost: HTMLDivElement, origin: MediaViewerOrigin) {
|
||||
case MediaViewerOrigin.ScheduledInline:
|
||||
case MediaViewerOrigin.StarsTransaction:
|
||||
case MediaViewerOrigin.PreviewMedia:
|
||||
case MediaViewerOrigin.ChannelAvatar:
|
||||
ghost.classList.add('rounded-corners');
|
||||
break;
|
||||
|
||||
|
||||
@ -1,789 +0,0 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useCallback, useEffect, useMemo, useRef, useUnmountCleanup,
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions, getGlobal, withGlobal } from '../../global';
|
||||
|
||||
import type {
|
||||
ApiChat, ApiMessage, ApiMessageActionStarGift, ApiSticker, ApiTopic, ApiUser,
|
||||
} from '../../api/types';
|
||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
import type { FocusDirection, MessageListType, ThreadId } from '../../types';
|
||||
import type { OnIntersectPinnedMessage } from './hooks/usePinnedMessage';
|
||||
|
||||
import {
|
||||
getChatTitle, getMessageHtmlId, getPeerTitle, isJoinedChannelMessage,
|
||||
} from '../../global/helpers';
|
||||
import { getMessageReplyInfo } from '../../global/helpers/replies';
|
||||
import {
|
||||
selectCanPlayAnimatedEmojis,
|
||||
selectChat,
|
||||
selectChatMessage,
|
||||
selectGiftStickerForDuration,
|
||||
selectGiftStickerForStars,
|
||||
selectIsCurrentUserPremium,
|
||||
selectIsMessageFocused,
|
||||
selectPeer,
|
||||
selectTabState,
|
||||
selectTheme,
|
||||
selectTopicFromMessage,
|
||||
selectUser,
|
||||
} from '../../global/selectors';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { formatInteger, formatIntegerCompact } from '../../util/textFormat';
|
||||
import { getGiftAttributes, getStickerFromGift } from '../common/helpers/gifts';
|
||||
import { renderActionMessageText } from '../common/helpers/renderActionMessageText';
|
||||
import renderText from '../common/helpers/renderText';
|
||||
import { renderTextWithEntities } from '../common/helpers/renderTextWithEntities';
|
||||
import { preventMessageInputBlur } from './helpers/preventMessageInputBlur';
|
||||
|
||||
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
||||
import useEnsureMessage from '../../hooks/useEnsureMessage';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import { useIsIntersecting, useOnIntersect } from '../../hooks/useIntersectionObserver';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
import useMessageResizeObserver from '../../hooks/useResizeMessageObserver';
|
||||
import useShowTransitionDeprecated from '../../hooks/useShowTransitionDeprecated';
|
||||
import useFocusMessage from './message/hooks/useFocusMessage';
|
||||
|
||||
import AnimatedIconFromSticker from '../common/AnimatedIconFromSticker';
|
||||
import Avatar from '../common/Avatar';
|
||||
import GiftRibbon from '../common/gift/GiftRibbon';
|
||||
import RadialPatternBackground from '../common/profile/RadialPatternBackground';
|
||||
import Sparkles from '../common/Sparkles';
|
||||
import ActionMessageSuggestedAvatar from './ActionMessageSuggestedAvatar';
|
||||
import ActionMessageUpdatedAvatar from './ActionMessageUpdatedAvatar';
|
||||
import ContextMenuContainer from './message/ContextMenuContainer.async';
|
||||
import Reactions from './message/reactions/Reactions';
|
||||
import SimilarChannels from './message/SimilarChannels';
|
||||
|
||||
type OwnProps = {
|
||||
message: ApiMessage;
|
||||
threadId?: ThreadId;
|
||||
messageListType?: MessageListType;
|
||||
observeIntersectionForReading?: ObserveFn;
|
||||
observeIntersectionForLoading?: ObserveFn;
|
||||
observeIntersectionForPlaying?: ObserveFn;
|
||||
isEmbedded?: boolean;
|
||||
appearanceOrder?: number;
|
||||
isJustAdded?: boolean;
|
||||
isLastInList?: boolean;
|
||||
isInsideTopic?: boolean;
|
||||
memoFirstUnreadIdRef?: { current: number | undefined };
|
||||
onIntersectPinnedMessage?: OnIntersectPinnedMessage;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
senderUser?: ApiUser;
|
||||
senderChat?: ApiChat;
|
||||
targetUserIds?: string[];
|
||||
targetMessage?: ApiMessage;
|
||||
targetChatId?: string;
|
||||
targetChat?: ApiChat;
|
||||
isFocused: boolean;
|
||||
topic?: ApiTopic;
|
||||
focusDirection?: FocusDirection;
|
||||
noFocusHighlight?: boolean;
|
||||
premiumGiftSticker?: ApiSticker;
|
||||
starsGiftSticker?: ApiSticker;
|
||||
canPlayAnimatedEmojis?: boolean;
|
||||
patternColor?: string;
|
||||
currentUserId?: string;
|
||||
isCurrentUserPremium?: boolean;
|
||||
};
|
||||
|
||||
const APPEARANCE_DELAY = 10;
|
||||
const STAR_GIFT_STICKER_SIZE = 120;
|
||||
|
||||
const ActionMessage: FC<OwnProps & StateProps> = ({
|
||||
message,
|
||||
threadId,
|
||||
isEmbedded,
|
||||
appearanceOrder = 0,
|
||||
isJustAdded,
|
||||
isLastInList,
|
||||
senderUser,
|
||||
senderChat,
|
||||
targetUserIds,
|
||||
targetMessage,
|
||||
targetChatId,
|
||||
targetChat,
|
||||
isFocused,
|
||||
focusDirection,
|
||||
noFocusHighlight,
|
||||
premiumGiftSticker,
|
||||
starsGiftSticker,
|
||||
isInsideTopic,
|
||||
topic,
|
||||
memoFirstUnreadIdRef,
|
||||
canPlayAnimatedEmojis,
|
||||
patternColor,
|
||||
observeIntersectionForReading,
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
onIntersectPinnedMessage,
|
||||
currentUserId,
|
||||
isCurrentUserPremium,
|
||||
}) => {
|
||||
const {
|
||||
openPremiumModal,
|
||||
requestConfetti,
|
||||
checkGiftCode,
|
||||
getReceipt,
|
||||
openGiftInfoModalFromMessage,
|
||||
openPrizeStarsTransactionFromGiveaway,
|
||||
} = getActions();
|
||||
|
||||
const oldLang = useOldLang();
|
||||
const lang = useLang();
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
useOnIntersect(ref, observeIntersectionForReading);
|
||||
useEnsureMessage(
|
||||
message.chatId,
|
||||
message.replyInfo?.type === 'message' ? message.replyInfo.replyToMsgId : undefined,
|
||||
targetMessage,
|
||||
);
|
||||
useFocusMessage({
|
||||
elementRef: ref,
|
||||
chatId: message.chatId,
|
||||
isFocused,
|
||||
focusDirection,
|
||||
noFocusHighlight,
|
||||
isJustAdded,
|
||||
});
|
||||
|
||||
useUnmountCleanup(() => {
|
||||
if (message.isPinned) {
|
||||
onIntersectPinnedMessage?.({ viewportPinnedIdsToRemove: [message.id] });
|
||||
}
|
||||
});
|
||||
|
||||
const noAppearanceAnimation = appearanceOrder <= 0;
|
||||
const [isShown, markShown] = useFlag(noAppearanceAnimation);
|
||||
const isPremiumGift = message.content.action?.type === 'giftPremium';
|
||||
const isGiftCode = message.content.action?.type === 'giftCode';
|
||||
const isSuggestedAvatar = message.content.action?.type === 'suggestProfilePhoto' && message.content.action!.photo;
|
||||
const isUpdatedAvatar = message.content.action?.type === 'updateProfilePhoto' && message.content.action!.photo;
|
||||
const isJoinedMessage = isJoinedChannelMessage(message);
|
||||
const isStarsGift = message.content.action?.type === 'giftStars';
|
||||
const isStarGift = message.content.action?.type === 'starGift';
|
||||
const isStarGiftUnique = message.content.action?.type === 'starGiftUnique';
|
||||
const isPrizeStars = message.content.action?.type === 'prizeStars';
|
||||
|
||||
const withServiceReactions = Boolean(message.areReactionsPossible && message?.reactions);
|
||||
|
||||
useMessageResizeObserver(ref, isLastInList);
|
||||
|
||||
useEffect(() => {
|
||||
if (noAppearanceAnimation) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(markShown, appearanceOrder * APPEARANCE_DELAY);
|
||||
}, [appearanceOrder, markShown, noAppearanceAnimation]);
|
||||
|
||||
const isVisible = useIsIntersecting(ref, observeIntersectionForPlaying);
|
||||
|
||||
const shouldShowConfettiRef = useRef((() => {
|
||||
const isUnread = memoFirstUnreadIdRef?.current && message.id >= memoFirstUnreadIdRef.current;
|
||||
return isPremiumGift && !message.isOutgoing && isUnread;
|
||||
})());
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisible && shouldShowConfettiRef.current) {
|
||||
shouldShowConfettiRef.current = false;
|
||||
requestConfetti({ withStars: true });
|
||||
}
|
||||
}, [isVisible, requestConfetti]);
|
||||
|
||||
const { transitionClassNames } = useShowTransitionDeprecated(isShown, undefined, noAppearanceAnimation, false);
|
||||
|
||||
// No need for expensive global updates on users and chats, so we avoid them
|
||||
const usersById = getGlobal().users.byId;
|
||||
const targetUsers = useMemo(() => {
|
||||
return targetUserIds
|
||||
? targetUserIds.map((userId) => usersById?.[userId]).filter(Boolean)
|
||||
: undefined;
|
||||
}, [targetUserIds, usersById]);
|
||||
|
||||
const renderContent = useCallback(() => {
|
||||
return renderActionMessageText(
|
||||
oldLang,
|
||||
message,
|
||||
senderUser,
|
||||
senderChat,
|
||||
targetUsers,
|
||||
targetMessage,
|
||||
targetChatId,
|
||||
topic,
|
||||
{ isEmbedded },
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
);
|
||||
}, [
|
||||
isEmbedded, message, observeIntersectionForLoading, observeIntersectionForPlaying, oldLang,
|
||||
senderChat, senderUser, targetChatId, targetMessage, targetUsers, topic,
|
||||
]);
|
||||
|
||||
const {
|
||||
isContextMenuOpen, contextMenuAnchor,
|
||||
handleBeforeContextMenu, handleContextMenu,
|
||||
handleContextMenuClose, handleContextMenuHide,
|
||||
} = useContextMenuHandlers(ref);
|
||||
const isContextMenuShown = contextMenuAnchor !== undefined;
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
preventMessageInputBlur(e);
|
||||
handleBeforeContextMenu(e);
|
||||
};
|
||||
|
||||
const handleStarGiftClick = () => {
|
||||
const starGift = message.content.action?.starGift;
|
||||
if (!starGift) return;
|
||||
|
||||
openGiftInfoModalFromMessage({
|
||||
chatId: message.chatId,
|
||||
messageId: message.id,
|
||||
});
|
||||
};
|
||||
|
||||
const handlePremiumGiftClick = () => {
|
||||
openPremiumModal({
|
||||
isGift: true,
|
||||
fromUserId: senderUser?.id,
|
||||
toUserId: targetUserIds?.[0],
|
||||
monthsAmount: message.content.action?.months || 0,
|
||||
});
|
||||
};
|
||||
|
||||
const handlePrizeStarsClick = () => {
|
||||
openPrizeStarsTransactionFromGiveaway({
|
||||
chatId: message.chatId,
|
||||
messageId: message.id,
|
||||
});
|
||||
};
|
||||
|
||||
const handleGiftCodeClick = () => {
|
||||
const slug = message.content.action?.slug;
|
||||
if (!slug) return;
|
||||
checkGiftCode({ slug, message: { chatId: message.chatId, messageId: message.id } });
|
||||
};
|
||||
|
||||
const handleClick = () => {
|
||||
if (message.content.action?.type === 'receipt') {
|
||||
getReceipt({
|
||||
chatId: message.chatId,
|
||||
messageId: message.id,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// TODO Refactoring for action rendering
|
||||
const shouldSkipRender = isInsideTopic && message.content.action?.text === 'TopicWasCreatedAction';
|
||||
if (shouldSkipRender) {
|
||||
return <span ref={ref} />;
|
||||
}
|
||||
|
||||
if (isEmbedded) {
|
||||
return <span ref={ref} className="embedded-action-message">{renderContent()}</span>;
|
||||
}
|
||||
|
||||
function renderGift() {
|
||||
const giftMessage = message.content.action?.message;
|
||||
return (
|
||||
<span
|
||||
className="action-message-gift"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
onClick={handlePremiumGiftClick}
|
||||
>
|
||||
<AnimatedIconFromSticker
|
||||
key={message.id}
|
||||
sticker={premiumGiftSticker}
|
||||
play={canPlayAnimatedEmojis}
|
||||
noLoop
|
||||
nonInteractive
|
||||
/>
|
||||
<strong>{oldLang('ActionGiftPremiumTitle')}</strong>
|
||||
<span>
|
||||
{oldLang('ActionGiftPremiumSubtitle', oldLang('Months', message.content.action?.months, 'i'))}
|
||||
</span>
|
||||
{giftMessage && (
|
||||
<div className="action-message-gift-subtitle">
|
||||
{renderTextWithEntities({ text: giftMessage.text, entities: giftMessage.entities })}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<span className="action-message-button">
|
||||
<Sparkles preset="button" />
|
||||
{oldLang('ActionGiftPremiumView')}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function renderGiftCode() {
|
||||
const isFromGiveaway = message.content.action?.isGiveaway;
|
||||
const isUnclaimed = message.content.action?.isUnclaimed;
|
||||
const giftMessage = message.content.action?.message;
|
||||
return (
|
||||
<span
|
||||
className="action-message-gift action-message-centered"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
onClick={handleGiftCodeClick}
|
||||
>
|
||||
<AnimatedIconFromSticker
|
||||
key={message.id}
|
||||
sticker={premiumGiftSticker}
|
||||
play={canPlayAnimatedEmojis}
|
||||
noLoop
|
||||
nonInteractive
|
||||
/>
|
||||
<strong>
|
||||
{oldLang(isUnclaimed ? 'BoostingUnclaimedPrize' : 'BoostingCongratulations')}
|
||||
</strong>
|
||||
<span className="action-message-subtitle">
|
||||
{targetChat && renderText(
|
||||
oldLang(
|
||||
isFromGiveaway ? 'BoostingReceivedGiftFrom' : isUnclaimed
|
||||
? 'BoostingReceivedPrizeFrom' : 'BoostingYouHaveUnclaimedPrize',
|
||||
getChatTitle(oldLang, targetChat),
|
||||
),
|
||||
['simple_markdown'],
|
||||
)}
|
||||
</span>
|
||||
<span className="action-message-subtitle">
|
||||
{renderText(oldLang(
|
||||
'BoostingUnclaimedPrizeDuration',
|
||||
oldLang('Months', message.content.action?.months, 'i'),
|
||||
), ['simple_markdown'])}
|
||||
</span>
|
||||
|
||||
{giftMessage && (
|
||||
<div className="action-message-gift-subtitle">
|
||||
{renderTextWithEntities({ text: giftMessage.text, entities: giftMessage.entities })}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<span className="action-message-button">
|
||||
{oldLang('BoostingReceivedGiftOpenBtn')}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function renderStarsGift() {
|
||||
return (
|
||||
<span
|
||||
className="action-message-gift action-message-centered"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
onClick={handleStarGiftClick}
|
||||
>
|
||||
<AnimatedIconFromSticker
|
||||
key={message.id}
|
||||
sticker={starsGiftSticker}
|
||||
play={canPlayAnimatedEmojis}
|
||||
noLoop
|
||||
nonInteractive
|
||||
/>
|
||||
<div className="action-message-stars-balance">
|
||||
{formatInteger(message.content.action!.stars!)}
|
||||
<strong>{oldLang('Stars')}</strong>
|
||||
</div>
|
||||
<span className="action-message-stars-subtitle">
|
||||
{renderText(
|
||||
oldLang(!message.isOutgoing
|
||||
? 'ActionGiftStarsSubtitleYou' : 'ActionGiftStarsSubtitle', getChatTitle(oldLang, targetChat!)),
|
||||
['simple_markdown'],
|
||||
)}
|
||||
</span>
|
||||
<span className="action-message-button">
|
||||
<Sparkles preset="button" />
|
||||
{oldLang('ActionGiftPremiumView')}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function renderStarGiftUserCaption() {
|
||||
const starGift = message.content.action?.starGift;
|
||||
if (!starGift) return undefined;
|
||||
const { fromId, peerId } = starGift;
|
||||
|
||||
const fromPeer = fromId ? selectPeer(getGlobal(), fromId) : undefined;
|
||||
const targetPeer = peerId
|
||||
? selectPeer(getGlobal(), peerId)
|
||||
: starGift.type === 'starGiftUnique' && !message.isOutgoing
|
||||
? targetChat : undefined;
|
||||
|
||||
if (targetPeer && targetPeer.id !== currentUserId) {
|
||||
return (
|
||||
<div className="action-message-user-caption">
|
||||
<span> {lang('GiftTo')} </span>
|
||||
{starGift.type === 'starGift' && (
|
||||
<Avatar className="action-message-user-avatar" size="micro" peer={targetPeer} />
|
||||
)}
|
||||
<span> {getPeerTitle(lang, targetPeer)} </span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="action-message-user-caption">
|
||||
<span> {lang('GiftFrom')} </span>
|
||||
{starGift.type === 'starGift' && (
|
||||
<Avatar className="action-message-user-avatar" size="micro" peer={fromPeer || senderUser} />
|
||||
)}
|
||||
<span> {getPeerTitle(lang, fromPeer || senderUser!)} </span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderStarGiftUserDescription() {
|
||||
const starGift = message.content.action?.starGift as ApiMessageActionStarGift;
|
||||
const targetChatTitle = targetChat && getPeerTitle(lang, targetChat);
|
||||
const starGiftMessage = starGift?.message;
|
||||
if (!starGift) return undefined;
|
||||
|
||||
if (starGiftMessage) {
|
||||
return renderTextWithEntities({ text: starGiftMessage.text, entities: starGiftMessage.entities });
|
||||
}
|
||||
const amountToConvert = starGift?.starsToConvert;
|
||||
|
||||
if (starGift.isSaved) {
|
||||
return lang(starGift.savedId ? 'ActionStarGiftChannelDisplaying' : 'ActionStarGiftDisplaying');
|
||||
}
|
||||
|
||||
if (starGift.isUpgraded) {
|
||||
return lang('ActionStarGiftUpgraded');
|
||||
}
|
||||
|
||||
if (message.isOutgoing) {
|
||||
if (amountToConvert) {
|
||||
return lang('ActionStarGiftPeerOutDescription', {
|
||||
peer: targetChatTitle || 'Someone',
|
||||
count: amountToConvert,
|
||||
}, { withNodes: true, pluralValue: amountToConvert });
|
||||
}
|
||||
|
||||
if (starGift.canUpgrade) {
|
||||
return lang('ActionStarGiftPeerOutDescriptionUpgrade', {
|
||||
peer: targetChatTitle || 'Someone',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (starGift.isConverted) {
|
||||
return message.isOutgoing
|
||||
? lang('GiftInfoPeerDescriptionOutConverted', {
|
||||
amount: formatInteger(amountToConvert!),
|
||||
peer: targetChatTitle || 'Chat',
|
||||
}, {
|
||||
pluralValue: amountToConvert!,
|
||||
withNodes: true,
|
||||
withMarkdown: true,
|
||||
})
|
||||
: lang('GiftInfoDescriptionConverted', {
|
||||
amount: formatInteger(amountToConvert!),
|
||||
}, {
|
||||
pluralValue: amountToConvert!,
|
||||
withNodes: true,
|
||||
withMarkdown: true,
|
||||
});
|
||||
}
|
||||
|
||||
if (amountToConvert) {
|
||||
return lang('ActionStarGiftDescription2', {
|
||||
count: amountToConvert,
|
||||
}, { withNodes: true, pluralValue: amountToConvert });
|
||||
}
|
||||
|
||||
if (starGift.canUpgrade) {
|
||||
return lang('ActionStarGiftDescriptionUpgrade');
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function renderStarGift() {
|
||||
const starGift = message.content.action?.starGift as ApiMessageActionStarGift;
|
||||
if (!starGift || starGift.gift.type !== 'starGift') return undefined;
|
||||
|
||||
return (
|
||||
<span
|
||||
className="action-message-gift action-message-centered"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
onClick={handleStarGiftClick}
|
||||
>
|
||||
|
||||
<AnimatedIconFromSticker
|
||||
sticker={starGift.gift.sticker}
|
||||
play={canPlayAnimatedEmojis}
|
||||
noLoop
|
||||
nonInteractive
|
||||
size={STAR_GIFT_STICKER_SIZE}
|
||||
/>
|
||||
|
||||
{renderStarGiftUserCaption()}
|
||||
<div className="action-message-gift-subtitle">
|
||||
{renderStarGiftUserDescription()}
|
||||
</div>
|
||||
|
||||
<div className="action-message-button">
|
||||
<Sparkles preset="button" />
|
||||
{starGift.alreadyPaidUpgradeStars && (!message.isOutgoing || targetUsers?.[0]?.isSelf)
|
||||
? lang('ActionStarGiftUnpack') : oldLang('ActionGiftPremiumView')}
|
||||
</div>
|
||||
{starGift.gift.availabilityTotal && (
|
||||
<GiftRibbon
|
||||
color={patternColor || 'blue'}
|
||||
text={oldLang('Gift2Limited1OfRibbon', formatIntegerCompact(starGift.gift.availabilityTotal))}
|
||||
/>
|
||||
)}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function renderStarGiftUnique() {
|
||||
const starGift = message.content.action?.starGift;
|
||||
if (!starGift || starGift.gift.type !== 'starGiftUnique') return undefined;
|
||||
|
||||
const sticker = getStickerFromGift(starGift.gift)!;
|
||||
const attributes = getGiftAttributes(starGift.gift);
|
||||
const { backdrop, pattern, model } = attributes || {};
|
||||
|
||||
if (!backdrop || !pattern || !model) return undefined;
|
||||
|
||||
const backgroundColors = [backdrop.centerColor, backdrop.edgeColor];
|
||||
|
||||
const adaptedPatternColor = `${backdrop.patternColor.slice(0, 7)}55`;
|
||||
|
||||
return (
|
||||
<span
|
||||
className="action-message-gift action-message-centered action-message-unique"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
style={`--pattern-color: ${adaptedPatternColor}`}
|
||||
onClick={handleStarGiftClick}
|
||||
>
|
||||
<div className="action-message-unique-background-wrapper">
|
||||
<RadialPatternBackground
|
||||
className="action-message-unique-background"
|
||||
backgroundColors={backgroundColors}
|
||||
patternColor={backdrop.patternColor}
|
||||
patternIcon={pattern.sticker}
|
||||
clearBottomSector
|
||||
/>
|
||||
</div>
|
||||
<AnimatedIconFromSticker
|
||||
sticker={sticker}
|
||||
play={canPlayAnimatedEmojis}
|
||||
noLoop
|
||||
nonInteractive
|
||||
size={STAR_GIFT_STICKER_SIZE}
|
||||
/>
|
||||
{renderStarGiftUserCaption()}
|
||||
<div className="action-message-unique-title" style={`color: ${backdrop.textColor}`}>
|
||||
{starGift.gift.title} #{starGift.gift.number}
|
||||
</div>
|
||||
<div className="action-message-unique-properties" style={`color: ${backdrop.textColor}`}>
|
||||
<div className="action-message-unique-property">
|
||||
{oldLang('Gift2AttributeModel')}
|
||||
</div>
|
||||
<div className="action-message-unique-value">
|
||||
{model.name}
|
||||
</div>
|
||||
<div className="action-message-unique-property">
|
||||
{oldLang('Gift2AttributeBackdrop')}
|
||||
</div>
|
||||
<div className="action-message-unique-value">
|
||||
{backdrop.name}
|
||||
</div>
|
||||
<div className="action-message-unique-property">
|
||||
{oldLang('Gift2AttributeSymbol')}
|
||||
</div>
|
||||
<div className="action-message-unique-value">
|
||||
{pattern.name}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="action-message-button">
|
||||
<Sparkles preset="button" />
|
||||
{oldLang('Gift2UniqueView')}
|
||||
</div>
|
||||
<GiftRibbon
|
||||
color={adaptedPatternColor}
|
||||
text={oldLang('ActionStarGift')}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function renderPrizeStars() {
|
||||
const isUnclaimed = message.content.action?.isUnclaimed;
|
||||
|
||||
return (
|
||||
<span
|
||||
className="action-message-gift action-message-centered"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
onClick={handlePrizeStarsClick}
|
||||
>
|
||||
<AnimatedIconFromSticker
|
||||
key={message.id}
|
||||
sticker={starsGiftSticker}
|
||||
play={canPlayAnimatedEmojis}
|
||||
noLoop
|
||||
nonInteractive
|
||||
/>
|
||||
<strong>
|
||||
{oldLang(isUnclaimed ? 'BoostingUnclaimedPrize' : 'BoostingCongratulations')}
|
||||
</strong>
|
||||
<span className="action-message-subtitle">
|
||||
{targetChat && renderText(oldLang(isUnclaimed
|
||||
? 'BoostingReceivedPrizeFrom' : 'BoostingYouHaveUnclaimedPrize', getChatTitle(oldLang, targetChat)),
|
||||
['simple_markdown'])}
|
||||
</span>
|
||||
<span className="action-message-subtitle">
|
||||
{renderText(lang(
|
||||
'PrizeCredits2', {
|
||||
count: (
|
||||
<b>{formatInteger(message.content.action?.stars!)}</b>
|
||||
),
|
||||
}, {
|
||||
withNodes: true,
|
||||
pluralValue: message.content.action?.stars!,
|
||||
},
|
||||
), ['simple_markdown'])}
|
||||
</span>
|
||||
<span className="action-message-button">{
|
||||
oldLang('ActionGiftPremiumView')
|
||||
}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const className = buildClassName(
|
||||
'ActionMessage message-list-item',
|
||||
isFocused && !noFocusHighlight && 'focused',
|
||||
(isPremiumGift || isSuggestedAvatar || isUpdatedAvatar) && 'centered-action',
|
||||
isContextMenuShown && 'has-menu-open',
|
||||
isLastInList && 'last-in-list',
|
||||
transitionClassNames,
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
id={getMessageHtmlId(message.id)}
|
||||
className={className}
|
||||
data-message-id={message.id}
|
||||
data-is-pinned={message.isPinned || undefined}
|
||||
onMouseDown={handleMouseDown}
|
||||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
{!isSuggestedAvatar && !isGiftCode && !isJoinedMessage && !isUpdatedAvatar && (
|
||||
<span className="action-message-content" onClick={handleClick}>{renderContent()}</span>
|
||||
)}
|
||||
{isPremiumGift && renderGift()}
|
||||
{isGiftCode && renderGiftCode()}
|
||||
{isStarsGift && renderStarsGift()}
|
||||
{isStarGift && renderStarGift()}
|
||||
{isStarGiftUnique && renderStarGiftUnique()}
|
||||
{isPrizeStars && renderPrizeStars()}
|
||||
{isSuggestedAvatar && (
|
||||
<ActionMessageSuggestedAvatar message={message} renderContent={renderContent} />
|
||||
)}
|
||||
{isUpdatedAvatar && (
|
||||
<ActionMessageUpdatedAvatar message={message} renderContent={renderContent} />
|
||||
)}
|
||||
{isJoinedMessage && <SimilarChannels chatId={targetChatId!} />}
|
||||
{contextMenuAnchor && (
|
||||
<ContextMenuContainer
|
||||
isOpen={isContextMenuOpen}
|
||||
anchor={contextMenuAnchor}
|
||||
message={message}
|
||||
messageListType="thread"
|
||||
onClose={handleContextMenuClose}
|
||||
onCloseAnimationEnd={handleContextMenuHide}
|
||||
/>
|
||||
)}
|
||||
{withServiceReactions && (
|
||||
<Reactions
|
||||
isOutside
|
||||
message={message!}
|
||||
threadId={threadId}
|
||||
observeIntersection={observeIntersectionForPlaying}
|
||||
isCurrentUserPremium={isCurrentUserPremium}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { message, threadId }): StateProps => {
|
||||
const {
|
||||
chatId, senderId, content,
|
||||
} = message;
|
||||
|
||||
const { targetUserIds, targetChatId } = content.action || {};
|
||||
const targetMessageId = getMessageReplyInfo(message)?.replyToMsgId;
|
||||
const targetMessage = targetMessageId
|
||||
? selectChatMessage(global, chatId, targetMessageId)
|
||||
: undefined;
|
||||
|
||||
const theme = selectTheme(global);
|
||||
const {
|
||||
patternColor,
|
||||
} = global.settings.themes[theme] || {};
|
||||
|
||||
const isFocused = threadId ? selectIsMessageFocused(global, message, threadId) : false;
|
||||
const {
|
||||
direction: focusDirection,
|
||||
noHighlight: noFocusHighlight,
|
||||
} = (isFocused && selectTabState(global).focusedMessage) || {};
|
||||
|
||||
const senderUser = selectUser(global, senderId || chatId);
|
||||
const senderChat = selectChat(global, chatId);
|
||||
|
||||
const targetChat = targetChatId ? selectChat(global, targetChatId) : undefined;
|
||||
|
||||
const giftDuration = content.action?.months;
|
||||
const premiumGiftSticker = selectGiftStickerForDuration(global, giftDuration);
|
||||
|
||||
const starCount = content.action?.stars;
|
||||
const starsGiftSticker = selectGiftStickerForStars(global, starCount);
|
||||
|
||||
const topic = selectTopicFromMessage(global, message);
|
||||
|
||||
return {
|
||||
senderUser,
|
||||
senderChat,
|
||||
targetChat,
|
||||
targetChatId,
|
||||
targetUserIds,
|
||||
targetMessage,
|
||||
isFocused,
|
||||
premiumGiftSticker,
|
||||
starsGiftSticker,
|
||||
topic,
|
||||
patternColor,
|
||||
canPlayAnimatedEmojis: selectCanPlayAnimatedEmojis(global),
|
||||
...(isFocused && {
|
||||
focusDirection,
|
||||
noFocusHighlight,
|
||||
}),
|
||||
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
||||
currentUserId: global.currentUserId,
|
||||
};
|
||||
},
|
||||
)(ActionMessage));
|
||||
@ -1,128 +0,0 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, { memo, useState } from '../../lib/teact/teact';
|
||||
import { getActions } from '../../global';
|
||||
|
||||
import type { ApiMessage } from '../../api/types';
|
||||
import type { TextPart } from '../../types';
|
||||
import { MAIN_THREAD_ID } from '../../api/types';
|
||||
import { MediaViewerOrigin, SettingsScreens } from '../../types';
|
||||
|
||||
import { getPhotoMediaHash, getVideoProfilePhotoMediaHash } from '../../global/helpers';
|
||||
import { fetchBlob } from '../../util/files';
|
||||
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useMedia from '../../hooks/useMedia';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
|
||||
import Avatar from '../common/Avatar';
|
||||
import ConfirmDialog from '../ui/ConfirmDialog';
|
||||
import CropModal from '../ui/CropModal';
|
||||
|
||||
type OwnProps = {
|
||||
message: ApiMessage;
|
||||
renderContent: () => TextPart | undefined;
|
||||
};
|
||||
|
||||
const ActionMessageSuggestedAvatar: FC<OwnProps> = ({
|
||||
message,
|
||||
renderContent,
|
||||
}) => {
|
||||
const {
|
||||
openMediaViewer, uploadProfilePhoto, showNotification,
|
||||
} = getActions();
|
||||
|
||||
const { isOutgoing } = message;
|
||||
|
||||
const lang = useOldLang();
|
||||
const [cropModalBlob, setCropModalBlob] = useState<Blob | undefined>();
|
||||
const [isVideoModalOpen, openVideoModal, closeVideoModal] = useFlag(false);
|
||||
const photo = message.content.action!.photo!;
|
||||
const suggestedPhotoUrl = useMedia(getPhotoMediaHash(photo, 'full'));
|
||||
const suggestedVideoUrl = useMedia(getVideoProfilePhotoMediaHash(photo));
|
||||
const isVideo = message.content.action!.photo?.isVideo;
|
||||
|
||||
const showAvatarNotification = useLastCallback(() => {
|
||||
showNotification({
|
||||
title: lang('ApplyAvatarHintTitle'),
|
||||
message: lang('ApplyAvatarHint'),
|
||||
action: {
|
||||
action: 'requestNextSettingsScreen',
|
||||
payload: {
|
||||
screen: SettingsScreens.Main,
|
||||
},
|
||||
},
|
||||
actionText: lang('Open'),
|
||||
});
|
||||
});
|
||||
|
||||
const handleSetSuggestedAvatar = useLastCallback((file: File) => {
|
||||
setCropModalBlob(undefined);
|
||||
uploadProfilePhoto({ file });
|
||||
showAvatarNotification();
|
||||
});
|
||||
|
||||
const handleCloseCropModal = useLastCallback(() => {
|
||||
setCropModalBlob(undefined);
|
||||
});
|
||||
|
||||
const handleSetVideo = useLastCallback(async () => {
|
||||
if (!suggestedVideoUrl) return;
|
||||
|
||||
closeVideoModal();
|
||||
showAvatarNotification();
|
||||
|
||||
// TODO Once we support uploading video avatars, add crop/trim modal here
|
||||
const blob = await fetchBlob(suggestedVideoUrl);
|
||||
uploadProfilePhoto({
|
||||
file: new File([blob], 'avatar.mp4'),
|
||||
isVideo: true,
|
||||
videoTs: photo.videoSizes?.find((l) => l.videoStartTs !== undefined)?.videoStartTs,
|
||||
});
|
||||
});
|
||||
|
||||
const handleViewSuggestedAvatar = async () => {
|
||||
if (!isOutgoing && suggestedPhotoUrl) {
|
||||
if (isVideo) {
|
||||
openVideoModal();
|
||||
} else {
|
||||
setCropModalBlob(await fetchBlob(suggestedPhotoUrl));
|
||||
}
|
||||
} else {
|
||||
openMediaViewer({
|
||||
chatId: message.chatId,
|
||||
messageId: message.id,
|
||||
threadId: MAIN_THREAD_ID,
|
||||
origin: MediaViewerOrigin.SuggestedAvatar,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<span className="action-message-suggested-avatar" tabIndex={0} role="button" onClick={handleViewSuggestedAvatar}>
|
||||
<Avatar
|
||||
photo={message.content.action!.photo}
|
||||
loopIndefinitely
|
||||
withVideo={isVideo}
|
||||
size="jumbo"
|
||||
/>
|
||||
<span>{renderContent()}</span>
|
||||
|
||||
<span className="action-message-button">{lang(isVideo ? 'ViewVideoAction' : 'ViewPhotoAction')}</span>
|
||||
<CropModal
|
||||
file={cropModalBlob}
|
||||
onClose={handleCloseCropModal}
|
||||
onChange={handleSetSuggestedAvatar}
|
||||
/>
|
||||
<ConfirmDialog
|
||||
isOpen={isVideoModalOpen}
|
||||
title={lang('SuggestedVideo')}
|
||||
confirmHandler={handleSetVideo}
|
||||
onClose={closeVideoModal}
|
||||
textParts={renderContent()}
|
||||
/>
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ActionMessageSuggestedAvatar);
|
||||
@ -1,59 +0,0 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, { memo } from '../../lib/teact/teact';
|
||||
import { getActions } from '../../global';
|
||||
|
||||
import type { ApiMessage } from '../../api/types';
|
||||
import type { TextPart } from '../../types';
|
||||
import { MAIN_THREAD_ID } from '../../api/types';
|
||||
import { MediaViewerOrigin } from '../../types';
|
||||
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
|
||||
import Avatar from '../common/Avatar';
|
||||
|
||||
type OwnProps = {
|
||||
message: ApiMessage;
|
||||
renderContent: () => TextPart | undefined;
|
||||
};
|
||||
|
||||
const ActionMessageUpdatedAvatar: FC<OwnProps> = ({
|
||||
message,
|
||||
renderContent,
|
||||
}) => {
|
||||
const {
|
||||
openMediaViewer,
|
||||
} = getActions();
|
||||
|
||||
const lang = useOldLang();
|
||||
|
||||
const handleViewUpdatedAvatar = () => {
|
||||
openMediaViewer({
|
||||
chatId: message.chatId,
|
||||
messageId: message.id,
|
||||
threadId: MAIN_THREAD_ID,
|
||||
origin: MediaViewerOrigin.SuggestedAvatar,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<span>{renderContent()}</span>
|
||||
<span
|
||||
className="action-message-updated-avatar"
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
onClick={handleViewUpdatedAvatar}
|
||||
aria-label={lang('ViewPhotoAction')}
|
||||
>
|
||||
<Avatar
|
||||
photo={message.content.action!.photo}
|
||||
loopIndefinitely
|
||||
withVideo
|
||||
size="jumbo"
|
||||
/>
|
||||
</span>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ActionMessageUpdatedAvatar);
|
||||
@ -1,4 +1,6 @@
|
||||
.MessageList {
|
||||
--action-message-bg: var(--pattern-color);
|
||||
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
margin-bottom: 0.5rem;
|
||||
@ -200,7 +202,7 @@
|
||||
.Message,
|
||||
.ActionMessage {
|
||||
&::before {
|
||||
background: var(--pattern-color);
|
||||
background-color: var(--action-message-bg);
|
||||
}
|
||||
|
||||
&.focused,
|
||||
@ -226,8 +228,6 @@
|
||||
text-align: left;
|
||||
}
|
||||
}
|
||||
|
||||
.join-text,
|
||||
.sticky-date,
|
||||
.local-action-message,
|
||||
.ActionMessage,
|
||||
@ -235,17 +235,17 @@
|
||||
text-align: center;
|
||||
user-select: none;
|
||||
|
||||
--custom-emoji-size: calc(var(--message-text-size, 1rem) + 0.125rem);
|
||||
font-size: calc(var(--message-text-size, 1rem) - 0.0625rem);
|
||||
line-height: 1.25;
|
||||
|
||||
> span {
|
||||
display: inline-block;
|
||||
background: var(--pattern-color);
|
||||
background-color: var(--action-message-bg);
|
||||
color: white;
|
||||
font-size: calc(var(--message-text-size, 1rem) - 0.0625rem);
|
||||
font-weight: var(--font-weight-medium);
|
||||
line-height: 1rem;
|
||||
padding: 0.1875rem 0.5rem;
|
||||
border-radius: var(--border-radius-messages);
|
||||
word-break: break-word;
|
||||
|
||||
position: relative;
|
||||
border-radius: var(--border-radius-messages);
|
||||
|
||||
z-index: 0;
|
||||
|
||||
body.is-ios &,
|
||||
@ -263,182 +263,14 @@
|
||||
}
|
||||
}
|
||||
|
||||
.action-message-content {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.ActionMessage.centered-action {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.centered-action .action-message-content {
|
||||
max-width: 17rem;
|
||||
}
|
||||
|
||||
.web-page-gift,
|
||||
.action-message-gift {
|
||||
display: flex !important;
|
||||
width: 13.75rem;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
line-height: 1rem !important;
|
||||
padding-bottom: 0.75rem !important;
|
||||
margin-top: 0.5rem;
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
outline: none;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.web-page-gift {
|
||||
position: relative;
|
||||
min-width: 12.5rem;
|
||||
width: 100%;
|
||||
margin-top: 0;
|
||||
padding-block: 2rem !important;
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
.web-page-centered,
|
||||
.action-message-centered {
|
||||
margin-inline: auto;
|
||||
}
|
||||
|
||||
.web-page-unique,
|
||||
.action-message-unique {
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -0.25rem;
|
||||
background: var(--pattern-color);
|
||||
border-radius: calc(var(--border-radius-messages) + 0.25rem);
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
.web-page-unique-background-wrapper,
|
||||
.action-message-unique-background-wrapper {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.web-page-unique-background,
|
||||
.action-message-unique-background {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
top: -6rem;
|
||||
}
|
||||
.web-page-unique-background {
|
||||
top: -1rem;
|
||||
}
|
||||
|
||||
.action-message-user-caption,
|
||||
.action-message-stars-balance {
|
||||
position: relative;
|
||||
margin-top: 0.5rem;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
line-height: 1.5;
|
||||
.sticky-date,
|
||||
.local-action-message,
|
||||
.empty {
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.action-message-user-caption {
|
||||
align-items: center;
|
||||
font-size: 0.9375rem;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
}
|
||||
|
||||
.action-message-unique-title {
|
||||
position: relative;
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.action-message-unique-properties {
|
||||
display: grid;
|
||||
grid-template-columns: min-content 1fr;
|
||||
justify-content: center;
|
||||
gap: 0.375rem;
|
||||
font-size: 0.875rem;
|
||||
margin-top: 0.5rem;
|
||||
|
||||
position: relative;
|
||||
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.action-message-unique-value {
|
||||
color: white;
|
||||
justify-self: flex-start;
|
||||
width: 100%; // Grid ellipsis hack
|
||||
text-align: initial;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.action-message-unique-property {
|
||||
justify-self: flex-end;
|
||||
font-weight: var(--font-weight-normal);
|
||||
}
|
||||
|
||||
.action-message-user-avatar {
|
||||
margin: 0 0.25rem;
|
||||
}
|
||||
|
||||
.action-message-subtitle {
|
||||
margin-top: 1rem;
|
||||
font-weight: normal;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.action-message-gift-subtitle {
|
||||
position: relative;
|
||||
font-weight: normal;
|
||||
text-wrap: balance;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
|
||||
.action-message-suggested-avatar {
|
||||
max-width: 16rem;
|
||||
display: flex !important;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
line-height: 1rem !important;
|
||||
padding-bottom: 0.75rem !important;
|
||||
margin-top: 0.5rem;
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
outline: none;
|
||||
|
||||
.Avatar {
|
||||
width: 6.5rem;
|
||||
height: 6.5rem;
|
||||
margin: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
.action-message-updated-avatar {
|
||||
background: transparent !important;
|
||||
margin-top: 0.5rem;
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.action-message-button {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
border-radius: var(--border-radius-modal);
|
||||
padding: 0.5rem 1.5rem;
|
||||
margin-top: 0.6875rem;
|
||||
background-color: var(--pattern-color);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
transition: opacity 0.15s;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
opacity: 0.8;
|
||||
& > span {
|
||||
display: inline-block;
|
||||
padding: 0.1875rem 0.5rem;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
@ -491,24 +323,6 @@
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.ActionMessage {
|
||||
.action-link {
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
|
||||
&:hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.underlined-link {
|
||||
text-decoration: underline;
|
||||
|
||||
&:hover {
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.sticky-date + .ActionMessage {
|
||||
margin-top: -0.375rem;
|
||||
}
|
||||
|
||||
@ -290,11 +290,10 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
||||
isOutgoing: false,
|
||||
content: {
|
||||
action: {
|
||||
type: 'joinedChannel',
|
||||
mediaType: 'action',
|
||||
text: '',
|
||||
translationValues: [],
|
||||
targetChatId: message.chatId,
|
||||
type: 'channelJoined',
|
||||
inviterId: channelJoinInfo?.inviterId,
|
||||
isViaRequest: channelJoinInfo?.isViaRequest || undefined,
|
||||
},
|
||||
},
|
||||
} satisfies ApiMessage);
|
||||
|
||||
@ -29,7 +29,7 @@ import usePreviousDeprecated from '../../hooks/usePreviousDeprecated';
|
||||
import useMessageObservers from './hooks/useMessageObservers';
|
||||
import useScrollHooks from './hooks/useScrollHooks';
|
||||
|
||||
import ActionMessage from './ActionMessage';
|
||||
import ActionMessage from './message/ActionMessage';
|
||||
import Message from './message/Message';
|
||||
import SenderGroupContainer from './message/SenderGroupContainer';
|
||||
import SponsoredMessage from './message/SponsoredMessage';
|
||||
@ -155,7 +155,7 @@ const MessageListContent: FC<OwnProps> = ({
|
||||
senderGroup.length === 1
|
||||
&& !isAlbum(senderGroup[0])
|
||||
&& isActionMessage(senderGroup[0])
|
||||
&& !senderGroup[0].content.action?.phoneCall
|
||||
&& senderGroup[0].content.action?.type !== 'phoneCall'
|
||||
) {
|
||||
const message = senderGroup[0]!;
|
||||
const isLastInList = (
|
||||
@ -169,15 +169,14 @@ const MessageListContent: FC<OwnProps> = ({
|
||||
key={message.id}
|
||||
message={message}
|
||||
threadId={threadId}
|
||||
messageListType={type}
|
||||
isInsideTopic={Boolean(threadId && threadId !== MAIN_THREAD_ID && !isSavedDialog)}
|
||||
observeIntersectionForReading={observeIntersectionForReading}
|
||||
observeIntersectionForBottom={observeIntersectionForReading}
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
memoFirstUnreadIdRef={memoFirstUnreadIdRef}
|
||||
appearanceOrder={messageCountToAnimate - ++appearanceIndex}
|
||||
isJustAdded={isLastInList && isNewMessage}
|
||||
isLastInList={isLastInList}
|
||||
getIsMessageListReady={getIsReady}
|
||||
onIntersectPinnedMessage={onIntersectPinnedMessage}
|
||||
/>,
|
||||
]);
|
||||
|
||||
@ -78,6 +78,7 @@ import { useResize } from '../../hooks/useResize';
|
||||
import useSyncEffect from '../../hooks/useSyncEffect';
|
||||
import useWindowSize from '../../hooks/window/useWindowSize';
|
||||
import usePinnedMessage from './hooks/usePinnedMessage';
|
||||
import useFluidBackgroundFilter from './message/hooks/useFluidBackgroundFilter';
|
||||
|
||||
import Composer from '../common/Composer';
|
||||
import Icon from '../common/icons/Icon';
|
||||
@ -466,6 +467,9 @@ function MiddleColumn({
|
||||
onBack: exitMessageSelectMode,
|
||||
});
|
||||
|
||||
// Prepare filter beforehand to avoid flickering
|
||||
useFluidBackgroundFilter(patternColor);
|
||||
|
||||
const isMessagingDisabled = Boolean(
|
||||
!isPinnedMessageList && !isSavedDialog && !renderingCanPost && !renderingCanRestartBot && !renderingCanStartBot
|
||||
&& !renderingCanSubscribe && composerRestrictionMessage,
|
||||
|
||||
@ -53,7 +53,7 @@ const StickerSetCover: FC<OwnProps> = ({
|
||||
|
||||
const { customEmoji } = useCustomEmoji(thumbCustomEmojiId);
|
||||
const hasCustomColor = customEmoji?.shouldUseTextColor;
|
||||
const customColor = useDynamicColorListener(containerRef, !hasCustomColor);
|
||||
const customColor = useDynamicColorListener(containerRef, undefined, !hasCustomColor);
|
||||
const colorFilter = useColorFilter(customColor);
|
||||
|
||||
const isIntersecting = useIsIntersecting(containerRef, observeIntersection);
|
||||
|
||||
@ -48,7 +48,7 @@ export default function useInputCustomEmojis(
|
||||
isReady?: boolean,
|
||||
isActive?: boolean,
|
||||
) {
|
||||
const customColor = useDynamicColorListener(inputRef, !isReady);
|
||||
const customColor = useDynamicColorListener(inputRef, undefined, !isReady);
|
||||
const colorFilter = useColorFilter(customColor, true);
|
||||
const dpr = useDevicePixelRatio();
|
||||
const playersById = useRef<Map<string, CustomEmojiPlayer>>(new Map());
|
||||
|
||||
@ -92,8 +92,8 @@ export function groupMessages(
|
||||
|| message.senderId !== nextMessage.senderId
|
||||
|| message.isOutgoing !== nextMessage.isOutgoing
|
||||
|| message.postAuthorTitle !== nextMessage.postAuthorTitle
|
||||
|| (isActionMessage(message) && !message.content.action?.phoneCall)
|
||||
|| (isActionMessage(nextMessage) && !nextMessage.content.action?.phoneCall)
|
||||
|| (isActionMessage(message) && message.content.action?.type !== 'phoneCall')
|
||||
|| (isActionMessage(nextMessage) && nextMessage.content.action?.type !== 'phoneCall')
|
||||
|| message.inlineButtons
|
||||
|| nextMessage.inlineButtons
|
||||
|| (nextMessage.date - message.date) > GROUP_INTERVAL_SECONDS
|
||||
|
||||
184
src/components/middle/message/ActionMessage.module.scss
Normal file
184
src/components/middle/message/ActionMessage.module.scss
Normal file
@ -0,0 +1,184 @@
|
||||
.root {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
justify-items: center;
|
||||
|
||||
padding-block: 0.125rem;
|
||||
|
||||
word-break: break-word;
|
||||
|
||||
:global(.star-amount-icon) {
|
||||
vertical-align: text-bottom;
|
||||
margin-inline: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.contentBox {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
max-width: 15rem;
|
||||
|
||||
padding: 0.5rem 0.75rem;
|
||||
border-radius: var(--border-radius-messages);
|
||||
|
||||
background-color: var(--action-message-bg);
|
||||
color: white;
|
||||
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.textContent {
|
||||
display: inline-block;
|
||||
padding: 0.1875rem 0.5rem;
|
||||
background-color: var(--action-message-bg);
|
||||
color: white;
|
||||
border-radius: var(--border-radius-messages);
|
||||
max-width: 100%;
|
||||
|
||||
position: relative;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.hasFullContent {
|
||||
& > .textContent,
|
||||
& > .inlineWrapper {
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
.singleLine .textContent {
|
||||
display: inline-flex;
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
.strong {
|
||||
font-weight: var(--font-weight-medium);
|
||||
}
|
||||
|
||||
.messageLink {
|
||||
overflow: hidden;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.singleLine, .messageLink {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.inlineWrapper {
|
||||
grid-area: 1 / 1;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.contextContainer {
|
||||
grid-area: 1 / 1;
|
||||
}
|
||||
|
||||
.fluidMultiline {
|
||||
background-color: transparent;
|
||||
|
||||
// These two elements should align perfectly
|
||||
.fluidBackground, .textContent {
|
||||
display: inline;
|
||||
text-wrap: pretty;
|
||||
padding: 0.1875rem 0.5rem;
|
||||
box-decoration-break: clone;
|
||||
border-radius: var(--border-radius-messages);
|
||||
}
|
||||
|
||||
.fluidBackground {
|
||||
pointer-events: none;
|
||||
// Solid color for filter
|
||||
background-color: black;
|
||||
color: black;
|
||||
}
|
||||
|
||||
.textContent {
|
||||
background-color: transparent !important;
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
font-size: 0.9375rem;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.stickerWrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.channelPhoto {
|
||||
--radius: var(--border-radius-messages);
|
||||
}
|
||||
|
||||
.suggestedAvatar {
|
||||
margin-top: 0.25rem 0.125rem;
|
||||
}
|
||||
|
||||
.suggestedText {
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.actionButton {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
border-radius: 1.25rem;
|
||||
padding: 0.5rem 1.25rem;
|
||||
background-color: var(--action-message-bg);
|
||||
font-weight: var(--font-weight-semibold);
|
||||
transition: opacity 0.15s;
|
||||
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: 0;
|
||||
font-size: inherit;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
font-size: 0.8125rem;
|
||||
text-wrap: balance;
|
||||
}
|
||||
|
||||
.starGift {
|
||||
width: 13.75rem;
|
||||
}
|
||||
|
||||
.uniqueGift {
|
||||
margin-block: 0.25rem;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
inset: -0.25rem;
|
||||
background: var(--action-message-bg);
|
||||
border-radius: calc(var(--border-radius-messages) + 0.25rem);
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
.uniqueBackgroundWrapper {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.uniqueBackground {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
top: -6rem;
|
||||
}
|
||||
|
||||
.uniqueValue {
|
||||
color: white;
|
||||
}
|
||||
486
src/components/middle/message/ActionMessage.tsx
Normal file
486
src/components/middle/message/ActionMessage.tsx
Normal file
@ -0,0 +1,486 @@
|
||||
import React, {
|
||||
memo, useEffect, useMemo, useRef, useUnmountCleanup,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiMessageAction } from '../../../api/types/messageActions';
|
||||
import type {
|
||||
FocusDirection,
|
||||
ThreadId,
|
||||
} from '../../../types';
|
||||
import type { Signal } from '../../../util/signals';
|
||||
import { type ApiMessage, type ApiPeer, MAIN_THREAD_ID } from '../../../api/types';
|
||||
import { MediaViewerOrigin } from '../../../types';
|
||||
|
||||
import { MESSAGE_APPEARANCE_DELAY } from '../../../config';
|
||||
import { getMessageHtmlId } from '../../../global/helpers';
|
||||
import { getMessageReplyInfo } from '../../../global/helpers/replies';
|
||||
import {
|
||||
selectChat,
|
||||
selectChatMessage,
|
||||
selectIsCurrentUserPremium,
|
||||
selectIsInSelectMode,
|
||||
selectIsMessageFocused,
|
||||
selectSender,
|
||||
selectTabState,
|
||||
selectTheme,
|
||||
} from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { isLocalMessageId } from '../../../util/keys/messageKey';
|
||||
import { isElementInViewport } from '../../../util/visibility/isElementInViewport';
|
||||
import { IS_ANDROID, IS_ELECTRON, IS_FLUID_BACKGROUND_SUPPORTED } from '../../../util/windowEnvironment';
|
||||
import { preventMessageInputBlur } from '../helpers/preventMessageInputBlur';
|
||||
|
||||
import useAppLayout from '../../../hooks/useAppLayout';
|
||||
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
||||
import useEnsureMessage from '../../../hooks/useEnsureMessage';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import { type ObserveFn, useOnIntersect } from '../../../hooks/useIntersectionObserver';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useMessageResizeObserver from '../../../hooks/useResizeMessageObserver';
|
||||
import useShowTransition from '../../../hooks/useShowTransition';
|
||||
import { type OnIntersectPinnedMessage } from '../hooks/usePinnedMessage';
|
||||
import useFluidBackgroundFilter from './hooks/useFluidBackgroundFilter';
|
||||
import useFocusMessage from './hooks/useFocusMessage';
|
||||
|
||||
import ActionMessageText from './ActionMessageText';
|
||||
import ChannelPhoto from './actions/ChannelPhoto';
|
||||
import Gift from './actions/Gift';
|
||||
import PremiumGiftCode from './actions/GiveawayPrize';
|
||||
import StarGift from './actions/StarGift';
|
||||
import StarGiftUnique from './actions/StarGiftUnique';
|
||||
import SuggestedPhoto from './actions/SuggestedPhoto';
|
||||
import ContextMenuContainer from './ContextMenuContainer';
|
||||
import Reactions from './reactions/Reactions';
|
||||
import SimilarChannels from './SimilarChannels';
|
||||
|
||||
import styles from './ActionMessage.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
message: ApiMessage;
|
||||
threadId: ThreadId;
|
||||
appearanceOrder: number;
|
||||
isJustAdded?: boolean;
|
||||
isLastInList?: boolean;
|
||||
memoFirstUnreadIdRef?: { current: number | undefined };
|
||||
getIsMessageListReady?: Signal<boolean>;
|
||||
onIntersectPinnedMessage?: OnIntersectPinnedMessage;
|
||||
observeIntersectionForBottom?: ObserveFn;
|
||||
observeIntersectionForLoading?: ObserveFn;
|
||||
observeIntersectionForPlaying?: ObserveFn;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
sender?: ApiPeer;
|
||||
currentUserId?: string;
|
||||
isInsideTopic?: boolean;
|
||||
isFocused?: boolean;
|
||||
focusDirection?: FocusDirection;
|
||||
noFocusHighlight?: boolean;
|
||||
replyMessage?: ApiMessage;
|
||||
patternColor?: string;
|
||||
isCurrentUserPremium?: boolean;
|
||||
isInSelectMode?: boolean;
|
||||
hasUnreadReaction?: boolean;
|
||||
};
|
||||
|
||||
const SINGLE_LINE_ACTIONS: Set<ApiMessageAction['type']> = new Set([
|
||||
'pinMessage',
|
||||
'chatEditPhoto',
|
||||
'chatDeletePhoto',
|
||||
'unsupported',
|
||||
]);
|
||||
const HIDDEN_TEXT_ACTIONS: Set<ApiMessageAction['type']> = new Set(['giftCode', 'prizeStars', 'suggestProfilePhoto']);
|
||||
|
||||
const ActionMessage = ({
|
||||
message,
|
||||
threadId,
|
||||
sender,
|
||||
currentUserId,
|
||||
appearanceOrder,
|
||||
isJustAdded,
|
||||
isLastInList,
|
||||
memoFirstUnreadIdRef,
|
||||
getIsMessageListReady,
|
||||
isInsideTopic,
|
||||
isFocused,
|
||||
focusDirection,
|
||||
noFocusHighlight,
|
||||
replyMessage,
|
||||
patternColor,
|
||||
isCurrentUserPremium,
|
||||
isInSelectMode,
|
||||
hasUnreadReaction,
|
||||
onIntersectPinnedMessage,
|
||||
observeIntersectionForBottom,
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
}: OwnProps & StateProps) => {
|
||||
const {
|
||||
requestConfetti,
|
||||
openMediaViewer,
|
||||
getReceipt,
|
||||
checkGiftCode,
|
||||
openPrizeStarsTransactionFromGiveaway,
|
||||
openPremiumModal,
|
||||
openStarsTransactionFromGift,
|
||||
openGiftInfoModalFromMessage,
|
||||
toggleChannelRecommendations,
|
||||
animateUnreadReaction,
|
||||
markMentionsRead,
|
||||
} = getActions();
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { id, chatId } = message;
|
||||
const action = message.content.action!;
|
||||
const isLocal = isLocalMessageId(id);
|
||||
|
||||
const isTextHidden = HIDDEN_TEXT_ACTIONS.has(action.type);
|
||||
const isSingleLine = SINGLE_LINE_ACTIONS.has(action.type);
|
||||
const isFluidMultiline = IS_FLUID_BACKGROUND_SUPPORTED && !isSingleLine;
|
||||
|
||||
const messageReplyInfo = getMessageReplyInfo(message);
|
||||
const { replyToMsgId, replyToPeerId } = messageReplyInfo || {};
|
||||
|
||||
const withServiceReactions = Boolean(message.areReactionsPossible && message?.reactions?.results?.length);
|
||||
|
||||
const shouldSkipRender = isInsideTopic && action.type === 'topicCreate';
|
||||
|
||||
const { isTouchScreen } = useAppLayout();
|
||||
|
||||
useOnIntersect(ref, !shouldSkipRender ? observeIntersectionForBottom : undefined);
|
||||
|
||||
useMessageResizeObserver(ref, !shouldSkipRender && isLastInList && action.type !== 'channelJoined');
|
||||
|
||||
useEnsureMessage(
|
||||
replyToPeerId || chatId,
|
||||
replyToMsgId,
|
||||
replyMessage,
|
||||
id,
|
||||
);
|
||||
useFocusMessage({
|
||||
elementRef: ref,
|
||||
chatId: message.chatId,
|
||||
isFocused,
|
||||
focusDirection,
|
||||
noFocusHighlight,
|
||||
isJustAdded,
|
||||
});
|
||||
|
||||
useUnmountCleanup(() => {
|
||||
if (message.isPinned) {
|
||||
onIntersectPinnedMessage?.({ viewportPinnedIdsToRemove: [message.id] });
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
isContextMenuOpen, contextMenuAnchor,
|
||||
handleBeforeContextMenu, handleContextMenu,
|
||||
handleContextMenuClose, handleContextMenuHide,
|
||||
} = useContextMenuHandlers(
|
||||
ref,
|
||||
isTouchScreen && isInSelectMode,
|
||||
!IS_ELECTRON,
|
||||
IS_ANDROID,
|
||||
getIsMessageListReady,
|
||||
);
|
||||
const isContextMenuShown = contextMenuAnchor !== undefined;
|
||||
|
||||
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
|
||||
preventMessageInputBlur(e);
|
||||
handleBeforeContextMenu(e);
|
||||
};
|
||||
|
||||
const noAppearanceAnimation = appearanceOrder <= 0;
|
||||
const [isShown, markShown] = useFlag(noAppearanceAnimation);
|
||||
useEffect(() => {
|
||||
if (noAppearanceAnimation) {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(markShown, appearanceOrder * MESSAGE_APPEARANCE_DELAY);
|
||||
}, [appearanceOrder, markShown, noAppearanceAnimation]);
|
||||
|
||||
const { ref: refWithTransition } = useShowTransition({
|
||||
isOpen: isShown,
|
||||
noOpenTransition: noAppearanceAnimation,
|
||||
noCloseTransition: true,
|
||||
className: false,
|
||||
ref,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const bottomMarker = ref.current;
|
||||
if (!bottomMarker || !isElementInViewport(bottomMarker)) return;
|
||||
|
||||
if (hasUnreadReaction) {
|
||||
animateUnreadReaction({ messageIds: [id] });
|
||||
}
|
||||
|
||||
if (message.hasUnreadMention) {
|
||||
markMentionsRead({ messageIds: [id] });
|
||||
}
|
||||
}, [hasUnreadReaction, id, animateUnreadReaction, message.hasUnreadMention]);
|
||||
|
||||
useEffect(() => {
|
||||
if (action.type !== 'giftPremium') return;
|
||||
if ((memoFirstUnreadIdRef?.current && id >= memoFirstUnreadIdRef.current) || isLocal) {
|
||||
requestConfetti({});
|
||||
}
|
||||
}, [action.type, id, isLocal, memoFirstUnreadIdRef]);
|
||||
|
||||
const fluidBackgroundStyle = useFluidBackgroundFilter(isFluidMultiline ? patternColor : undefined);
|
||||
|
||||
const handleClick = useLastCallback(() => {
|
||||
switch (action.type) {
|
||||
case 'paymentSent':
|
||||
case 'paymentRefunded': {
|
||||
getReceipt({
|
||||
chatId: message.chatId,
|
||||
messageId: message.id,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'chatEditPhoto': {
|
||||
openMediaViewer({
|
||||
chatId: message.chatId,
|
||||
messageId: message.id,
|
||||
threadId,
|
||||
origin: MediaViewerOrigin.ChannelAvatar,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'giftCode': {
|
||||
checkGiftCode({ slug: action.slug, message: { chatId: message.chatId, messageId: message.id } });
|
||||
break;
|
||||
}
|
||||
|
||||
case 'prizeStars': {
|
||||
openPrizeStarsTransactionFromGiveaway({
|
||||
chatId: message.chatId,
|
||||
messageId: message.id,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'giftPremium': {
|
||||
openPremiumModal({
|
||||
isGift: true,
|
||||
fromUserId: sender?.id,
|
||||
toUserId: sender && sender.id === currentUserId ? chatId : currentUserId,
|
||||
monthsAmount: action.months,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'giftStars': {
|
||||
openStarsTransactionFromGift({
|
||||
chatId: message.chatId,
|
||||
messageId: message.id,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'starGift':
|
||||
case 'starGiftUnique': {
|
||||
openGiftInfoModalFromMessage({
|
||||
chatId: message.chatId,
|
||||
messageId: message.id,
|
||||
});
|
||||
break;
|
||||
}
|
||||
|
||||
case 'channelJoined': {
|
||||
toggleChannelRecommendations({ chatId });
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const fullContent = useMemo(() => {
|
||||
switch (action.type) {
|
||||
case 'chatEditPhoto': {
|
||||
if (!action.photo) return undefined;
|
||||
return (
|
||||
<ChannelPhoto
|
||||
action={action}
|
||||
observeIntersection={observeIntersectionForLoading}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
case 'suggestProfilePhoto':
|
||||
return (
|
||||
<SuggestedPhoto
|
||||
message={message}
|
||||
action={action}
|
||||
observeIntersection={observeIntersectionForLoading}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'prizeStars':
|
||||
case 'giftCode':
|
||||
return (
|
||||
<PremiumGiftCode
|
||||
action={action}
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'giftPremium':
|
||||
case 'giftStars':
|
||||
return (
|
||||
<Gift
|
||||
action={action}
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'starGift':
|
||||
return (
|
||||
<StarGift
|
||||
action={action}
|
||||
message={message}
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'starGiftUnique':
|
||||
return (
|
||||
<StarGiftUnique
|
||||
action={action}
|
||||
message={message}
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
onClick={handleClick}
|
||||
/>
|
||||
);
|
||||
|
||||
case 'channelJoined':
|
||||
return (
|
||||
<SimilarChannels
|
||||
chatId={message.chatId}
|
||||
/>
|
||||
);
|
||||
|
||||
default:
|
||||
return undefined;
|
||||
}
|
||||
}, [action, observeIntersectionForLoading, message, observeIntersectionForPlaying]);
|
||||
|
||||
if ((isInsideTopic && action.type === 'topicCreate') || action.type === 'phoneCall') {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={refWithTransition}
|
||||
id={getMessageHtmlId(id)}
|
||||
className={buildClassName(
|
||||
'ActionMessage',
|
||||
styles.root,
|
||||
isSingleLine && styles.singleLine,
|
||||
isFluidMultiline && styles.fluidMultiline,
|
||||
fullContent && styles.hasFullContent,
|
||||
isFocused && !noFocusHighlight && 'focused',
|
||||
isContextMenuShown && 'has-menu-open',
|
||||
isLastInList && 'last-in-list',
|
||||
)}
|
||||
data-message-id={message.id}
|
||||
data-is-pinned={message.isPinned || undefined}
|
||||
data-has-unread-mention={message.hasUnreadMention || undefined}
|
||||
data-has-unread-reaction={hasUnreadReaction || undefined}
|
||||
onMouseDown={handleMouseDown}
|
||||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
{!isTextHidden && (
|
||||
<>
|
||||
{isFluidMultiline && (
|
||||
<div className={styles.inlineWrapper}>
|
||||
<span className={styles.fluidBackground} style={fluidBackgroundStyle}>
|
||||
<ActionMessageText message={message} isInsideTopic={isInsideTopic} />
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
<div className={styles.inlineWrapper}>
|
||||
<span className={styles.textContent} onClick={handleClick}>
|
||||
<ActionMessageText message={message} isInsideTopic={isInsideTopic} />
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{fullContent}
|
||||
{contextMenuAnchor && (
|
||||
<ContextMenuContainer
|
||||
isOpen={isContextMenuOpen}
|
||||
anchor={contextMenuAnchor}
|
||||
message={message}
|
||||
messageListType="thread"
|
||||
className={styles.contextContainer}
|
||||
onClose={handleContextMenuClose}
|
||||
onCloseAnimationEnd={handleContextMenuHide}
|
||||
/>
|
||||
)}
|
||||
{withServiceReactions && (
|
||||
<Reactions
|
||||
isOutside
|
||||
message={message!}
|
||||
threadId={threadId}
|
||||
observeIntersection={observeIntersectionForPlaying}
|
||||
isCurrentUserPremium={isCurrentUserPremium}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { message, threadId }): StateProps => {
|
||||
const { settings: { themes } } = global;
|
||||
const tabState = selectTabState(global);
|
||||
const chat = selectChat(global, message.chatId);
|
||||
|
||||
const sender = selectSender(global, message);
|
||||
|
||||
const isInsideTopic = chat?.isForum && threadId !== MAIN_THREAD_ID;
|
||||
|
||||
const { replyToMsgId, replyToPeerId } = getMessageReplyInfo(message) || {};
|
||||
const replyMessage = replyToMsgId
|
||||
? selectChatMessage(global, replyToPeerId || message.chatId, replyToMsgId) : undefined;
|
||||
|
||||
const isFocused = threadId ? selectIsMessageFocused(global, message, threadId) : false;
|
||||
const {
|
||||
direction: focusDirection,
|
||||
noHighlight: noFocusHighlight,
|
||||
} = (isFocused && tabState.focusedMessage) || {};
|
||||
|
||||
const isCurrentUserPremium = selectIsCurrentUserPremium(global);
|
||||
|
||||
const hasUnreadReaction = chat?.unreadReactions?.includes(message.id);
|
||||
|
||||
return {
|
||||
sender,
|
||||
currentUserId: global.currentUserId,
|
||||
isCurrentUserPremium,
|
||||
isFocused,
|
||||
focusDirection,
|
||||
noFocusHighlight,
|
||||
isInsideTopic,
|
||||
replyMessage,
|
||||
isInSelectMode: selectIsInSelectMode(global),
|
||||
patternColor: themes[selectTheme(global)]?.patternColor,
|
||||
hasUnreadReaction,
|
||||
};
|
||||
},
|
||||
)(ActionMessage));
|
||||
725
src/components/middle/message/ActionMessageText.tsx
Normal file
725
src/components/middle/message/ActionMessageText.tsx
Normal file
@ -0,0 +1,725 @@
|
||||
import React, { memo, type TeactNode } from '../../../lib/teact/teact';
|
||||
import { getActions, getGlobal, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiChat, ApiMessage, ApiPeer } from '../../../api/types';
|
||||
|
||||
import { GENERAL_TOPIC_ID, SERVICE_NOTIFICATIONS_USER_ID, TME_LINK_PREFIX } from '../../../config';
|
||||
import {
|
||||
getMessageInvoice, getMessageText, getPeerTitle, isChatChannel,
|
||||
} from '../../../global/helpers';
|
||||
import { getMessageReplyInfo } from '../../../global/helpers/replies';
|
||||
import {
|
||||
selectChat,
|
||||
selectChatMessage,
|
||||
selectPeer,
|
||||
selectSender,
|
||||
selectThreadIdFromMessage,
|
||||
selectTopic,
|
||||
} from '../../../global/selectors';
|
||||
import { formatDateTimeToString, formatShortDuration } from '../../../util/dates/dateFormat';
|
||||
import { ensureProtocol } from '../../../util/ensureProtocol';
|
||||
import { formatCurrency } from '../../../util/formatCurrency';
|
||||
import { formatStarsAsText } from '../../../util/localization/format';
|
||||
import { conjuctionWithNodes } from '../../../util/localization/utils';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities';
|
||||
import {
|
||||
getCallMessageKey,
|
||||
getPinnedMediaValue,
|
||||
renderMessageLink,
|
||||
renderPeerLink,
|
||||
translateWithOutgoing,
|
||||
} from './helpers/messageActions';
|
||||
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import CustomEmoji from '../../common/CustomEmoji';
|
||||
import TopicDefaultIcon from '../../common/TopicDefaultIcon';
|
||||
import Link from '../../ui/Link';
|
||||
|
||||
import styles from './ActionMessage.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
message: ApiMessage;
|
||||
isInsideTopic?: boolean;
|
||||
asPreview?: boolean;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
currentUserId?: string;
|
||||
sender?: ApiPeer;
|
||||
replyMessage?: ApiMessage;
|
||||
chat?: ApiChat;
|
||||
};
|
||||
|
||||
const NBSP = '\u00A0';
|
||||
const DEFAULT_TOPIC_ICON_ID = '0';
|
||||
|
||||
const UNSUPPORTED_LANG_KEY = 'ActionUnsupported';
|
||||
|
||||
const ActionMessageText = ({
|
||||
message,
|
||||
currentUserId,
|
||||
sender,
|
||||
chat,
|
||||
replyMessage,
|
||||
isInsideTopic,
|
||||
asPreview,
|
||||
}: OwnProps & StateProps) => {
|
||||
const {
|
||||
openThread,
|
||||
openTelegramLink,
|
||||
openUrl,
|
||||
} = getActions();
|
||||
const { chatId, isOutgoing } = message;
|
||||
const action = message.content.action!;
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
function renderStrong(text: TeactNode) {
|
||||
if (asPreview) return text;
|
||||
return <span className={styles.strong}>{text}</span>;
|
||||
}
|
||||
|
||||
const renderActionText = useLastCallback(() => {
|
||||
const global = getGlobal();
|
||||
|
||||
const isChannel = chat && isChatChannel(chat);
|
||||
const isServiceNotificationsChat = chatId === SERVICE_NOTIFICATIONS_USER_ID;
|
||||
const isSavedMessages = chatId === currentUserId;
|
||||
|
||||
const senderTitle = sender && getPeerTitle(lang, sender);
|
||||
const chatTitle = chat && getPeerTitle(lang, chat);
|
||||
|
||||
const userFallbackText = lang('ActionFallbackUser');
|
||||
const chatFallbackText = lang('ActionFallbackChat');
|
||||
const channelFallbackText = lang('ActionFallbackChannel');
|
||||
|
||||
const senderLink = renderPeerLink(sender?.id, senderTitle || userFallbackText, asPreview);
|
||||
const chatLink = renderPeerLink(chat?.id, chatTitle || chatFallbackText, asPreview);
|
||||
|
||||
switch (action.type) {
|
||||
case 'pinMessage': {
|
||||
if (replyMessage) {
|
||||
const formattedText = getMessageText(replyMessage);
|
||||
if (formattedText) {
|
||||
const textLink = renderMessageLink(
|
||||
replyMessage,
|
||||
renderTextWithEntities({
|
||||
text: formattedText.text,
|
||||
entities: formattedText.entities,
|
||||
asPreview: true,
|
||||
}),
|
||||
asPreview,
|
||||
);
|
||||
|
||||
return translateWithOutgoing(
|
||||
lang, 'ActionPinnedText', isOutgoing, { text: textLink, from: senderLink },
|
||||
);
|
||||
}
|
||||
|
||||
const mediaValue = getPinnedMediaValue(lang, replyMessage);
|
||||
if (mediaValue) {
|
||||
const messageLink = renderMessageLink(replyMessage, mediaValue, asPreview);
|
||||
return translateWithOutgoing(
|
||||
lang, 'ActionPinnedMedia', isOutgoing, { from: senderLink, media: messageLink },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return translateWithOutgoing(
|
||||
lang,
|
||||
'ActionPinnedNotFound',
|
||||
isOutgoing,
|
||||
{ from: senderLink },
|
||||
);
|
||||
}
|
||||
|
||||
case 'gameScore': {
|
||||
const { score } = action;
|
||||
|
||||
const gameTitle = replyMessage?.content.game?.title;
|
||||
const gameLink = gameTitle && renderMessageLink(replyMessage, renderText(gameTitle), asPreview);
|
||||
|
||||
if (gameLink) {
|
||||
return translateWithOutgoing(
|
||||
lang,
|
||||
'ActionGameScore',
|
||||
isOutgoing,
|
||||
{ from: senderLink, count: score, game: gameLink },
|
||||
{ pluralValue: score },
|
||||
);
|
||||
}
|
||||
|
||||
return translateWithOutgoing(
|
||||
lang, 'ActionGameScoreNoGame', isOutgoing, { from: senderLink, count: score }, { pluralValue: score },
|
||||
);
|
||||
}
|
||||
|
||||
case 'chatJoinedByLink':
|
||||
return lang('ActionUserJoinedByLink', { from: senderLink }, { withNodes: true });
|
||||
|
||||
case 'chatJoinedByRequest':
|
||||
return translateWithOutgoing(lang, 'ActionJoinedByRequest', isOutgoing, { from: senderLink });
|
||||
|
||||
case 'channelJoined': {
|
||||
const { isViaRequest, inviterId } = action;
|
||||
const inviter = inviterId ? selectPeer(global, inviterId) : undefined;
|
||||
if (inviter && inviterId !== currentUserId) {
|
||||
const inviterLink = renderPeerLink(inviterId, getPeerTitle(lang, inviter) || userFallbackText, asPreview);
|
||||
return lang('ActionAddYou', { from: inviterLink }, { withNodes: true });
|
||||
}
|
||||
|
||||
return lang(isViaRequest ? 'ActionChannelJoinedByRequestChannelYou' : 'ActionChannelJoinedYou');
|
||||
}
|
||||
|
||||
case 'chatEditTitle': {
|
||||
const { title } = action;
|
||||
if (isChannel) return lang('ActionChangedTitleChannel', { title });
|
||||
return translateWithOutgoing(lang, 'ActionChangedTitle', isOutgoing, { title, from: senderLink });
|
||||
}
|
||||
|
||||
case 'chatDeletePhoto':
|
||||
return isChannel ? lang('ActionRemovedPhotoChannel')
|
||||
: translateWithOutgoing(lang, 'ActionRemovedPhoto', isOutgoing, { from: senderLink });
|
||||
|
||||
case 'chatEditPhoto':
|
||||
return isChannel ? lang('ActionChangedPhotoChannel')
|
||||
: translateWithOutgoing(lang, 'ActionChangedPhoto', isOutgoing, { from: senderLink });
|
||||
|
||||
case 'chatCreate': {
|
||||
const { title } = action;
|
||||
return lang('ActionCreatedChat', { title, from: senderLink }, { withNodes: true });
|
||||
}
|
||||
|
||||
case 'channelCreate': {
|
||||
const { title } = action;
|
||||
return isChannel ? lang('ActionCreatedChannel')
|
||||
: translateWithOutgoing(lang, 'ActionCreatedChat', isOutgoing, { title, from: senderLink });
|
||||
}
|
||||
|
||||
case 'chatMigrateTo': {
|
||||
const { channelId } = action;
|
||||
const channel = selectChat(global, channelId)!;
|
||||
const channelLink = renderPeerLink(channelId, getPeerTitle(lang, channel)!, asPreview);
|
||||
return lang('ActionMigratedTo', { chat: channelLink }, { withNodes: true });
|
||||
}
|
||||
|
||||
case 'channelMigrateFrom': {
|
||||
const { chatId: originalChatId, title } = action;
|
||||
const originalChatLink = renderPeerLink(originalChatId, title || chatFallbackText, asPreview);
|
||||
return lang('ActionMigratedFrom', { chat: originalChatLink }, { withNodes: true });
|
||||
}
|
||||
|
||||
case 'topicCreate': {
|
||||
const { title, iconColor, iconEmojiId } = action;
|
||||
|
||||
const topicId = selectThreadIdFromMessage(global, message);
|
||||
|
||||
const topicLink = (
|
||||
<Link
|
||||
className={styles.topicLink}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => openThread({ chatId, threadId: topicId })}
|
||||
>
|
||||
{iconEmojiId ? <CustomEmoji documentId={iconEmojiId} isSelectable />
|
||||
: <TopicDefaultIcon topicId={topicId} title={title} iconColor={iconColor} />}
|
||||
{NBSP}
|
||||
{renderText(title)}
|
||||
</Link>
|
||||
);
|
||||
return lang('ActionTopicCreated', { topic: topicLink }, { withNodes: true });
|
||||
}
|
||||
|
||||
case 'topicEdit': {
|
||||
const {
|
||||
iconEmojiId, isClosed, isHidden, title,
|
||||
} = action;
|
||||
|
||||
const topicId = selectThreadIdFromMessage(global, message);
|
||||
const currentTopic = selectTopic(global, chatId, topicId);
|
||||
const topicLink = (
|
||||
<Link
|
||||
className={styles.topicLink}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => openThread({ chatId, threadId: topicId })}
|
||||
>
|
||||
{iconEmojiId && iconEmojiId !== DEFAULT_TOPIC_ICON_ID
|
||||
? <CustomEmoji documentId={iconEmojiId} isSelectable />
|
||||
: (
|
||||
<TopicDefaultIcon
|
||||
topicId={topicId}
|
||||
title={title || currentTopic?.title || lang('ActionTopicPlaceholder')}
|
||||
iconColor={currentTopic?.iconColor}
|
||||
/>
|
||||
)}
|
||||
{topicId !== GENERAL_TOPIC_ID && NBSP}
|
||||
{renderText(title || currentTopic?.title || lang('ActionTopicPlaceholder'))}
|
||||
</Link>
|
||||
);
|
||||
|
||||
const topicPlaceholderLink = (
|
||||
<Link
|
||||
className={styles.topicLink}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => openThread({ chatId, threadId: topicId })}
|
||||
>
|
||||
{lang('ActionTopicPlaceholder')}
|
||||
</Link>
|
||||
);
|
||||
|
||||
if (isClosed !== undefined) {
|
||||
if (isInsideTopic) {
|
||||
return lang(isClosed ? 'ActionTopicClosedInside' : 'ActionTopicReopenedInside');
|
||||
}
|
||||
|
||||
return lang(
|
||||
isClosed ? 'ActionTopicClosed' : 'ActionTopicReopened',
|
||||
{ from: senderLink, topic: topicLink },
|
||||
{ withNodes: true },
|
||||
);
|
||||
}
|
||||
|
||||
if (isHidden !== undefined) {
|
||||
if (isInsideTopic) {
|
||||
return lang(isHidden ? 'ActionTopicHiddenInside' : 'ActionTopicUnhiddenInside');
|
||||
}
|
||||
return lang(
|
||||
isHidden ? 'ActionTopicHidden' : 'ActionTopicUnhidden',
|
||||
{ topic: topicLink },
|
||||
{ withNodes: true },
|
||||
);
|
||||
}
|
||||
|
||||
if (title && iconEmojiId) {
|
||||
return lang(
|
||||
'ActionTopicIconAndRenamed',
|
||||
{
|
||||
from: senderLink,
|
||||
link: topicPlaceholderLink,
|
||||
topic: topicLink,
|
||||
},
|
||||
{ withNodes: true },
|
||||
);
|
||||
}
|
||||
|
||||
if (title === undefined) {
|
||||
if (!iconEmojiId || iconEmojiId === DEFAULT_TOPIC_ICON_ID) {
|
||||
return lang(
|
||||
'ActionTopicIconRemoved', { from: senderLink, link: topicPlaceholderLink }, { withNodes: true },
|
||||
);
|
||||
}
|
||||
return lang(
|
||||
'ActionTopicIconChanged',
|
||||
{
|
||||
from: senderLink,
|
||||
link: topicPlaceholderLink,
|
||||
emoji: <CustomEmoji documentId={iconEmojiId} loopLimit={2} />,
|
||||
},
|
||||
{ withNodes: true },
|
||||
);
|
||||
}
|
||||
|
||||
return lang('ActionTopicRenamed', { from: senderLink, link: topicPlaceholderLink, title }, { withNodes: true });
|
||||
}
|
||||
|
||||
case 'boostApply':
|
||||
return translateWithOutgoing(
|
||||
lang,
|
||||
'ActionBoostApply',
|
||||
isOutgoing,
|
||||
{ from: senderLink, count: action.boosts },
|
||||
{ pluralValue: action.boosts },
|
||||
);
|
||||
|
||||
case 'chatAddUser': {
|
||||
const { userIds } = action;
|
||||
|
||||
if (sender?.id === userIds[0]) {
|
||||
return translateWithOutgoing(lang, 'ActionUserJoined', isOutgoing, { from: senderLink });
|
||||
}
|
||||
|
||||
if (userIds.length === 1) {
|
||||
const user = selectPeer(global, userIds[0]);
|
||||
const userTitle = (user && getPeerTitle(lang, user)) || userFallbackText;
|
||||
const userLink = renderPeerLink(user?.id, userTitle, asPreview);
|
||||
return translateWithOutgoing(lang, 'ActionAddUser', isOutgoing, { from: senderLink, user: userLink });
|
||||
}
|
||||
|
||||
const users = userIds.map((userId) => selectPeer(global, userId)).filter(Boolean);
|
||||
|
||||
if (!users.length) {
|
||||
return translateWithOutgoing(lang, 'ActionAddUser', isOutgoing, { from: senderLink, user: userFallbackText });
|
||||
}
|
||||
|
||||
const userLinks = users.map((user) => (
|
||||
renderPeerLink(user.id, getPeerTitle(lang, user) || userFallbackText, asPreview)
|
||||
));
|
||||
|
||||
return translateWithOutgoing(
|
||||
lang, 'ActionAddUsersMany', isOutgoing, { from: senderLink, users: conjuctionWithNodes(lang, userLinks) },
|
||||
);
|
||||
}
|
||||
|
||||
case 'chatDeleteUser': {
|
||||
const { userId } = action;
|
||||
if (sender?.id === userId) {
|
||||
return translateWithOutgoing(lang, 'ActionUserLeft', isOutgoing, { from: senderLink });
|
||||
}
|
||||
|
||||
const user = selectPeer(global, userId);
|
||||
const userTitle = (user && getPeerTitle(lang, user)) || userFallbackText;
|
||||
const userLink = renderPeerLink(user?.id, userTitle, asPreview);
|
||||
return translateWithOutgoing(lang, 'ActionKickUser', isOutgoing, { from: senderLink, user: userLink });
|
||||
}
|
||||
|
||||
case 'botAllowed': {
|
||||
const {
|
||||
app, domain, isAttachMenu, isFromRequest,
|
||||
} = action;
|
||||
if (isAttachMenu) return lang('ActionAttachMenuBotAllowed');
|
||||
if (isFromRequest) return lang('ActionWebappBotAllowed');
|
||||
if (app) {
|
||||
const link = sender?.usernames?.length
|
||||
&& `${TME_LINK_PREFIX + sender.usernames[0].username}/${app.shortName}`;
|
||||
const appLink = link
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
? <Link onClick={() => openTelegramLink({ url: link })}>{app.title}</Link>
|
||||
: lang('ActionBotAppPlaceholder');
|
||||
return lang('ActionBotAllowedFromApp', { app: appLink }, { withNodes: true });
|
||||
}
|
||||
|
||||
if (!domain) return lang(UNSUPPORTED_LANG_KEY);
|
||||
|
||||
const url = ensureProtocol(domain)!;
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
const link = <Link onClick={() => openUrl({ url })}>{domain}</Link>;
|
||||
return lang('ActionBotAllowedFromDomain', { domain: link }, { withNodes: true });
|
||||
}
|
||||
|
||||
case 'giveawayLaunch': {
|
||||
const { stars } = action;
|
||||
|
||||
if (stars) {
|
||||
return lang(
|
||||
isChannel ? 'ActionGiveawayStarsStarted' : 'ActionGiveawayStarsStartedGroup',
|
||||
{ from: senderLink, amount: renderStrong(formatStarsAsText(lang, stars)) },
|
||||
{ withNodes: true },
|
||||
);
|
||||
}
|
||||
|
||||
return lang(
|
||||
isChannel ? 'ActionGiveawayStarted' : 'ActionGiveawayStartedGroup',
|
||||
{ from: senderLink },
|
||||
{ withNodes: true },
|
||||
);
|
||||
}
|
||||
|
||||
case 'giveawayResults': {
|
||||
const { winnersCount, isStars, unclaimedCount } = action;
|
||||
if (!winnersCount) return lang('ActionGiveawayResultsNone');
|
||||
if (unclaimedCount) {
|
||||
return lang(isStars ? 'ActionGiveawayResultsStarsSome' : 'ActionGiveawayResultsSome');
|
||||
}
|
||||
|
||||
return lang(
|
||||
isStars ? 'ActionGiveawayResultsStars' : 'ActionGiveawayResults',
|
||||
{ count: winnersCount },
|
||||
{ pluralValue: winnersCount },
|
||||
);
|
||||
}
|
||||
|
||||
case 'giftStars':
|
||||
case 'giftPremium': {
|
||||
const {
|
||||
amount, currency, cryptoAmount, cryptoCurrency,
|
||||
} = action;
|
||||
|
||||
const price = formatCurrency(lang, amount, currency, { asFontIcon: true });
|
||||
const cryptoPrice = cryptoAmount ? formatCurrency(lang, cryptoAmount, cryptoCurrency!) : undefined;
|
||||
|
||||
const cost = cryptoPrice ? lang('ActionCostCrypto', { price, cryptoPrice }, { withNodes: true }) : price;
|
||||
|
||||
if (isServiceNotificationsChat) {
|
||||
return lang('ActionGiftTextCostAnonymous', { cost }, { withNodes: true });
|
||||
}
|
||||
return translateWithOutgoing(
|
||||
lang, 'ActionGiftTextCost', isOutgoing, { from: senderLink, cost: renderStrong(cost) },
|
||||
);
|
||||
}
|
||||
|
||||
case 'prizeStars':
|
||||
case 'giftCode': {
|
||||
return lang('ActionGiftTextUnknown');
|
||||
}
|
||||
|
||||
case 'groupCall': {
|
||||
const { duration } = action;
|
||||
const durationText = duration ? formatShortDuration(lang, duration) : undefined;
|
||||
if (durationText) {
|
||||
if (isChannel) {
|
||||
return lang('ActionGroupCallFinishedChannel', { duration: durationText });
|
||||
}
|
||||
return lang(
|
||||
'ActionGroupCallFinishedGroup', { from: senderLink, duration: durationText }, { withNodes: true },
|
||||
);
|
||||
}
|
||||
|
||||
if (isChannel) return lang('ActionGroupCallStartedChannel');
|
||||
return lang('ActionGroupCallStartedGroup', { from: senderLink }, { withNodes: true });
|
||||
}
|
||||
|
||||
case 'groupCallScheduled': {
|
||||
const { scheduleDate } = action;
|
||||
const formattedDate = formatDateTimeToString(scheduleDate * 1000, lang.code, true);
|
||||
|
||||
if (isChannel) return lang('ActionGroupCallScheduledChannel', { date: formattedDate });
|
||||
return lang('ActionGroupCallScheduledGroup', { from: senderLink, date: formattedDate }, { withNodes: true });
|
||||
}
|
||||
|
||||
case 'inviteToGroupCall': {
|
||||
const { userIds } = action;
|
||||
|
||||
if (userIds.length === 1) {
|
||||
const user = selectPeer(global, userIds[0]);
|
||||
const userTitle = (user && getPeerTitle(lang, user)) || userFallbackText;
|
||||
const userLink = renderPeerLink(user?.id, userTitle, asPreview);
|
||||
return translateWithOutgoing(lang, 'ActionVideoInvited', isOutgoing, { from: senderLink, user: userLink });
|
||||
}
|
||||
|
||||
const users = userIds.map((userId) => selectPeer(global, userId)).filter(Boolean);
|
||||
|
||||
if (!users.length) {
|
||||
return translateWithOutgoing(
|
||||
lang, 'ActionVideoInvited', isOutgoing, { from: senderLink, user: userFallbackText },
|
||||
);
|
||||
}
|
||||
|
||||
const userLinks = users.map((user) => (
|
||||
renderPeerLink(user.id, getPeerTitle(lang, user) || userFallbackText, asPreview)
|
||||
));
|
||||
|
||||
return translateWithOutgoing(
|
||||
lang, 'ActionVideoInvitedMany', isOutgoing, { from: senderLink, users: conjuctionWithNodes(lang, userLinks) },
|
||||
);
|
||||
}
|
||||
|
||||
case 'paymentSent': {
|
||||
const {
|
||||
currency, totalAmount, isRecurringInit, isRecurringUsed,
|
||||
} = action;
|
||||
|
||||
const cost = renderStrong(formatCurrency(lang, totalAmount, currency, { asFontIcon: true }));
|
||||
const invoice = replyMessage && getMessageInvoice(replyMessage);
|
||||
const invoiceTitle = invoice?.title;
|
||||
|
||||
if (isRecurringUsed) {
|
||||
return lang('ActionPaymentUsedRecurring', { amount: cost }, { withNodes: true });
|
||||
}
|
||||
|
||||
if (!invoiceTitle) {
|
||||
if (isRecurringInit) {
|
||||
return lang('ActionPaymentInitRecurring', { amount: cost, user: chatLink }, { withNodes: true });
|
||||
}
|
||||
|
||||
return lang('ActionPaymentDone', { amount: cost, user: chatLink }, { withNodes: true });
|
||||
}
|
||||
|
||||
if (isRecurringInit) {
|
||||
return lang(
|
||||
'ActionPaymentInitRecurringFor',
|
||||
{ amount: cost, user: chatLink, invoice: renderMessageLink(replyMessage!, invoiceTitle, asPreview) },
|
||||
{ withNodes: true },
|
||||
);
|
||||
}
|
||||
|
||||
return lang(
|
||||
'ActionPaymentDoneFor',
|
||||
{ amount: cost, user: chatLink, invoice: renderMessageLink(replyMessage!, invoiceTitle, asPreview) },
|
||||
{ withNodes: true },
|
||||
);
|
||||
}
|
||||
|
||||
case 'paymentRefunded': {
|
||||
const { currency, totalAmount, peerId } = action;
|
||||
const peer = selectPeer(global, peerId);
|
||||
const peerTitle = (peer && getPeerTitle(lang, peer)) || userFallbackText;
|
||||
const peerLink = renderPeerLink(peer?.id, peerTitle, asPreview);
|
||||
const amount = formatCurrency(lang, totalAmount, currency, { asFontIcon: true });
|
||||
|
||||
return lang('ActionPaymentRefunded', { peer: peerLink, amount }, { withNodes: true });
|
||||
}
|
||||
|
||||
case 'starGift': {
|
||||
const {
|
||||
gift, alreadyPaidUpgradeStars, peerId, savedId, fromId,
|
||||
} = action;
|
||||
const isToChannel = Boolean(peerId && savedId);
|
||||
|
||||
const fromPeer = fromId ? selectPeer(global, fromId) : sender;
|
||||
const fromTitle = (fromPeer && getPeerTitle(lang, fromPeer)) || userFallbackText;
|
||||
const fromLink = renderPeerLink(fromPeer?.id, fromTitle, asPreview);
|
||||
|
||||
const starsAmount = gift.stars + (alreadyPaidUpgradeStars || 0);
|
||||
const cost = renderStrong(formatStarsAsText(lang, starsAmount));
|
||||
|
||||
if (isToChannel) {
|
||||
const channelPeer = selectPeer(global, peerId!);
|
||||
const isYou = fromPeer?.id === currentUserId;
|
||||
|
||||
const channelTitle = (channelPeer && getPeerTitle(lang, channelPeer)) || channelFallbackText;
|
||||
const channelLink = renderPeerLink(peerId, channelTitle, asPreview);
|
||||
return translateWithOutgoing(
|
||||
lang, 'ActionStarGiftSentChannel', isYou, { user: fromLink, channel: channelLink, cost },
|
||||
);
|
||||
}
|
||||
|
||||
if (isServiceNotificationsChat) {
|
||||
return lang('ActionStarGiftReceivedAnonymous', { cost }, { withNodes: true });
|
||||
}
|
||||
|
||||
if (isSavedMessages) {
|
||||
return lang('ActionStarGiftSelfBought', { cost }, { withNodes: true });
|
||||
}
|
||||
|
||||
if (isOutgoing) {
|
||||
return lang('ActionStarGiftSent', { cost }, { withNodes: true });
|
||||
}
|
||||
|
||||
return lang('ActionStarGiftReceived', { user: senderLink, cost }, { withNodes: true });
|
||||
}
|
||||
|
||||
case 'starGiftUnique': {
|
||||
const {
|
||||
isTransferred, isUpgrade, savedId, peerId, fromId,
|
||||
} = action;
|
||||
|
||||
const isToChannel = Boolean(peerId && savedId);
|
||||
|
||||
const fromPeer = fromId ? selectPeer(global, fromId) : sender;
|
||||
const fromTitle = (fromPeer && getPeerTitle(lang, fromPeer)) || userFallbackText;
|
||||
const fromLink = renderPeerLink(fromPeer?.id, fromTitle, asPreview);
|
||||
|
||||
if (isToChannel) {
|
||||
const channelPeer = selectPeer(global, peerId!);
|
||||
const isYou = fromPeer?.id === currentUserId;
|
||||
const isAnonymous = fromPeer?.id === SERVICE_NOTIFICATIONS_USER_ID;
|
||||
|
||||
const channelTitle = (channelPeer && getPeerTitle(lang, channelPeer)) || channelFallbackText;
|
||||
const channelLink = renderPeerLink(peerId, channelTitle, asPreview);
|
||||
|
||||
if (isUpgrade) {
|
||||
return translateWithOutgoing(
|
||||
lang, 'ActionStarGiftUpgradedChannel', isYou, { user: fromLink, channel: channelLink },
|
||||
);
|
||||
}
|
||||
|
||||
if (isTransferred) {
|
||||
if (isAnonymous) {
|
||||
return lang('ActionStarGiftTransferredUnknownChannel', { channel: channelLink }, { withNodes: true });
|
||||
}
|
||||
|
||||
return translateWithOutgoing(
|
||||
lang, 'ActionStarGiftTransferredChannel', isYou, { user: fromLink, channel: channelLink },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (isSavedMessages) {
|
||||
if (isUpgrade) return lang('ActionStarGiftUpgradedSelf');
|
||||
if (isTransferred) return lang('ActionStarGiftTransferredSelf');
|
||||
}
|
||||
|
||||
if (isUpgrade) {
|
||||
if (isOutgoing) {
|
||||
return lang('ActionStarGiftUpgradedMine', { user: chatLink }, { withNodes: true });
|
||||
}
|
||||
|
||||
if (isSavedMessages) {
|
||||
return lang('ActionStarGiftUpgradedSelf');
|
||||
}
|
||||
|
||||
return lang('ActionStarGiftUpgradedUser', { user: senderLink }, { withNodes: true });
|
||||
}
|
||||
|
||||
if (isTransferred) {
|
||||
if (sender?.id === SERVICE_NOTIFICATIONS_USER_ID) {
|
||||
return lang('ActionStarGiftTransferredUnknown');
|
||||
}
|
||||
|
||||
if (isSavedMessages) {
|
||||
return lang('ActionStarGiftTransferredSelf');
|
||||
}
|
||||
|
||||
if (isOutgoing) {
|
||||
return lang('ActionStarGiftTransferredMine', { user: chatLink }, { withNodes: true });
|
||||
}
|
||||
|
||||
return lang('ActionStarGiftTransferred', { user: senderLink }, { withNodes: true });
|
||||
}
|
||||
|
||||
if (isOutgoing) {
|
||||
return lang('ActionGiftUniqueSent');
|
||||
}
|
||||
|
||||
return lang('ActionGiftUniqueReceived', { user: senderLink }, { withNodes: true });
|
||||
}
|
||||
|
||||
case 'suggestProfilePhoto': {
|
||||
const actionPeer = (isOutgoing ? chat : sender)!;
|
||||
const actionPeerLink = renderPeerLink(actionPeer.id, getPeerTitle(lang, actionPeer) || userFallbackText);
|
||||
|
||||
return translateWithOutgoing(lang, 'ActionSuggestedPhoto', isOutgoing, { user: actionPeerLink });
|
||||
}
|
||||
|
||||
case 'webViewDataSent':
|
||||
return lang('ActionWebviewDataDone', { text: action.text });
|
||||
|
||||
case 'expired': {
|
||||
const { isRoundVideo, isVoice } = action;
|
||||
if (isVoice) return lang('ActionExpiredVoice');
|
||||
if (isRoundVideo) return lang('ActionExpiredVideo');
|
||||
|
||||
return lang(UNSUPPORTED_LANG_KEY);
|
||||
}
|
||||
|
||||
case 'historyClear':
|
||||
return lang('ActionHistoryCleared');
|
||||
|
||||
case 'screenshotTaken':
|
||||
return translateWithOutgoing(lang, 'ActionScreenshotTaken', isOutgoing, { from: senderLink });
|
||||
|
||||
case 'contactSignUp':
|
||||
return lang('ActionUserRegistered', { from: senderLink }, { withNodes: true });
|
||||
|
||||
case 'customAction':
|
||||
return action.message;
|
||||
|
||||
case 'phoneCall': // Rendered as a regular message, but considered an action for the summary
|
||||
return lang(getCallMessageKey(action, isOutgoing));
|
||||
default:
|
||||
return lang(UNSUPPORTED_LANG_KEY);
|
||||
}
|
||||
});
|
||||
|
||||
return renderActionText();
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { message }): StateProps => {
|
||||
const chat = selectChat(global, message.chatId);
|
||||
const sender = selectSender(global, message);
|
||||
|
||||
const { replyToMsgId, replyToPeerId } = getMessageReplyInfo(message) || {};
|
||||
const replyMessage = replyToMsgId
|
||||
? selectChatMessage(global, replyToPeerId || message.chatId, replyToMsgId) : undefined;
|
||||
|
||||
return {
|
||||
currentUserId: global.currentUserId,
|
||||
replyMessage,
|
||||
chat,
|
||||
sender,
|
||||
};
|
||||
},
|
||||
)(ActionMessageText));
|
||||
@ -72,6 +72,7 @@ import {
|
||||
selectUser,
|
||||
selectUserStatus,
|
||||
} from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { copyTextToClipboard } from '../../../util/clipboard';
|
||||
import { getSelectionAsFormattedText } from './helpers/getSelectionAsFormattedText';
|
||||
import { isSelectionRangeInsideMessage } from './helpers/isSelectionRangeInsideMessage';
|
||||
@ -96,6 +97,7 @@ export type OwnProps = {
|
||||
noReplies?: boolean;
|
||||
detectedLanguage?: string;
|
||||
repliesThreadInfo?: ApiThreadInfo;
|
||||
className?: string;
|
||||
onClose: NoneToVoidFunction;
|
||||
onCloseAnimationEnd: NoneToVoidFunction;
|
||||
};
|
||||
@ -217,10 +219,11 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
isInSavedMessages,
|
||||
canReplyInChat,
|
||||
isWithPaidReaction,
|
||||
onClose,
|
||||
onCloseAnimationEnd,
|
||||
userFullName,
|
||||
canGift,
|
||||
className,
|
||||
onClose,
|
||||
onCloseAnimationEnd,
|
||||
}) => {
|
||||
const {
|
||||
openThread,
|
||||
@ -621,7 +624,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
scheduledMaxDate.setFullYear(scheduledMaxDate.getFullYear() + 1);
|
||||
|
||||
return (
|
||||
<div ref={containerRef} className="ContextMenuContainer">
|
||||
<div ref={containerRef} className={buildClassName('ContextMenuContainer', className)}>
|
||||
<MessageContextMenu
|
||||
isReactionPickerOpen={isReactionPickerOpen}
|
||||
availableReactions={availableReactions}
|
||||
|
||||
@ -12,6 +12,7 @@ import { formatCurrency } from '../../../util/formatCurrency';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
import getCustomAppendixBg from './helpers/getCustomAppendixBg';
|
||||
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLayoutEffectWithPrevDeps from '../../../hooks/useLayoutEffectWithPrevDeps';
|
||||
import useMedia from '../../../hooks/useMedia';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
@ -41,7 +42,8 @@ const Invoice: FC<OwnProps> = ({
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const lang = useOldLang();
|
||||
const oldLang = useOldLang();
|
||||
const lang = useLang();
|
||||
const invoice = getMessageInvoice(message);
|
||||
|
||||
const {
|
||||
@ -120,8 +122,8 @@ const Invoice: FC<OwnProps> = ({
|
||||
</div>
|
||||
)}
|
||||
<p className="description-text">
|
||||
{formatCurrency(amount, currency, lang.code, { iconClassName: 'invoice-currency-icon' })}
|
||||
{isTest && <span className="test-invoice">{lang('PaymentTestInvoice')}</span>}
|
||||
{formatCurrency(lang, amount, currency, { iconClassName: 'invoice-currency-icon' })}
|
||||
{isTest && <span className="test-invoice">{oldLang('PaymentTestInvoice')}</span>}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -43,7 +43,7 @@ import type { OnIntersectPinnedMessage } from '../hooks/usePinnedMessage';
|
||||
import { MAIN_THREAD_ID } from '../../../api/types';
|
||||
import { AudioOrigin } from '../../../types';
|
||||
|
||||
import { EMOJI_STATUS_LOOP_LIMIT } from '../../../config';
|
||||
import { EMOJI_STATUS_LOOP_LIMIT, MESSAGE_APPEARANCE_DELAY } from '../../../config';
|
||||
import {
|
||||
areReactionsEmpty,
|
||||
getIsDownloading,
|
||||
@ -311,7 +311,6 @@ type QuickReactionPosition =
|
||||
| 'in-meta';
|
||||
|
||||
const NBSP = '\u00A0';
|
||||
const APPEARANCE_DELAY = 10;
|
||||
const NO_MEDIA_CORNERS_THRESHOLD = 18;
|
||||
const QUICK_REACTION_SIZE = 1.75 * REM;
|
||||
const EXTRA_SPACE_FOR_REACTIONS = 2.25 * REM;
|
||||
@ -471,7 +470,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(markShown, appearanceOrder * APPEARANCE_DELAY);
|
||||
setTimeout(markShown, appearanceOrder * MESSAGE_APPEARANCE_DELAY);
|
||||
}, [appearanceOrder, markShown, noAppearanceAnimation]);
|
||||
|
||||
useShowTransition({
|
||||
@ -742,7 +741,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const currentTranslatedText = translatedText || previousTranslatedText;
|
||||
|
||||
const { phoneCall } = action || {};
|
||||
const phoneCall = action?.type === 'phoneCall' ? action : undefined;
|
||||
|
||||
const isMediaWithCommentButton = (repliesThreadInfo || (hasLinkedChat && isChannel && isLocal))
|
||||
&& !isInDocumentGroupNotLast
|
||||
@ -850,10 +849,19 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
animateUnreadReaction({ messageIds: [messageId] });
|
||||
}
|
||||
|
||||
let unreadMentionIds: number[] = [];
|
||||
if (message.hasUnreadMention) {
|
||||
markMentionsRead({ messageIds: [messageId] });
|
||||
unreadMentionIds = [messageId];
|
||||
}
|
||||
}, [hasUnreadReaction, messageId, animateUnreadReaction, message.hasUnreadMention]);
|
||||
|
||||
if (album) {
|
||||
unreadMentionIds = album.messages.filter((msg) => msg.hasUnreadMention).map((msg) => msg.id);
|
||||
}
|
||||
|
||||
if (unreadMentionIds.length) {
|
||||
markMentionsRead({ messageIds: unreadMentionIds });
|
||||
}
|
||||
}, [hasUnreadReaction, album, messageId, animateUnreadReaction, message.hasUnreadMention]);
|
||||
|
||||
const albumLayout = useMemo(() => {
|
||||
return isAlbum
|
||||
|
||||
@ -21,7 +21,7 @@ import type { IAnchorPosition } from '../../../types';
|
||||
|
||||
import {
|
||||
getUserFullName,
|
||||
groupStatetefulContent,
|
||||
groupStatefulContent,
|
||||
isUserId,
|
||||
} from '../../../global/helpers';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
@ -297,7 +297,7 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
|
||||
const copyOptions = getMessageCopyOptions(
|
||||
message,
|
||||
groupStatetefulContent({ poll, story }),
|
||||
groupStatefulContent({ poll, story }),
|
||||
targetHref,
|
||||
canCopy,
|
||||
handleAfterCopy,
|
||||
|
||||
@ -2,11 +2,13 @@ import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { memo, useMemo } from '../../../lib/teact/teact';
|
||||
import { getActions } from '../../../global';
|
||||
|
||||
import type { ApiMessage, PhoneCallAction } from '../../../api/types';
|
||||
import type { ApiMessage } from '../../../api/types';
|
||||
import type { ApiMessageActionPhoneCall } from '../../../api/types/messageActions';
|
||||
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { formatTime, formatTimeDuration } from '../../../util/dates/dateFormat';
|
||||
import { ARE_CALLS_SUPPORTED } from '../../../util/windowEnvironment';
|
||||
import { getCallMessageKey } from './helpers/messageActions';
|
||||
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
@ -17,7 +19,7 @@ import Button from '../../ui/Button';
|
||||
import styles from './MessagePhoneCall.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
phoneCall: PhoneCallAction;
|
||||
phoneCall: ApiMessageActionPhoneCall;
|
||||
message: ApiMessage;
|
||||
chatId: string;
|
||||
};
|
||||
@ -31,8 +33,9 @@ const MessagePhoneCall: FC<OwnProps> = ({
|
||||
|
||||
const lang = useOldLang();
|
||||
const {
|
||||
isOutgoing, isVideo, reason, duration,
|
||||
isVideo, reason, duration,
|
||||
} = phoneCall;
|
||||
const isOutgoing = message.isOutgoing;
|
||||
const isMissed = reason === 'missed';
|
||||
const isCancelled = reason === 'busy' || duration === undefined;
|
||||
|
||||
@ -40,20 +43,6 @@ const MessagePhoneCall: FC<OwnProps> = ({
|
||||
requestMasterAndRequestCall({ isVideo, userId: chatId });
|
||||
});
|
||||
|
||||
const reasonText = useMemo(() => {
|
||||
if (isVideo) {
|
||||
if (isMissed) return isOutgoing ? 'CallMessageVideoOutgoingMissed' : 'CallMessageVideoIncomingMissed';
|
||||
if (isCancelled) return 'CallMessageVideoIncomingDeclined';
|
||||
|
||||
return isOutgoing ? 'CallMessageVideoOutgoing' : 'CallMessageVideoIncoming';
|
||||
} else {
|
||||
if (isMissed) return isOutgoing ? 'CallMessageOutgoingMissed' : 'CallMessageIncomingMissed';
|
||||
if (isCancelled) return 'CallMessageIncomingDeclined';
|
||||
|
||||
return isOutgoing ? 'CallMessageOutgoing' : 'CallMessageIncoming';
|
||||
}
|
||||
}, [isCancelled, isMissed, isOutgoing, isVideo]);
|
||||
|
||||
const formattedDuration = useMemo(() => {
|
||||
return phoneCall.duration ? formatTimeDuration(lang, phoneCall.duration) : undefined;
|
||||
}, [lang, phoneCall.duration]);
|
||||
@ -74,7 +63,7 @@ const MessagePhoneCall: FC<OwnProps> = ({
|
||||
<Icon name={isVideo ? 'video-outlined' : 'phone'} />
|
||||
</Button>
|
||||
<div className={styles.info}>
|
||||
<div className={styles.reason}>{lang(reasonText)}</div>
|
||||
<div className={styles.reason}>{lang(getCallMessageKey(phoneCall, message.isOutgoing))}</div>
|
||||
<div className={styles.meta}>
|
||||
<Icon
|
||||
name="arrow-right"
|
||||
|
||||
@ -3,12 +3,13 @@ import { getActions } from '../../../global';
|
||||
|
||||
import type { ApiPaidMedia } from '../../../api/types';
|
||||
|
||||
import { STARS_CURRENCY_CODE, STARS_ICON_PLACEHOLDER } from '../../../config';
|
||||
import { STARS_ICON_PLACEHOLDER } from '../../../config';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { formatCurrency } from '../../../util/formatCurrency';
|
||||
import { formatStarsAsIcon } from '../../../util/localization/format';
|
||||
import { replaceWithTeact } from '../../../util/replaceWithTeact';
|
||||
import stopEvent from '../../../util/stopEvent';
|
||||
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
@ -33,17 +34,18 @@ const PaidMediaOverlay = ({
|
||||
children,
|
||||
}: OwnProps) => {
|
||||
const { openInvoice } = getActions();
|
||||
const lang = useOldLang();
|
||||
const oldLang = useOldLang();
|
||||
const lang = useLang();
|
||||
|
||||
const isClickable = !paidMedia.isBought;
|
||||
|
||||
const buttonText = useMemo(() => {
|
||||
const value = lang('UnlockPaidContent', paidMedia.starsAmount);
|
||||
const value = oldLang('UnlockPaidContent', paidMedia.starsAmount);
|
||||
|
||||
return replaceWithTeact(
|
||||
value, STARS_ICON_PLACEHOLDER, <StarIcon className={styles.star} type="gold" size="adaptive" />,
|
||||
);
|
||||
}, [lang, paidMedia]);
|
||||
}, [oldLang, paidMedia]);
|
||||
|
||||
const handleClick = useLastCallback((e: React.MouseEvent) => {
|
||||
openInvoice({
|
||||
@ -73,7 +75,9 @@ const PaidMediaOverlay = ({
|
||||
)}
|
||||
{paidMedia.isBought && (
|
||||
<div className={buildClassName('message-paid-media-status', styles.boughtStatus)}>
|
||||
{isOutgoing ? formatCurrency(paidMedia.starsAmount, STARS_CURRENCY_CODE) : lang('Chat.PaidMedia.Purchased')}
|
||||
{isOutgoing
|
||||
? formatStarsAsIcon(lang, paidMedia.starsAmount)
|
||||
: oldLang('Chat.PaidMedia.Purchased')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -18,10 +18,6 @@
|
||||
fill: var(--color-background);
|
||||
}
|
||||
|
||||
.join-text {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 0.375rem 0.375rem 0 0.75rem;
|
||||
display: flex;
|
||||
|
||||
@ -31,6 +31,7 @@ import styles from './SimilarChannels.module.scss';
|
||||
|
||||
const DEFAULT_BADGE_COLOR = '#3C3C4399';
|
||||
const SHOW_CHANNELS_NUMBER = 10;
|
||||
const ANIMATION_DURATION = 150;
|
||||
const MIN_SKELETON_DELAY = 300;
|
||||
const MAX_SKELETON_DELAY = 2000;
|
||||
const AUTO_EXPAND_TIME = 10; // Seconds from joining
|
||||
@ -55,12 +56,17 @@ const SimilarChannels = ({
|
||||
isCurrentUserPremium,
|
||||
channelJoinInfo,
|
||||
}: StateProps & OwnProps) => {
|
||||
const lang = useOldLang();
|
||||
const { toggleChannelRecommendations, loadChannelRecommendations } = getActions();
|
||||
|
||||
const lang = useOldLang();
|
||||
|
||||
const [isShowing, markShowing, markNotShowing] = useFlag(false);
|
||||
const [isHiding, markHiding, markNotHiding] = useFlag(false);
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const ignoreAutoScrollRef = useRef(false);
|
||||
const similarChannels = useMemo(() => {
|
||||
if (!similarChannelIds) {
|
||||
return undefined;
|
||||
@ -103,35 +109,40 @@ const SimilarChannels = ({
|
||||
return undefined;
|
||||
}, [similarChannels, isExpanded, shouldRenderSkeleton]);
|
||||
|
||||
const handleToggle = useLastCallback(() => {
|
||||
toggleChannelRecommendations({ chatId });
|
||||
useEffect(() => {
|
||||
if (isExpanded) {
|
||||
markNotShowing();
|
||||
markHiding();
|
||||
} else {
|
||||
markShowing();
|
||||
markNotHiding();
|
||||
setShouldRenderSkeleton(!similarChannelIds);
|
||||
if (!ignoreAutoScrollRef.current) {
|
||||
setTimeout(() => {
|
||||
ref.current?.scrollIntoView({ behavior: 'smooth' });
|
||||
}, ANIMATION_DURATION);
|
||||
}
|
||||
} else {
|
||||
markNotShowing();
|
||||
markHiding();
|
||||
}
|
||||
}, [isExpanded, similarChannelIds]);
|
||||
|
||||
const handleToggle = useLastCallback(() => {
|
||||
toggleChannelRecommendations({ chatId });
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!channelJoinInfo?.joinedDate || isExpanded) return;
|
||||
if (getServerTime() - channelJoinInfo.joinedDate <= AUTO_EXPAND_TIME) {
|
||||
handleToggle();
|
||||
ignoreAutoScrollRef.current = true;
|
||||
}
|
||||
}, [channelJoinInfo, isExpanded]);
|
||||
|
||||
if (!shouldRenderChannels && !shouldRenderSkeleton) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={buildClassName(styles.root)}>
|
||||
<div className="join-text">
|
||||
<span
|
||||
className={buildClassName(areSimilarChannelsPresent && styles.joinText)}
|
||||
onClick={areSimilarChannelsPresent ? handleToggle : undefined}
|
||||
>
|
||||
{lang('ChannelJoined')}
|
||||
</span>
|
||||
</div>
|
||||
{shouldRenderSkeleton && <Skeleton className={styles.skeleton} />}
|
||||
{shouldRenderChannels && (
|
||||
<div
|
||||
|
||||
@ -34,14 +34,6 @@
|
||||
border-radius: 0.25rem;
|
||||
}
|
||||
|
||||
&--unique-sticker {
|
||||
position: relative;
|
||||
width: 7.5rem;
|
||||
height: 7.5rem;
|
||||
overflow: hidden;
|
||||
margin-block: 0.5rem;
|
||||
}
|
||||
|
||||
&--stickers {
|
||||
color: var(--accent-color);
|
||||
border-radius: 0 !important;
|
||||
@ -67,17 +59,15 @@
|
||||
background-color: var(--background-active-color);
|
||||
}
|
||||
}
|
||||
|
||||
.site-title, .site-name {
|
||||
font-weight: var(--font-weight-normal);
|
||||
}
|
||||
}
|
||||
|
||||
.WebPage--content {
|
||||
position: relative;
|
||||
|
||||
&.is-gift {
|
||||
.site-title {
|
||||
font-weight: var(--font-weight-normal);
|
||||
}
|
||||
}
|
||||
|
||||
&.is-gift,
|
||||
&.is-story {
|
||||
display: flex;
|
||||
|
||||
@ -10,7 +10,6 @@ import { getMessageWebPage } from '../../../global/helpers';
|
||||
import { selectCanPlayAnimatedEmojis } from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import trimText from '../../../util/trimText';
|
||||
import { getGiftAttributes, getStickerFromGift } from '../../common/helpers/gifts';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
import { calculateMediaDimensions } from './helpers/mediaDimensions';
|
||||
import { getWebpageButtonLangKey } from './helpers/webpageType';
|
||||
@ -26,13 +25,13 @@ import Audio from '../../common/Audio';
|
||||
import Document from '../../common/Document';
|
||||
import EmojiIconBackground from '../../common/embedded/EmojiIconBackground';
|
||||
import PeerColorWrapper from '../../common/PeerColorWrapper';
|
||||
import RadialPatternBackground from '../../common/profile/RadialPatternBackground';
|
||||
import SafeLink from '../../common/SafeLink';
|
||||
import StickerView from '../../common/StickerView';
|
||||
import Button from '../../ui/Button';
|
||||
import BaseStory from './BaseStory';
|
||||
import Photo from './Photo';
|
||||
import Video from './Video';
|
||||
import WebPageUniqueGift from './WebPageUniqueGift';
|
||||
|
||||
import './WebPage.scss';
|
||||
|
||||
@ -68,7 +67,6 @@ type OwnProps = {
|
||||
type StateProps = {
|
||||
canPlayAnimatedEmojis: boolean;
|
||||
};
|
||||
const STAR_GIFT_STICKER_SIZE = 120;
|
||||
|
||||
const WebPage: FC<OwnProps & StateProps> = ({
|
||||
message,
|
||||
@ -98,8 +96,6 @@ const WebPage: FC<OwnProps & StateProps> = ({
|
||||
const { isMobile } = useAppLayout();
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const stickersRef = useRef<HTMLDivElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const giftStickersRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const oldLang = useOldLang();
|
||||
const lang = useLang();
|
||||
@ -125,7 +121,7 @@ const WebPage: FC<OwnProps & StateProps> = ({
|
||||
useEnsureStory(storyData?.peerId, storyData?.id, story);
|
||||
|
||||
const hasCustomColor = stickers?.isWithTextColor || stickers?.documents?.[0]?.shouldUseTextColor;
|
||||
const customColor = useDynamicColorListener(stickersRef, !hasCustomColor);
|
||||
const customColor = useDynamicColorListener(stickersRef, undefined, !hasCustomColor);
|
||||
|
||||
if (!webPage) {
|
||||
return undefined;
|
||||
@ -195,44 +191,6 @@ const WebPage: FC<OwnProps & StateProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
function renderStarGiftUnique() {
|
||||
const gift = webPage?.gift;
|
||||
if (!gift || gift.type !== 'starGiftUnique') return undefined;
|
||||
|
||||
const sticker = getStickerFromGift(gift)!;
|
||||
const attributes = getGiftAttributes(gift);
|
||||
const { backdrop, pattern, model } = attributes || {};
|
||||
|
||||
if (!backdrop || !pattern || !model) return undefined;
|
||||
|
||||
const backgroundColors = [backdrop.centerColor, backdrop.edgeColor];
|
||||
|
||||
return (
|
||||
<div
|
||||
className="web-page-gift web-page-centered web-page-unique"
|
||||
onClick={() => handleOpenTelegramLink()}
|
||||
>
|
||||
<div className="web-page-unique-background-wrapper">
|
||||
<RadialPatternBackground
|
||||
className="web-page-unique-background"
|
||||
backgroundColors={backgroundColors}
|
||||
patternColor={backdrop.patternColor}
|
||||
patternIcon={pattern.sticker}
|
||||
/>
|
||||
</div>
|
||||
<div ref={giftStickersRef} key={sticker.id} className="WebPage--unique-sticker">
|
||||
<StickerView
|
||||
containerRef={giftStickersRef}
|
||||
sticker={sticker}
|
||||
size={STAR_GIFT_STICKER_SIZE}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PeerColorWrapper
|
||||
className={className}
|
||||
@ -256,7 +214,12 @@ const WebPage: FC<OwnProps & StateProps> = ({
|
||||
<BaseStory story={story} isProtected={isProtected} isConnected={isConnected} isPreview />
|
||||
)}
|
||||
{isGift && !inPreview && (
|
||||
renderStarGiftUnique()
|
||||
<WebPageUniqueGift
|
||||
gift={webPage.gift!}
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
onClick={handleOpenTelegramLink}
|
||||
/>
|
||||
)}
|
||||
{isArticle && (
|
||||
<div
|
||||
|
||||
40
src/components/middle/message/WebPageUniqueGift.module.scss
Normal file
40
src/components/middle/message/WebPageUniqueGift.module.scss
Normal file
@ -0,0 +1,40 @@
|
||||
.root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
position: relative;
|
||||
min-width: 12.5rem;
|
||||
width: 100%;
|
||||
|
||||
margin-top: 0;
|
||||
padding-block: 2rem !important;
|
||||
padding-bottom: 0.75rem;
|
||||
|
||||
border-radius: 0.25rem;
|
||||
|
||||
font-weight: var(--font-weight-semibold);
|
||||
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
}
|
||||
|
||||
.backgroundWrapper {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
overflow: hidden;
|
||||
border-radius: inherit;
|
||||
}
|
||||
|
||||
.background {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
top: -1rem;
|
||||
}
|
||||
|
||||
.stickerWrapper {
|
||||
position: relative;
|
||||
width: 7.5rem;
|
||||
height: 7.5rem;
|
||||
overflow: hidden;
|
||||
margin-block: 0.5rem;
|
||||
}
|
||||
63
src/components/middle/message/WebPageUniqueGift.tsx
Normal file
63
src/components/middle/message/WebPageUniqueGift.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import React, { memo, useRef } from '../../../lib/teact/teact';
|
||||
|
||||
import type { ApiStarGiftUnique } from '../../../api/types';
|
||||
|
||||
import { getGiftAttributes } from '../../common/helpers/gifts';
|
||||
|
||||
import { type ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
|
||||
import RadialPatternBackground from '../../common/profile/RadialPatternBackground';
|
||||
import StickerView from '../../common/StickerView';
|
||||
|
||||
import styles from './WebPageUniqueGift.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
gift: ApiStarGiftUnique;
|
||||
observeIntersectionForLoading?: ObserveFn;
|
||||
observeIntersectionForPlaying?: ObserveFn;
|
||||
onClick?: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
const STAR_GIFT_STICKER_SIZE = 120;
|
||||
|
||||
const WebPageUniqueGift = ({
|
||||
gift,
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
onClick,
|
||||
}: OwnProps) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const stickerRef = useRef<HTMLDivElement>(null);
|
||||
const {
|
||||
backdrop, model, pattern,
|
||||
} = getGiftAttributes(gift)!;
|
||||
|
||||
const backgroundColors = [backdrop!.centerColor, backdrop!.edgeColor];
|
||||
|
||||
return (
|
||||
<div
|
||||
className={styles.root}
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className={styles.backgroundWrapper}>
|
||||
<RadialPatternBackground
|
||||
className={styles.background}
|
||||
backgroundColors={backgroundColors}
|
||||
patternColor={backdrop!.patternColor}
|
||||
patternIcon={pattern!.sticker}
|
||||
/>
|
||||
</div>
|
||||
<div ref={stickerRef} className={styles.stickerWrapper}>
|
||||
<StickerView
|
||||
containerRef={stickerRef}
|
||||
sticker={model!.sticker}
|
||||
size={STAR_GIFT_STICKER_SIZE}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(WebPageUniqueGift);
|
||||
39
src/components/middle/message/actions/ChannelPhoto.tsx
Normal file
39
src/components/middle/message/actions/ChannelPhoto.tsx
Normal file
@ -0,0 +1,39 @@
|
||||
import React, { memo } from '../../../../lib/teact/teact';
|
||||
|
||||
import type { ApiMessageActionChatEditPhoto } from '../../../../api/types/messageActions';
|
||||
|
||||
import { REM } from '../../../common/helpers/mediaDimensions';
|
||||
|
||||
import { type ObserveFn } from '../../../../hooks/useIntersectionObserver';
|
||||
|
||||
import Avatar from '../../../common/Avatar';
|
||||
|
||||
import styles from '../ActionMessage.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
action: ApiMessageActionChatEditPhoto;
|
||||
observeIntersection?: ObserveFn;
|
||||
onClick?: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
const AVATAR_SIZE = 15 * REM;
|
||||
|
||||
const ChannelPhotoAction = ({
|
||||
action,
|
||||
onClick,
|
||||
observeIntersection,
|
||||
} : OwnProps) => {
|
||||
return (
|
||||
<Avatar
|
||||
className={styles.channelPhoto}
|
||||
photo={action.photo}
|
||||
loopIndefinitely
|
||||
withVideo
|
||||
observeIntersection={observeIntersection}
|
||||
onClick={onClick}
|
||||
size={AVATAR_SIZE}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(ChannelPhotoAction);
|
||||
100
src/components/middle/message/actions/Gift.tsx
Normal file
100
src/components/middle/message/actions/Gift.tsx
Normal file
@ -0,0 +1,100 @@
|
||||
import React, { memo, useRef } from '../../../../lib/teact/teact';
|
||||
import { withGlobal } from '../../../../global';
|
||||
|
||||
import type { ApiSticker } from '../../../../api/types';
|
||||
import type { ApiMessageActionGiftPremium, ApiMessageActionGiftStars } from '../../../../api/types/messageActions';
|
||||
|
||||
import {
|
||||
selectCanPlayAnimatedEmojis,
|
||||
selectGiftStickerForDuration,
|
||||
selectGiftStickerForStars,
|
||||
} from '../../../../global/selectors';
|
||||
import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities';
|
||||
|
||||
import { type ObserveFn } from '../../../../hooks/useIntersectionObserver';
|
||||
import useLang from '../../../../hooks/useLang';
|
||||
|
||||
import Sparkles from '../../../common/Sparkles';
|
||||
import StickerView from '../../../common/StickerView';
|
||||
|
||||
import styles from '../ActionMessage.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
action: ApiMessageActionGiftPremium | ApiMessageActionGiftStars;
|
||||
observeIntersectionForLoading?: ObserveFn;
|
||||
observeIntersectionForPlaying?: ObserveFn;
|
||||
onClick?: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
sticker?: ApiSticker;
|
||||
canPlayAnimatedEmojis: boolean;
|
||||
};
|
||||
|
||||
const STICKER_SIZE = 150;
|
||||
|
||||
const GiftAction = ({
|
||||
action,
|
||||
sticker,
|
||||
canPlayAnimatedEmojis,
|
||||
onClick,
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
}: OwnProps & StateProps) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const stickerRef = useRef<HTMLDivElement>(null);
|
||||
const lang = useLang();
|
||||
const message = action.type === 'giftPremium' ? action.message : undefined;
|
||||
|
||||
return (
|
||||
<div className={styles.contentBox} tabIndex={0} role="button" onClick={onClick}>
|
||||
<div
|
||||
ref={stickerRef}
|
||||
className={styles.stickerWrapper}
|
||||
style={`width: ${STICKER_SIZE}px; height: ${STICKER_SIZE}px`}
|
||||
>
|
||||
{sticker && (
|
||||
<StickerView
|
||||
containerRef={stickerRef}
|
||||
sticker={sticker}
|
||||
size={STICKER_SIZE}
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
noLoad={!canPlayAnimatedEmojis}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.info}>
|
||||
<h3 className={styles.title}>
|
||||
{action.type === 'giftPremium' ? (
|
||||
lang('ActionGiftPremiumTitle', { months: action.months }, { pluralValue: action.months })
|
||||
) : (
|
||||
lang('ActionGiftStarsTitle', { amount: action.stars }, { pluralValue: action.stars })
|
||||
)}
|
||||
</h3>
|
||||
<div>
|
||||
{message && renderTextWithEntities(message)}
|
||||
{!message && (lang(action.type === 'giftPremium' ? 'ActionGiftPremiumText' : 'ActionGiftStarsText'))}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.actionButton}>
|
||||
<Sparkles preset="button" />
|
||||
{lang('ActionViewButton')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { action }): StateProps => {
|
||||
const sticker = action.type === 'giftPremium'
|
||||
? selectGiftStickerForDuration(global, action.months)
|
||||
: selectGiftStickerForStars(global, action.stars);
|
||||
const canPlayAnimatedEmojis = selectCanPlayAnimatedEmojis(global);
|
||||
|
||||
return {
|
||||
sticker,
|
||||
canPlayAnimatedEmojis,
|
||||
};
|
||||
},
|
||||
)(GiftAction));
|
||||
129
src/components/middle/message/actions/GiveawayPrize.tsx
Normal file
129
src/components/middle/message/actions/GiveawayPrize.tsx
Normal file
@ -0,0 +1,129 @@
|
||||
import React, { memo, useMemo, useRef } from '../../../../lib/teact/teact';
|
||||
import { withGlobal } from '../../../../global';
|
||||
|
||||
import type { ApiChat, ApiSticker } from '../../../../api/types';
|
||||
import type { ApiMessageActionGiftCode, ApiMessageActionPrizeStars } from '../../../../api/types/messageActions';
|
||||
|
||||
import { getPeerTitle } from '../../../../global/helpers';
|
||||
import {
|
||||
selectCanPlayAnimatedEmojis,
|
||||
selectChat,
|
||||
selectGiftStickerForDuration,
|
||||
selectGiftStickerForStars,
|
||||
} from '../../../../global/selectors';
|
||||
import { renderPeerLink } from '../helpers/messageActions';
|
||||
|
||||
import { type ObserveFn } from '../../../../hooks/useIntersectionObserver';
|
||||
import useLang from '../../../../hooks/useLang';
|
||||
|
||||
import Sparkles from '../../../common/Sparkles';
|
||||
import StickerView from '../../../common/StickerView';
|
||||
|
||||
import styles from '../ActionMessage.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
action: ApiMessageActionGiftCode | ApiMessageActionPrizeStars;
|
||||
observeIntersectionForLoading?: ObserveFn;
|
||||
observeIntersectionForPlaying?: ObserveFn;
|
||||
onClick?: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
channel?: ApiChat;
|
||||
sticker?: ApiSticker;
|
||||
canPlayAnimatedEmojis: boolean;
|
||||
};
|
||||
|
||||
const STICKER_SIZE = 150;
|
||||
|
||||
const GiveawayPrizeAction = ({
|
||||
action,
|
||||
sticker,
|
||||
canPlayAnimatedEmojis,
|
||||
channel,
|
||||
onClick,
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
}: OwnProps & StateProps) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const stickerRef = useRef<HTMLDivElement>(null);
|
||||
const lang = useLang();
|
||||
|
||||
const channelLink = useMemo(() => {
|
||||
const channelTitle = channel && getPeerTitle(lang, channel);
|
||||
const channelFallbackText = lang('ActionFallbackChannel');
|
||||
|
||||
return renderPeerLink(channel?.id, channelTitle || channelFallbackText);
|
||||
}, [channel, lang]);
|
||||
|
||||
return (
|
||||
<div className={styles.contentBox} tabIndex={0} role="button" onClick={onClick}>
|
||||
<div
|
||||
ref={stickerRef}
|
||||
className={styles.stickerWrapper}
|
||||
style={`width: ${STICKER_SIZE}px; height: ${STICKER_SIZE}px`}
|
||||
>
|
||||
{sticker && (
|
||||
<StickerView
|
||||
containerRef={stickerRef}
|
||||
sticker={sticker}
|
||||
size={STICKER_SIZE}
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
noLoad={!canPlayAnimatedEmojis}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div>
|
||||
<h3 className={styles.title}>{lang('ActionGiveawayResultTitle')}</h3>
|
||||
<div>
|
||||
{action.type === 'giftCode' && (
|
||||
lang(
|
||||
action.isViaGiveaway ? 'ActionGiveawayResultPremiumText' : 'ActionGiftCodePremiumText',
|
||||
{ months: action.months, channel: channelLink },
|
||||
{
|
||||
withNodes: true,
|
||||
withMarkdown: true,
|
||||
pluralValue: action.months,
|
||||
renderTextFilters: ['br'],
|
||||
},
|
||||
)
|
||||
)}
|
||||
{action.type === 'prizeStars' && (
|
||||
lang(
|
||||
'ActionGiveawayResultStarsText',
|
||||
{ amount: action.stars, channel: channelLink },
|
||||
{
|
||||
withNodes: true,
|
||||
withMarkdown: true,
|
||||
pluralValue: action.stars,
|
||||
renderTextFilters: ['br'],
|
||||
},
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.actionButton}>
|
||||
<Sparkles preset="button" />
|
||||
{lang(action.type === 'giftCode' ? 'ActionOpenGiftButton' : 'ActionViewButton')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { action }): StateProps => {
|
||||
const sticker = action.type === 'giftCode'
|
||||
? selectGiftStickerForDuration(global, action.months)
|
||||
: selectGiftStickerForStars(global, action.stars);
|
||||
const canPlayAnimatedEmojis = selectCanPlayAnimatedEmojis(global);
|
||||
|
||||
const channel = selectChat(global, action.boostPeerId!);
|
||||
|
||||
return {
|
||||
sticker,
|
||||
canPlayAnimatedEmojis,
|
||||
channel,
|
||||
};
|
||||
},
|
||||
)(GiveawayPrizeAction));
|
||||
190
src/components/middle/message/actions/StarGift.tsx
Normal file
190
src/components/middle/message/actions/StarGift.tsx
Normal file
@ -0,0 +1,190 @@
|
||||
import React, { memo, useMemo, useRef } from '../../../../lib/teact/teact';
|
||||
import { withGlobal } from '../../../../global';
|
||||
|
||||
import type { ApiMessage, ApiPeer } from '../../../../api/types';
|
||||
import type { ApiMessageActionStarGift } from '../../../../api/types/messageActions';
|
||||
|
||||
import { getPeerTitle, isChatChannel } from '../../../../global/helpers';
|
||||
import { isApiPeerChat } from '../../../../global/helpers/peers';
|
||||
import {
|
||||
selectCanPlayAnimatedEmojis,
|
||||
selectPeer,
|
||||
selectSender,
|
||||
} from '../../../../global/selectors';
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
import { formatStarsAsText } from '../../../../util/localization/format';
|
||||
import { getServerTime } from '../../../../util/serverTime';
|
||||
import { formatIntegerCompact } from '../../../../util/textFormat';
|
||||
import { getStickerFromGift } from '../../../common/helpers/gifts';
|
||||
import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities';
|
||||
import { renderPeerLink, translateWithOutgoing } from '../helpers/messageActions';
|
||||
|
||||
import useDynamicColorListener from '../../../../hooks/stickers/useDynamicColorListener';
|
||||
import { type ObserveFn } from '../../../../hooks/useIntersectionObserver';
|
||||
import useLang from '../../../../hooks/useLang';
|
||||
|
||||
import GiftRibbon from '../../../common/gift/GiftRibbon';
|
||||
import Sparkles from '../../../common/Sparkles';
|
||||
import StickerView from '../../../common/StickerView';
|
||||
|
||||
import styles from '../ActionMessage.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
message: ApiMessage;
|
||||
action: ApiMessageActionStarGift;
|
||||
observeIntersectionForLoading?: ObserveFn;
|
||||
observeIntersectionForPlaying?: ObserveFn;
|
||||
onClick?: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
canPlayAnimatedEmojis: boolean;
|
||||
sender?: ApiPeer;
|
||||
recipient?: ApiPeer;
|
||||
starGiftMaxConvertPeriod?: number;
|
||||
};
|
||||
|
||||
const STICKER_SIZE = 120;
|
||||
|
||||
const StarGiftAction = ({
|
||||
action,
|
||||
message,
|
||||
canPlayAnimatedEmojis,
|
||||
sender,
|
||||
recipient,
|
||||
starGiftMaxConvertPeriod,
|
||||
onClick,
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
}: OwnProps & StateProps) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const stickerRef = useRef<HTMLDivElement>(null);
|
||||
const lang = useLang();
|
||||
|
||||
const { isOutgoing } = message;
|
||||
|
||||
const sticker = getStickerFromGift(action.gift)!;
|
||||
|
||||
const peer = isOutgoing ? recipient : sender;
|
||||
const isChannel = peer && isApiPeerChat(peer) && isChatChannel(peer);
|
||||
|
||||
const backgroundColor = useDynamicColorListener(ref, 'background-color', !action.gift.availabilityTotal);
|
||||
|
||||
const fallbackPeerTitle = lang('ActionFallbackSomeone');
|
||||
const peerTitle = peer && getPeerTitle(lang, peer);
|
||||
const isSelf = sender?.id === recipient?.id;
|
||||
|
||||
const giftDescription = useMemo(() => {
|
||||
const peerLink = renderPeerLink(peer?.id, peerTitle || fallbackPeerTitle);
|
||||
const starsAmount = action.starsToConvert !== undefined
|
||||
? formatStarsAsText(lang, action.starsToConvert) : undefined;
|
||||
|
||||
if (action.isUpgraded) {
|
||||
return lang('ActionStarGiftUpgraded');
|
||||
}
|
||||
|
||||
if (action.alreadyPaidUpgradeStars) {
|
||||
return translateWithOutgoing(
|
||||
lang, 'ActionStarGiftUpgradeText', !isOutgoing, { peer: peerLink },
|
||||
);
|
||||
}
|
||||
|
||||
if (action.isConverted) {
|
||||
return translateWithOutgoing(
|
||||
lang, 'ActionStarGiftConvertedText', !isOutgoing, { peer: peerLink, amount: starsAmount },
|
||||
);
|
||||
}
|
||||
|
||||
if (starGiftMaxConvertPeriod && getServerTime() < message.date + starGiftMaxConvertPeriod) {
|
||||
return translateWithOutgoing(
|
||||
lang, 'ActionStarGiftConvertText', !isOutgoing, { peer: peerLink, amount: starsAmount },
|
||||
);
|
||||
}
|
||||
|
||||
if (isChannel) {
|
||||
return lang(
|
||||
'ActionStarGiftChannelText', { amount: starsAmount }, { withNodes: true },
|
||||
);
|
||||
}
|
||||
|
||||
return translateWithOutgoing(
|
||||
lang, 'ActionStarGiftNoConvertText', !isOutgoing, { peer: peerLink },
|
||||
);
|
||||
}, [
|
||||
action, fallbackPeerTitle, isChannel, isOutgoing, lang, message.date, peer?.id, peerTitle, starGiftMaxConvertPeriod,
|
||||
]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={buildClassName(styles.contentBox, styles.starGift)}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
onClick={onClick}
|
||||
>
|
||||
<div
|
||||
ref={stickerRef}
|
||||
className={styles.stickerWrapper}
|
||||
style={`width: ${STICKER_SIZE}px; height: ${STICKER_SIZE}px`}
|
||||
>
|
||||
{sticker && (
|
||||
<StickerView
|
||||
containerRef={stickerRef}
|
||||
sticker={sticker}
|
||||
size={STICKER_SIZE}
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
noLoad={!canPlayAnimatedEmojis}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{action.gift.availabilityTotal && (
|
||||
<GiftRibbon
|
||||
color={backgroundColor || 'blue'}
|
||||
text={lang('ActionStarGiftLimitedRibbon', { total: formatIntegerCompact(action.gift.availabilityTotal) })}
|
||||
/>
|
||||
)}
|
||||
<div className={styles.info}>
|
||||
<h3 className={styles.title}>
|
||||
{isSelf ? lang('ActionStarGiftSelf') : lang(
|
||||
isOutgoing ? 'ActionStarGiftTo' : 'ActionStarGiftFrom',
|
||||
{
|
||||
peer: renderPeerLink(peer?.id, peerTitle || fallbackPeerTitle),
|
||||
},
|
||||
{
|
||||
withNodes: true,
|
||||
},
|
||||
)}
|
||||
</h3>
|
||||
<div className={styles.subtitle}>
|
||||
{action.message && renderTextWithEntities(action.message)}
|
||||
{!action.message && giftDescription}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.actionButton}>
|
||||
<Sparkles preset="button" />
|
||||
{action.alreadyPaidUpgradeStars && !action.isUpgraded && !isOutgoing
|
||||
? lang('ActionStarGiftUnpack') : lang('ActionViewButton')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { message, action }): StateProps => {
|
||||
const canPlayAnimatedEmojis = selectCanPlayAnimatedEmojis(global);
|
||||
const messageSender = selectSender(global, message);
|
||||
const giftSender = action.fromId ? selectPeer(global, action.fromId) : undefined;
|
||||
const messageRecipient = selectPeer(global, message.chatId);
|
||||
const giftRecipient = action.peerId ? selectPeer(global, action.peerId) : undefined;
|
||||
|
||||
return {
|
||||
canPlayAnimatedEmojis,
|
||||
sender: giftSender || messageSender,
|
||||
recipient: giftRecipient || messageRecipient,
|
||||
starGiftMaxConvertPeriod: global.appConfig?.starGiftMaxConvertPeriod,
|
||||
};
|
||||
},
|
||||
)(StarGiftAction));
|
||||
159
src/components/middle/message/actions/StarGiftUnique.tsx
Normal file
159
src/components/middle/message/actions/StarGiftUnique.tsx
Normal file
@ -0,0 +1,159 @@
|
||||
import React, { memo, useMemo, useRef } from '../../../../lib/teact/teact';
|
||||
import { withGlobal } from '../../../../global';
|
||||
|
||||
import type { ApiMessage, ApiPeer } from '../../../../api/types';
|
||||
import type { ApiMessageActionStarGiftUnique } from '../../../../api/types/messageActions';
|
||||
|
||||
import { getPeerTitle } from '../../../../global/helpers';
|
||||
import {
|
||||
selectCanPlayAnimatedEmojis,
|
||||
selectPeer,
|
||||
selectSender,
|
||||
} from '../../../../global/selectors';
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
import buildStyle from '../../../../util/buildStyle';
|
||||
import { getGiftAttributes, getStickerFromGift } from '../../../common/helpers/gifts';
|
||||
import { renderPeerLink } from '../helpers/messageActions';
|
||||
|
||||
import { type ObserveFn } from '../../../../hooks/useIntersectionObserver';
|
||||
import useLang from '../../../../hooks/useLang';
|
||||
|
||||
import GiftRibbon from '../../../common/gift/GiftRibbon';
|
||||
import MiniTable, { type TableEntry } from '../../../common/MiniTable';
|
||||
import RadialPatternBackground from '../../../common/profile/RadialPatternBackground';
|
||||
import Sparkles from '../../../common/Sparkles';
|
||||
import StickerView from '../../../common/StickerView';
|
||||
|
||||
import styles from '../ActionMessage.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
message: ApiMessage;
|
||||
action: ApiMessageActionStarGiftUnique;
|
||||
observeIntersectionForLoading?: ObserveFn;
|
||||
observeIntersectionForPlaying?: ObserveFn;
|
||||
onClick?: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
canPlayAnimatedEmojis: boolean;
|
||||
sender?: ApiPeer;
|
||||
recipient?: ApiPeer;
|
||||
};
|
||||
|
||||
const STICKER_SIZE = 120;
|
||||
|
||||
const StarGiftAction = ({
|
||||
action,
|
||||
message,
|
||||
canPlayAnimatedEmojis,
|
||||
sender,
|
||||
recipient,
|
||||
onClick,
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
}: OwnProps & StateProps) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const stickerRef = useRef<HTMLDivElement>(null);
|
||||
const lang = useLang();
|
||||
|
||||
const { isOutgoing } = message;
|
||||
|
||||
const sticker = getStickerFromGift(action.gift)!;
|
||||
const attributes = getGiftAttributes(action.gift)!;
|
||||
const model = attributes.model!;
|
||||
const pattern = attributes.pattern!;
|
||||
const backdrop = attributes.backdrop!;
|
||||
const backgroundColors = [backdrop.centerColor, backdrop.edgeColor];
|
||||
const adaptedPatternColor = `${backdrop.patternColor.slice(0, 7)}55`;
|
||||
|
||||
const tableData = useMemo((): TableEntry[] => [
|
||||
[lang('ActionStarGiftUniqueModel'), model.name],
|
||||
[lang('ActionStarGiftUniqueBackdrop'), backdrop.name],
|
||||
[lang('ActionStarGiftUniqueSymbol'), pattern.name],
|
||||
], [lang, model, pattern, backdrop]);
|
||||
|
||||
const peer = isOutgoing ? recipient : sender;
|
||||
|
||||
const fallbackPeerTitle = lang('ActionFallbackSomeone');
|
||||
const peerTitle = peer && getPeerTitle(lang, peer);
|
||||
const isSelf = sender?.id === recipient?.id;
|
||||
|
||||
return (
|
||||
<div
|
||||
className={buildClassName(styles.contentBox, styles.starGift, styles.uniqueGift)}
|
||||
tabIndex={0}
|
||||
role="button"
|
||||
onClick={onClick}
|
||||
>
|
||||
<div className={styles.uniqueBackgroundWrapper}>
|
||||
<RadialPatternBackground
|
||||
className={styles.uniqueBackground}
|
||||
backgroundColors={backgroundColors}
|
||||
patternColor={backdrop.patternColor}
|
||||
patternIcon={pattern.sticker}
|
||||
clearBottomSector
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
ref={stickerRef}
|
||||
className={styles.stickerWrapper}
|
||||
style={`width: ${STICKER_SIZE}px; height: ${STICKER_SIZE}px`}
|
||||
>
|
||||
{sticker && (
|
||||
<StickerView
|
||||
containerRef={stickerRef}
|
||||
sticker={sticker}
|
||||
size={STICKER_SIZE}
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
noLoad={!canPlayAnimatedEmojis}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<GiftRibbon
|
||||
color={adaptedPatternColor}
|
||||
text={lang('ActionStarGiftUniqueRibbon')}
|
||||
/>
|
||||
<div className={styles.info}>
|
||||
<h3 className={styles.title}>
|
||||
{isSelf ? lang('ActionStarGiftSelf') : lang(
|
||||
isOutgoing ? 'ActionStarGiftTo' : 'ActionStarGiftFrom',
|
||||
{
|
||||
peer: renderPeerLink(peer?.id, peerTitle || fallbackPeerTitle),
|
||||
},
|
||||
{
|
||||
withNodes: true,
|
||||
},
|
||||
)}
|
||||
</h3>
|
||||
<div className={styles.subtitle} style={`color: ${backdrop.textColor}`}>
|
||||
{lang('GiftUnique', { title: action.gift.title, number: action.gift.number })}
|
||||
</div>
|
||||
<MiniTable data={tableData} style={`color: ${backdrop.textColor}`} valueClassName={styles.uniqueValue} />
|
||||
</div>
|
||||
<div
|
||||
className={styles.actionButton}
|
||||
style={buildStyle(adaptedPatternColor && `background-color: ${adaptedPatternColor}`)}
|
||||
>
|
||||
<Sparkles preset="button" />
|
||||
{lang('ActionViewButton')}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { message, action }): StateProps => {
|
||||
const canPlayAnimatedEmojis = selectCanPlayAnimatedEmojis(global);
|
||||
const messageSender = selectSender(global, message);
|
||||
const giftSender = action.fromId ? selectPeer(global, action.fromId) : undefined;
|
||||
const messageRecipient = selectPeer(global, message.chatId);
|
||||
const giftRecipient = action.peerId ? selectPeer(global, action.peerId) : undefined;
|
||||
|
||||
return {
|
||||
canPlayAnimatedEmojis,
|
||||
sender: giftSender || messageSender,
|
||||
recipient: giftRecipient || messageRecipient,
|
||||
};
|
||||
},
|
||||
)(StarGiftAction));
|
||||
160
src/components/middle/message/actions/SuggestedPhoto.tsx
Normal file
160
src/components/middle/message/actions/SuggestedPhoto.tsx
Normal file
@ -0,0 +1,160 @@
|
||||
import React, { memo, useMemo, useState } from '../../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../../global';
|
||||
|
||||
import type { ApiMessageActionSuggestProfilePhoto } from '../../../../api/types/messageActions';
|
||||
import { type ApiMessage, type ApiPeer, MAIN_THREAD_ID } from '../../../../api/types';
|
||||
import { MediaViewerOrigin, SettingsScreens } from '../../../../types';
|
||||
|
||||
import { getPeerTitle, getPhotoMediaHash, getVideoProfilePhotoMediaHash } from '../../../../global/helpers';
|
||||
import { selectPeer } from '../../../../global/selectors';
|
||||
import { fetchBlob } from '../../../../util/files';
|
||||
import { renderPeerLink } from '../helpers/messageActions';
|
||||
|
||||
import useFlag from '../../../../hooks/useFlag';
|
||||
import { type ObserveFn } from '../../../../hooks/useIntersectionObserver';
|
||||
import useLang from '../../../../hooks/useLang';
|
||||
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||
import useMedia from '../../../../hooks/useMedia';
|
||||
|
||||
import Avatar from '../../../common/Avatar';
|
||||
import ConfirmDialog from '../../../ui/ConfirmDialog';
|
||||
import CropModal from '../../../ui/CropModal';
|
||||
|
||||
import styles from '../ActionMessage.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
message: ApiMessage;
|
||||
action: ApiMessageActionSuggestProfilePhoto;
|
||||
observeIntersection?: ObserveFn;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
peer?: ApiPeer;
|
||||
};
|
||||
|
||||
const SuggestedPhotoAction = ({
|
||||
message,
|
||||
action,
|
||||
peer,
|
||||
observeIntersection,
|
||||
} : OwnProps & StateProps) => {
|
||||
const { openMediaViewer, uploadProfilePhoto, showNotification } = getActions();
|
||||
const { isOutgoing } = message;
|
||||
const photo = action.photo;
|
||||
|
||||
const lang = useLang();
|
||||
const [cropModalBlob, setCropModalBlob] = useState<Blob | undefined>();
|
||||
const [isVideoModalOpen, openVideoModal, closeVideoModal] = useFlag(false);
|
||||
|
||||
const suggestedPhotoUrl = useMedia(getPhotoMediaHash(photo, 'full'));
|
||||
const suggestedVideoUrl = useMedia(getVideoProfilePhotoMediaHash(photo));
|
||||
const isVideo = photo.isVideo;
|
||||
|
||||
const text = useMemo(() => {
|
||||
const peerName = (peer && getPeerTitle(lang, peer)) || lang('ActionFallbackUser');
|
||||
const peerLink = renderPeerLink(peer?.id, peerName);
|
||||
|
||||
if (isOutgoing) {
|
||||
return lang('ActionSuggestedPhotoYou', { user: peerLink }, { withNodes: true });
|
||||
}
|
||||
|
||||
return lang('ActionSuggestedPhoto', { user: peerLink }, { withNodes: true });
|
||||
}, [lang, isOutgoing, peer]);
|
||||
|
||||
const showAvatarNotification = useLastCallback(() => {
|
||||
showNotification({
|
||||
title: lang('ActionSuggestedPhotoUpdatedTitle'),
|
||||
message: lang('ActionSuggestedPhotoUpdatedDescription'),
|
||||
action: {
|
||||
action: 'requestNextSettingsScreen',
|
||||
payload: {
|
||||
screen: SettingsScreens.Main,
|
||||
},
|
||||
},
|
||||
actionText: lang('Open'),
|
||||
});
|
||||
});
|
||||
|
||||
const handleSetSuggestedAvatar = useLastCallback((file: File) => {
|
||||
setCropModalBlob(undefined);
|
||||
uploadProfilePhoto({ file });
|
||||
showAvatarNotification();
|
||||
});
|
||||
|
||||
const handleCloseCropModal = useLastCallback(() => {
|
||||
setCropModalBlob(undefined);
|
||||
});
|
||||
|
||||
const handleSetVideo = useLastCallback(async () => {
|
||||
if (!suggestedVideoUrl) return;
|
||||
|
||||
closeVideoModal();
|
||||
showAvatarNotification();
|
||||
|
||||
// TODO Once we support uploading video avatars, add crop/trim modal here
|
||||
const blob = await fetchBlob(suggestedVideoUrl);
|
||||
uploadProfilePhoto({
|
||||
file: new File([blob], 'avatar.mp4'),
|
||||
isVideo: true,
|
||||
videoTs: photo.videoSizes?.find((l) => l.videoStartTs !== undefined)?.videoStartTs,
|
||||
});
|
||||
});
|
||||
|
||||
const handleViewSuggestedAvatar = async () => {
|
||||
if (!isOutgoing && suggestedPhotoUrl) {
|
||||
if (isVideo) {
|
||||
openVideoModal();
|
||||
} else {
|
||||
setCropModalBlob(await fetchBlob(suggestedPhotoUrl));
|
||||
}
|
||||
} else {
|
||||
openMediaViewer({
|
||||
chatId: message.chatId,
|
||||
messageId: message.id,
|
||||
threadId: MAIN_THREAD_ID,
|
||||
origin: MediaViewerOrigin.SuggestedAvatar,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={styles.contentBox} tabIndex={0} role="button" onClick={handleViewSuggestedAvatar}>
|
||||
<Avatar
|
||||
className={styles.suggestedAvatar}
|
||||
photo={action.photo}
|
||||
loopIndefinitely
|
||||
withVideo
|
||||
observeIntersection={observeIntersection}
|
||||
size="jumbo"
|
||||
/>
|
||||
<div className={styles.suggestedText}>
|
||||
{text}
|
||||
</div>
|
||||
<div className={styles.actionButton}>
|
||||
{lang('ActionSuggestedPhotoButton')}
|
||||
</div>
|
||||
<CropModal
|
||||
file={cropModalBlob}
|
||||
onClose={handleCloseCropModal}
|
||||
onChange={handleSetSuggestedAvatar}
|
||||
/>
|
||||
<ConfirmDialog
|
||||
isOpen={isVideoModalOpen}
|
||||
title={lang('ActionSuggestedVideoTitle')}
|
||||
confirmHandler={handleSetVideo}
|
||||
onClose={closeVideoModal}
|
||||
text={lang('ActionSuggestedVideoText')}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { message }): StateProps => {
|
||||
const peer = selectPeer(global, message.chatId);
|
||||
|
||||
return {
|
||||
peer,
|
||||
};
|
||||
},
|
||||
)(SuggestedPhotoAction));
|
||||
130
src/components/middle/message/helpers/messageActions.tsx
Normal file
130
src/components/middle/message/helpers/messageActions.tsx
Normal file
@ -0,0 +1,130 @@
|
||||
import React, { type TeactNode } from '../../../../lib/teact/teact';
|
||||
import { getActions } from '../../../../global';
|
||||
|
||||
import type { ApiMessage } from '../../../../api/types';
|
||||
import type { ApiMessageActionPhoneCall } from '../../../../api/types/messageActions';
|
||||
import type {
|
||||
LangKey,
|
||||
LangPairPluralWithVariables,
|
||||
LangPairWithVariables,
|
||||
PluralLangKeyWithVariables,
|
||||
RegularLangKey,
|
||||
RegularLangKeyWithVariables,
|
||||
} from '../../../../types/language';
|
||||
import type { LangFn } from '../../../../util/localization';
|
||||
|
||||
import { getMessageContent } from '../../../../global/helpers';
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
import { IS_SAFARI } from '../../../../util/windowEnvironment';
|
||||
import renderText from '../../../common/helpers/renderText';
|
||||
|
||||
import Link from '../../../ui/Link';
|
||||
|
||||
import styles from '../ActionMessage.module.scss';
|
||||
|
||||
type SuffixKey<T, K extends keyof T> = `${K & string}You` extends keyof T ? T[`${K & string}You`] : never;
|
||||
type VariablesForKey<K extends LangKey> =
|
||||
K extends RegularLangKeyWithVariables
|
||||
? LangPairWithVariables<TeactNode | undefined>[K] | SuffixKey<LangPairWithVariables, K>
|
||||
: K extends PluralLangKeyWithVariables
|
||||
? LangPairPluralWithVariables<TeactNode | undefined>[K] | SuffixKey<LangPairPluralWithVariables, K>
|
||||
: undefined;
|
||||
|
||||
export function translateWithOutgoing<K extends LangKey>(
|
||||
lang: LangFn,
|
||||
key: K,
|
||||
isOutgoing: boolean,
|
||||
variables: VariablesForKey<K>,
|
||||
options?: { pluralValue?: number; asText?: boolean; isMarkdown?: boolean },
|
||||
): TeactNode {
|
||||
const { pluralValue, asText, isMarkdown } = options || {};
|
||||
const translationKey = isOutgoing ? (`${key}You` as LangKey) : key;
|
||||
|
||||
return lang(
|
||||
// @ts-ignore -- I have no idea if this even possible to type correctly
|
||||
translationKey,
|
||||
variables,
|
||||
{ withNodes: !asText, isMarkdown, pluralValue },
|
||||
);
|
||||
}
|
||||
|
||||
export function getPinnedMediaValue(lang: LangFn, message: ApiMessage) {
|
||||
const {
|
||||
audio, contact, document, game, giveaway, giveawayResults, paidMedia, storyData,
|
||||
invoice, location, photo, pollId, sticker, video, voice,
|
||||
} = getMessageContent(message);
|
||||
|
||||
if (message.groupedId || paidMedia) return lang('ActionPinnedMediaAlbum');
|
||||
if (photo) return lang('ActionPinnedMediaPhoto');
|
||||
if (audio) return lang('ActionPinnedMediaAudio');
|
||||
if (voice) return lang('ActionPinnedMediaVoice');
|
||||
if (video?.isRound) return lang('ActionPinnedMediaVideoMessage');
|
||||
if (video?.isGif) return lang('ActionPinnedMediaGif');
|
||||
if (video) return lang('ActionPinnedMediaVideo');
|
||||
if (sticker) return lang('ActionPinnedMediaSticker');
|
||||
if (document) return lang('ActionPinnedMediaFile');
|
||||
if (contact) return lang('ActionPinnedMediaContact');
|
||||
if (location) return lang('ActionPinnedMediaLocation');
|
||||
if (storyData) return lang('ActionPinnedMediaStory');
|
||||
if (invoice) return lang('ActionPinnedMediaInvoice');
|
||||
if (game) return lang('ActionPinnedMediaGame', { game: game.title });
|
||||
if (pollId) return lang('ActionPinnedMediaPoll');
|
||||
if (giveaway) return lang('ActionPinnedMediaGiveaway');
|
||||
if (giveawayResults) return lang('ActionPinnedMediaGiveawayResults');
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function renderPeerLink(peerId: string | undefined, text: string, asPreview?: boolean) {
|
||||
if (!peerId || asPreview) {
|
||||
return renderText(text);
|
||||
}
|
||||
|
||||
return (
|
||||
<Link
|
||||
className={buildClassName(styles.peerLink, styles.strong)}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
getActions().openChat({ id: peerId });
|
||||
}}
|
||||
// box-decoration-break: clone; is broken when child has `dir` attribute
|
||||
withMultilineFix={IS_SAFARI}
|
||||
>
|
||||
{renderText(text)}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export function renderMessageLink(targetMessage: ApiMessage, text: TeactNode, asPreview?: boolean) {
|
||||
if (asPreview) return text;
|
||||
return (
|
||||
<Link
|
||||
className={styles.messageLink}
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
getActions().focusMessage({ chatId: targetMessage.chatId, messageId: targetMessage.id });
|
||||
}}
|
||||
withMultilineFix={IS_SAFARI}
|
||||
>
|
||||
{text}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export function getCallMessageKey(action: ApiMessageActionPhoneCall, isOutgoing: boolean): RegularLangKey {
|
||||
const isMissed = action.reason === 'missed';
|
||||
const isCancelled = action.reason === 'busy' || action.duration === undefined;
|
||||
if (action.isVideo) {
|
||||
if (isMissed) return isOutgoing ? 'CallMessageVideoOutgoingMissed' : 'CallMessageVideoIncomingMissed';
|
||||
if (isCancelled) return 'CallMessageVideoIncomingDeclined';
|
||||
|
||||
return isOutgoing ? 'CallMessageVideoOutgoing' : 'CallMessageVideoIncoming';
|
||||
} else {
|
||||
if (isMissed) return isOutgoing ? 'CallMessageOutgoingMissed' : 'CallMessageIncomingMissed';
|
||||
if (isCancelled) return 'CallMessageIncomingDeclined';
|
||||
|
||||
return isOutgoing ? 'CallMessageOutgoing' : 'CallMessageIncoming';
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,78 @@
|
||||
import React, { useEffect } from '../../../../lib/teact/teact';
|
||||
|
||||
import { SVG_NAMESPACE } from '../../../../config';
|
||||
import { addSvgDefinition, removeSvgDefinition } from '../../../../util/svgController';
|
||||
|
||||
const SVG_MAP = new Map<string, SvgFluidBackgroundFilter>();
|
||||
|
||||
class SvgFluidBackgroundFilter {
|
||||
public filterId: string;
|
||||
|
||||
private referenceCount = 0;
|
||||
|
||||
constructor(public color: string) {
|
||||
this.filterId = `fluid-background-filter-${color.slice(1)}`;
|
||||
|
||||
addSvgDefinition((
|
||||
<filter color-interpolation-filters="sRGB" xmlns={SVG_NAMESPACE}>
|
||||
<feGaussianBlur in="SourceAlpha" stdDeviation="4" result="blur" />
|
||||
<feColorMatrix in="blur" mode="matrix" values="1 0 0 0 0 0 1 0 0 0 0 0 1 0 0 0 0 0 30 -15" result="goo" />
|
||||
<feComposite in="SourceAlpha" in2="goo" operator="over" result="outline" />
|
||||
<feFlood flood-color={color} result="color" />
|
||||
<feComposite in="color" in2="outline" operator="in" />
|
||||
</filter>
|
||||
), this.filterId);
|
||||
}
|
||||
|
||||
public getFilterId() {
|
||||
this.referenceCount += 1;
|
||||
return this.filterId;
|
||||
}
|
||||
|
||||
public removeReference() {
|
||||
this.referenceCount -= 1;
|
||||
if (this.referenceCount === 0) {
|
||||
removeSvgDefinition(this.filterId);
|
||||
}
|
||||
}
|
||||
|
||||
public isUsed() {
|
||||
return this.referenceCount > 0;
|
||||
}
|
||||
}
|
||||
|
||||
export default function useFluidBackgroundFilter(color?: string, asValue?: boolean) {
|
||||
useEffect(() => {
|
||||
if (!color) return undefined;
|
||||
|
||||
return () => {
|
||||
const colorFilter = SVG_MAP.get(color);
|
||||
if (colorFilter) {
|
||||
colorFilter.removeReference();
|
||||
if (!colorFilter.isUsed()) {
|
||||
SVG_MAP.delete(colorFilter.color);
|
||||
}
|
||||
}
|
||||
};
|
||||
}, [color]);
|
||||
|
||||
if (!color) return undefined;
|
||||
|
||||
if (SVG_MAP.has(color)) {
|
||||
const svg = SVG_MAP.get(color)!;
|
||||
return prepareStyle(svg.getFilterId(), asValue);
|
||||
}
|
||||
|
||||
const svg = new SvgFluidBackgroundFilter(color);
|
||||
SVG_MAP.set(color, svg);
|
||||
|
||||
return prepareStyle(svg.getFilterId(), asValue);
|
||||
}
|
||||
|
||||
function prepareStyle(filterId: string, asValue?: boolean) {
|
||||
if (asValue) {
|
||||
return `url(#${filterId})`;
|
||||
}
|
||||
|
||||
return `filter: url(#${filterId});`;
|
||||
}
|
||||
@ -16,6 +16,12 @@
|
||||
--reaction-background-hover: #FFBC2E55 !important;
|
||||
--reaction-text-color: #E98111 !important;
|
||||
z-index: 2;
|
||||
|
||||
&.outside {
|
||||
--reaction-text-color: #FFFFFF !important;
|
||||
--reaction-background: #FFBC2E77 !important;
|
||||
--reaction-background-hover: #FFBC2E99 !important;
|
||||
}
|
||||
}
|
||||
|
||||
&.paid.chosen {
|
||||
|
||||
@ -40,6 +40,7 @@ type OwnProps = {
|
||||
recentReactors?: ApiPeer[];
|
||||
className?: string;
|
||||
chosenClassName?: string;
|
||||
isOutside?: boolean;
|
||||
observeIntersection?: ObserveFn;
|
||||
onClick?: (reaction: ApiReaction) => void;
|
||||
onPaidClick?: (count: number) => void;
|
||||
@ -58,6 +59,7 @@ const ReactionButton = ({
|
||||
chosenClassName,
|
||||
chatId,
|
||||
messageId,
|
||||
isOutside,
|
||||
observeIntersection,
|
||||
onClick,
|
||||
onPaidClick,
|
||||
@ -171,6 +173,7 @@ const ReactionButton = ({
|
||||
styles.root,
|
||||
isOwnMessage && styles.own,
|
||||
isPaid && styles.paid,
|
||||
isOutside && styles.outside,
|
||||
isReactionChosen(reaction) && styles.chosen,
|
||||
isReactionChosen(reaction) && chosenClassName,
|
||||
className,
|
||||
|
||||
@ -21,7 +21,7 @@
|
||||
&.is-service {
|
||||
justify-content: center;
|
||||
max-width: 19rem;
|
||||
margin: 0.3125rem auto;
|
||||
margin-top: 0.3125rem;
|
||||
}
|
||||
|
||||
.own &.is-outside {
|
||||
|
||||
@ -215,6 +215,7 @@ const Reactions: FC<OwnProps> = ({
|
||||
containerId={messageKey}
|
||||
isOwnMessage={message.isOutgoing}
|
||||
recentReactors={recentReactors}
|
||||
isOutside={isOutside}
|
||||
reaction={reaction}
|
||||
onClick={handleClick}
|
||||
onPaidClick={handlePaidClick}
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: auto;
|
||||
overflow-y: scroll;
|
||||
overflow-x: hidden;
|
||||
padding-top: 3.5rem;
|
||||
padding-inline: 0.75rem;
|
||||
@ -32,7 +32,7 @@
|
||||
|
||||
.balance-container {
|
||||
margin-left: auto;
|
||||
align-items: end;
|
||||
align-items: flex-end;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
@ -124,7 +124,6 @@
|
||||
padding: 1rem;
|
||||
padding-top: 0.5rem;
|
||||
margin-inline: -0.75rem; // Account for padding
|
||||
flex-grow: 1;
|
||||
flex-direction: column;
|
||||
background-color: var(--color-background-secondary);
|
||||
|
||||
@ -151,7 +150,6 @@
|
||||
display: flex;
|
||||
font-weight: var(--font-weight-semibold);
|
||||
font-size: 1rem;
|
||||
height: 3rem;
|
||||
}
|
||||
|
||||
.star {
|
||||
|
||||
@ -4,11 +4,10 @@ import React, {
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiMessage, ApiPeer } from '../../../api/types';
|
||||
import type { ThemeKey } from '../../../types';
|
||||
import type { GiftOption } from './GiftModal';
|
||||
import { type ApiMessage, type ApiPeer, MAIN_THREAD_ID } from '../../../api/types';
|
||||
|
||||
import { STARS_CURRENCY_CODE } from '../../../config';
|
||||
import { getPeerTitle } from '../../../global/helpers';
|
||||
import { isApiPeerUser } from '../../../global/helpers/peers';
|
||||
import { selectPeer, selectTabState, selectTheme } from '../../../global/selectors';
|
||||
@ -22,7 +21,7 @@ import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import PremiumProgress from '../../common/PremiumProgress';
|
||||
import ActionMessage from '../../middle/ActionMessage';
|
||||
import ActionMessage from '../../middle/message/ActionMessage';
|
||||
import Button from '../../ui/Button';
|
||||
import Link from '../../ui/Link';
|
||||
import ListItem from '../../ui/ListItem';
|
||||
@ -75,6 +74,7 @@ function GiftComposer({
|
||||
|
||||
const isStarGift = 'id' in gift;
|
||||
const isPeerUser = peer && isApiPeerUser(peer);
|
||||
const isSelf = peerId === currentUserId;
|
||||
|
||||
const localMessage = useMemo(() => {
|
||||
if (!isStarGift) {
|
||||
@ -86,17 +86,12 @@ function GiftComposer({
|
||||
date: Math.floor(Date.now() / 1000),
|
||||
content: {
|
||||
action: {
|
||||
targetChatId: peerId,
|
||||
mediaType: 'action',
|
||||
text: 'ActionGiftInbound',
|
||||
type: 'giftPremium',
|
||||
amount: gift.amount,
|
||||
currency: gift.currency,
|
||||
amount: gift.amount,
|
||||
months: gift.months,
|
||||
message: {
|
||||
text: giftMessage,
|
||||
},
|
||||
translationValues: ['%action_origin%', '%gift_payment_amount%'],
|
||||
message: giftMessage ? { text: giftMessage } : undefined,
|
||||
},
|
||||
},
|
||||
} satisfies ApiMessage;
|
||||
@ -110,27 +105,18 @@ function GiftComposer({
|
||||
date: Math.floor(Date.now() / 1000),
|
||||
content: {
|
||||
action: {
|
||||
targetChatId: peerId,
|
||||
mediaType: 'action',
|
||||
text: 'ActionGiftInbound',
|
||||
type: 'starGift',
|
||||
currency: STARS_CURRENCY_CODE,
|
||||
amount: gift.stars,
|
||||
starGift: {
|
||||
type: 'starGift',
|
||||
message: giftMessage?.length ? {
|
||||
text: giftMessage,
|
||||
} : undefined,
|
||||
isNameHidden: shouldHideName,
|
||||
starsToConvert: gift.starsToConvert,
|
||||
canUpgrade: shouldPayForUpgrade || undefined,
|
||||
alreadyPaidUpgradeStars: shouldPayForUpgrade ? gift.upgradeStars : undefined,
|
||||
isSaved: false,
|
||||
gift,
|
||||
peerId,
|
||||
fromId: currentUserId,
|
||||
},
|
||||
translationValues: ['%action_origin%', '%gift_payment_amount%'],
|
||||
message: giftMessage?.length ? {
|
||||
text: giftMessage,
|
||||
} : undefined,
|
||||
isNameHidden: shouldHideName || undefined,
|
||||
starsToConvert: gift.starsToConvert,
|
||||
canUpgrade: shouldPayForUpgrade || undefined,
|
||||
alreadyPaidUpgradeStars: shouldPayForUpgrade ? gift.upgradeStars : undefined,
|
||||
gift,
|
||||
peerId,
|
||||
fromId: currentUserId,
|
||||
},
|
||||
},
|
||||
} satisfies ApiMessage;
|
||||
@ -207,7 +193,7 @@ function GiftComposer({
|
||||
/>
|
||||
</ListItem>
|
||||
)}
|
||||
{isStarGift && (
|
||||
{isStarGift && gift.upgradeStars && (
|
||||
<div className={styles.description}>
|
||||
{isPeerUser
|
||||
? lang('GiftMakeUniqueDescription', {
|
||||
@ -237,7 +223,9 @@ function GiftComposer({
|
||||
)}
|
||||
{isStarGift && (
|
||||
<div className={styles.description}>
|
||||
{isPeerUser ? lang('GiftHideNameDescription', { receiver: title }) : lang('GiftHideNameDescriptionChannel')}
|
||||
{isSelf ? lang('GiftHideNameDescriptionSelf')
|
||||
: isPeerUser ? lang('GiftHideNameDescription', { receiver: title })
|
||||
: lang('GiftHideNameDescriptionChannel')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -247,7 +235,7 @@ function GiftComposer({
|
||||
function renderFooter() {
|
||||
const amount = isStarGift
|
||||
? formatStarsAsIcon(lang, gift.stars + (shouldPayForUpgrade ? gift.upgradeStars! : 0), { asFont: true })
|
||||
: formatCurrency(gift.amount, gift.currency);
|
||||
: formatCurrency(lang, gift.amount, gift.currency);
|
||||
|
||||
return (
|
||||
<div className={styles.footer}>
|
||||
@ -264,6 +252,7 @@ function GiftComposer({
|
||||
)}
|
||||
<Button
|
||||
className={styles.mainButton}
|
||||
size="smaller"
|
||||
onClick={handleMainButtonClick}
|
||||
isLoading={isPaymentFormLoading}
|
||||
>
|
||||
@ -300,7 +289,12 @@ function GiftComposer({
|
||||
className={bgClassName}
|
||||
style={customBackgroundValue ? `--custom-background: ${customBackgroundValue}` : undefined}
|
||||
/>
|
||||
<ActionMessage key={isStarGift ? gift.id : gift.months} message={localMessage} />
|
||||
<ActionMessage
|
||||
key={isStarGift ? gift.id : gift.months}
|
||||
message={localMessage}
|
||||
threadId={MAIN_THREAD_ID}
|
||||
appearanceOrder={0}
|
||||
/>
|
||||
</div>
|
||||
{renderOptionsSection()}
|
||||
<div className={styles.spacer} />
|
||||
|
||||
@ -102,7 +102,7 @@ const GiftUpgradeModal = ({ modal, recipient }: OwnProps & StateProps) => {
|
||||
}, [renderingModal?.sampleAttributes]);
|
||||
|
||||
const modalData = useMemo(() => {
|
||||
if (!previewAttributes) {
|
||||
if (!previewAttributes || !isOpen) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -159,7 +159,7 @@ const GiftUpgradeModal = ({ modal, recipient }: OwnProps & StateProps) => {
|
||||
header,
|
||||
footer,
|
||||
};
|
||||
}, [previewAttributes, lang, renderingRecipient, renderingModal?.gift, shouldKeepOriginalDetails]);
|
||||
}, [previewAttributes, isOpen, lang, renderingRecipient, renderingModal?.gift, shouldKeepOriginalDetails]);
|
||||
|
||||
return (
|
||||
<TableAboutModal
|
||||
|
||||
@ -112,7 +112,7 @@ const StarTopupOptionList: FC<OwnProps> = ({
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.optionBottom}>
|
||||
{formatCurrency(option.amount, option.currency, oldLang.code)}
|
||||
{formatCurrency(lang, option.amount, option.currency)}
|
||||
</div>
|
||||
{(isActiveOption || (selectedStarOption && 'winners' in selectedStarOption)) && perUserStarCount && (
|
||||
<div className={styles.optionBottom}>
|
||||
|
||||
@ -10,6 +10,7 @@ import type {
|
||||
} from '../../api/types';
|
||||
import type { FormEditDispatch } from '../../hooks/reducers/usePaymentReducer';
|
||||
import type { IconName } from '../../types/icons';
|
||||
import type { LangFn } from '../../util/localization';
|
||||
import { PaymentStep } from '../../types';
|
||||
|
||||
import { getWebDocumentHash } from '../../global/helpers';
|
||||
@ -17,6 +18,7 @@ import buildClassName from '../../util/buildClassName';
|
||||
import { formatCurrency } from '../../util/formatCurrency';
|
||||
import renderText from '../common/helpers/renderText';
|
||||
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useMedia from '../../hooks/useMedia';
|
||||
import useMediaTransition from '../../hooks/useMediaTransition';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
@ -74,7 +76,8 @@ const Checkout: FC<OwnProps> = ({
|
||||
}) => {
|
||||
const { setPaymentStep } = getActions();
|
||||
|
||||
const lang = useOldLang();
|
||||
const oldLang = useOldLang();
|
||||
const lang = useLang();
|
||||
const isInteractive = Boolean(dispatch);
|
||||
|
||||
const {
|
||||
@ -117,7 +120,7 @@ const Checkout: FC<OwnProps> = ({
|
||||
{title}
|
||||
</div>
|
||||
<div>
|
||||
{formatCurrency(tipAmount!, invoice.currency, lang.code)}
|
||||
{formatCurrency(lang, tipAmount!, invoice.currency)}
|
||||
</div>
|
||||
</div>
|
||||
<div className={styles.tipsList}>
|
||||
@ -127,7 +130,7 @@ const Checkout: FC<OwnProps> = ({
|
||||
className={buildClassName(styles.tipsItem, tip === tipAmount && styles.tipsItem_active)}
|
||||
onClick={dispatch ? () => handleTipsClick(tip === tipAmount ? 0 : tip) : undefined}
|
||||
>
|
||||
{formatCurrency(tip, invoice.currency, lang.code, { shouldOmitFractions: true })}
|
||||
{formatCurrency(lang, tip, invoice.currency, { shouldOmitFractions: true })}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
@ -136,7 +139,7 @@ const Checkout: FC<OwnProps> = ({
|
||||
}
|
||||
|
||||
function renderTosLink(url: string, isRtl?: boolean) {
|
||||
const langString = lang('PaymentCheckoutAcceptRecurrent', botName);
|
||||
const langString = oldLang('PaymentCheckoutAcceptRecurrent', botName);
|
||||
const langStringSplit = langString.split('*');
|
||||
return (
|
||||
<>
|
||||
@ -154,7 +157,7 @@ const Checkout: FC<OwnProps> = ({
|
||||
function renderTos(url: string) {
|
||||
return (
|
||||
<Checkbox
|
||||
label={renderTosLink(url, lang.isRtl)}
|
||||
label={renderTosLink(url, oldLang.isRtl)}
|
||||
name="checkout_tos"
|
||||
checked={Boolean(isTosAccepted)}
|
||||
className={styles.tosCheckbox}
|
||||
@ -197,47 +200,47 @@ const Checkout: FC<OwnProps> = ({
|
||||
</div>
|
||||
<div className={styles.priceInfo}>
|
||||
{invoice.prices.map((item) => (
|
||||
renderPaymentItem(lang.code, item.label, item.amount, invoice.currency)
|
||||
renderPaymentItem(lang, item.label, item.amount, invoice.currency)
|
||||
))}
|
||||
{shippingPrices && shippingPrices.map((item) => (
|
||||
renderPaymentItem(lang.code, item.label, item.amount, invoice.currency)
|
||||
renderPaymentItem(lang, item.label, item.amount, invoice.currency)
|
||||
))}
|
||||
{suggestedTipAmounts && suggestedTipAmounts.length > 0 && renderTips()}
|
||||
{totalPrice !== undefined && (
|
||||
renderPaymentItem(lang.code, lang('Checkout.TotalAmount'), totalPrice, invoice.currency, true)
|
||||
renderPaymentItem(lang, oldLang('Checkout.TotalAmount'), totalPrice, invoice.currency, true)
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.invoiceInfo}>
|
||||
{!isPaymentFormUrl && renderCheckoutItem({
|
||||
title: paymentMethod || savedCredentials?.[0].title,
|
||||
label: lang('PaymentCheckoutMethod'),
|
||||
label: oldLang('PaymentCheckoutMethod'),
|
||||
icon: 'card',
|
||||
onClick: isInteractive ? handlePaymentMethodClick : undefined,
|
||||
})}
|
||||
{paymentProvider && renderCheckoutItem({
|
||||
title: paymentProvider,
|
||||
label: lang('PaymentCheckoutProvider'),
|
||||
label: oldLang('PaymentCheckoutProvider'),
|
||||
customIcon: buildClassName(styles.provider, styles[paymentProvider.toLowerCase()]),
|
||||
})}
|
||||
{(needAddress || (!isInteractive && shippingAddress)) && renderCheckoutItem({
|
||||
title: shippingAddress,
|
||||
label: lang('PaymentShippingAddress'),
|
||||
label: oldLang('PaymentShippingAddress'),
|
||||
icon: 'location',
|
||||
onClick: isInteractive ? handleShippingAddressClick : undefined,
|
||||
})}
|
||||
{name && renderCheckoutItem({
|
||||
title: name,
|
||||
label: lang('PaymentCheckoutName'),
|
||||
label: oldLang('PaymentCheckoutName'),
|
||||
icon: 'user',
|
||||
})}
|
||||
{phone && renderCheckoutItem({
|
||||
title: phone,
|
||||
label: lang('PaymentCheckoutPhoneNumber'),
|
||||
label: oldLang('PaymentCheckoutPhoneNumber'),
|
||||
icon: 'phone',
|
||||
})}
|
||||
{(hasShippingOptions || (!isInteractive && shippingMethod)) && renderCheckoutItem({
|
||||
title: shippingMethod,
|
||||
label: lang('PaymentCheckoutShippingMethod'),
|
||||
label: oldLang('PaymentCheckoutShippingMethod'),
|
||||
icon: 'truck',
|
||||
onClick: isInteractive ? handleShippingMethodClick : undefined,
|
||||
})}
|
||||
@ -250,7 +253,7 @@ const Checkout: FC<OwnProps> = ({
|
||||
export default memo(Checkout);
|
||||
|
||||
function renderPaymentItem(
|
||||
langCode: string | undefined, title: string, value: number, currency: string, main = false,
|
||||
lang: LangFn, title: string, value: number, currency: string, main = false,
|
||||
) {
|
||||
return (
|
||||
<div className={buildClassName(styles.priceInfoItem, main && styles.priceInfoItemMain)}>
|
||||
@ -258,7 +261,7 @@ function renderPaymentItem(
|
||||
{title}
|
||||
</div>
|
||||
<div>
|
||||
{formatCurrency(value, currency, langCode)}
|
||||
{formatCurrency(lang, value, currency)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@ -9,6 +9,7 @@ import type { ShippingOption } from '../../types';
|
||||
|
||||
import { formatCurrency } from '../../util/formatCurrency';
|
||||
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
|
||||
import RadioGroup from '../ui/RadioGroup';
|
||||
@ -28,7 +29,8 @@ const Shipping: FC<OwnProps> = ({
|
||||
currency,
|
||||
dispatch,
|
||||
}) => {
|
||||
const lang = useOldLang();
|
||||
const oldLang = useOldLang();
|
||||
const lang = useLang();
|
||||
|
||||
useEffect(() => {
|
||||
if (!shippingOptions || !shippingOptions.length || state.shipping) {
|
||||
@ -43,14 +45,14 @@ const Shipping: FC<OwnProps> = ({
|
||||
|
||||
const options = useMemo(() => (shippingOptions.map(({ id: value, title: label, amount }) => ({
|
||||
label,
|
||||
subLabel: formatCurrency(amount, currency, lang.code),
|
||||
subLabel: formatCurrency(lang, amount, currency),
|
||||
value,
|
||||
}))), [shippingOptions, currency, lang.code]);
|
||||
}))), [shippingOptions, currency, lang]);
|
||||
|
||||
return (
|
||||
<div className="Shipping">
|
||||
<form>
|
||||
<p>{lang('PaymentShippingMethod')}</p>
|
||||
<p>{oldLang('PaymentShippingMethod')}</p>
|
||||
<RadioGroup
|
||||
name="shipping-options"
|
||||
options={options}
|
||||
|
||||
@ -44,9 +44,9 @@ const TestLocale = () => {
|
||||
</p>
|
||||
<p>{lang('Participants', { count: 42 }, { pluralValue: 42 })}</p>
|
||||
<p>
|
||||
{lang('ChatServiceGroupUpdatedPinnedMessage1', {
|
||||
message: 'Some message',
|
||||
user: 'Some user',
|
||||
{lang('ActionPinnedText', {
|
||||
text: 'Some message',
|
||||
from: 'Some user',
|
||||
})}
|
||||
</p>
|
||||
{/* <p>
|
||||
|
||||
@ -6,7 +6,7 @@ import React, {
|
||||
import { DEBUG } from '../../config';
|
||||
import { blobToDataUri, blobToFile } from '../../util/files';
|
||||
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
import useLang from '../../hooks/useLang';
|
||||
|
||||
import Icon from '../common/icons/Icon';
|
||||
import Button from './Button';
|
||||
@ -80,6 +80,8 @@ type OwnProps = {
|
||||
const CropModal: FC<OwnProps> = ({ file, onChange, onClose }: OwnProps) => {
|
||||
const [isCroppieReady, setIsCroppieReady] = useState(false);
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
useEffect(() => {
|
||||
if (!file) {
|
||||
return;
|
||||
@ -94,8 +96,6 @@ const CropModal: FC<OwnProps> = ({ file, onChange, onClose }: OwnProps) => {
|
||||
initCropper(file);
|
||||
}, [file, isCroppieReady]);
|
||||
|
||||
const lang = useOldLang();
|
||||
|
||||
const handleCropClick = useCallback(async () => {
|
||||
if (!cropper) {
|
||||
return;
|
||||
@ -111,7 +111,7 @@ const CropModal: FC<OwnProps> = ({ file, onChange, onClose }: OwnProps) => {
|
||||
<Modal
|
||||
isOpen={Boolean(file)}
|
||||
onClose={onClose}
|
||||
title="Drag to reposition"
|
||||
title={lang('CropperTitle')}
|
||||
className="CropModal"
|
||||
hasCloseButton
|
||||
>
|
||||
@ -125,7 +125,7 @@ const CropModal: FC<OwnProps> = ({ file, onChange, onClose }: OwnProps) => {
|
||||
round
|
||||
color="primary"
|
||||
onClick={handleCropClick}
|
||||
ariaLabel={lang('CropImage')}
|
||||
ariaLabel={lang('CropperApply')}
|
||||
>
|
||||
<Icon name="check" />
|
||||
</Button>
|
||||
|
||||
@ -12,11 +12,12 @@ type OwnProps = {
|
||||
className?: string;
|
||||
isRtl?: boolean;
|
||||
isPrimary?: boolean;
|
||||
withMultilineFix?: boolean;
|
||||
onClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void;
|
||||
};
|
||||
|
||||
const Link: FC<OwnProps> = ({
|
||||
children, isPrimary, className, isRtl, onClick,
|
||||
children, isPrimary, className, isRtl, withMultilineFix, onClick,
|
||||
}) => {
|
||||
const handleClick = useLastCallback((e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
e.preventDefault();
|
||||
@ -27,7 +28,7 @@ const Link: FC<OwnProps> = ({
|
||||
<a
|
||||
href="#"
|
||||
className={buildClassName('Link', styles.link, className, isPrimary && styles.isPrimary)}
|
||||
dir={isRtl ? 'rtl' : 'auto'}
|
||||
dir={!withMultilineFix ? (isRtl ? 'rtl' : 'auto') : undefined}
|
||||
onClick={onClick ? handleClick : undefined}
|
||||
>
|
||||
{children}
|
||||
|
||||
@ -50,7 +50,7 @@ const ProgressSpinner: FC<{
|
||||
|
||||
const dpr = useDevicePixelRatio();
|
||||
|
||||
const color = useDynamicColorListener(canvasRef, !withColor);
|
||||
const color = useDynamicColorListener(canvasRef, undefined, !withColor);
|
||||
|
||||
useEffect(() => {
|
||||
let isFirst = true;
|
||||
|
||||
@ -173,6 +173,7 @@ export const TMP_CHAT_ID = '0';
|
||||
|
||||
export const ANIMATION_END_DELAY = 100;
|
||||
export const ANIMATION_WAVE_MIN_INTERVAL = 200;
|
||||
export const MESSAGE_APPEARANCE_DELAY = 10;
|
||||
|
||||
export const SCROLL_MIN_DURATION = 300;
|
||||
export const SCROLL_MAX_DURATION = 600;
|
||||
|
||||
@ -180,10 +180,11 @@ function onUpdateAuthorizationState<T extends GlobalState>(global: T, update: Ap
|
||||
}
|
||||
|
||||
function onUpdateAuthorizationError<T extends GlobalState>(global: T, update: ApiUpdateAuthorizationError) {
|
||||
// TODO: Investigate why TS is not happy with spread for lang related types
|
||||
global = {
|
||||
...global,
|
||||
authErrorKey: update.errorKey,
|
||||
};
|
||||
global.authErrorKey = update.errorKey;
|
||||
setGlobal(global);
|
||||
}
|
||||
|
||||
|
||||
@ -208,7 +208,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
if (!message) return;
|
||||
|
||||
// Workaround for a weird behavior when interaction is received after watching reaction
|
||||
if (getMessageText(message) !== update.emoji) return;
|
||||
if (getMessageText(message)?.text !== update.emoji) return;
|
||||
|
||||
const tabState = selectTabState(global, tabId);
|
||||
global = updateTabState(global, {
|
||||
@ -1163,7 +1163,7 @@ export function deleteMessages<T extends GlobalState>(
|
||||
return;
|
||||
}
|
||||
|
||||
if (message.content.action?.photo) {
|
||||
if (message.content.action?.type === 'chatEditPhoto' && message.content.action.photo) {
|
||||
global = deletePeerPhoto(global, chatId, message.content.action.photo.id, true);
|
||||
}
|
||||
|
||||
@ -1251,7 +1251,7 @@ export function deleteMessages<T extends GlobalState>(
|
||||
}
|
||||
}
|
||||
|
||||
if (message?.content.action?.photo) {
|
||||
if (message?.content.action?.type === 'chatEditPhoto' && message.content.action.photo) {
|
||||
global = deletePeerPhoto(global, commonBoxChatId, message.content.action.photo.id, true);
|
||||
}
|
||||
|
||||
|
||||
@ -217,15 +217,16 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
}
|
||||
|
||||
case 'newMessage': {
|
||||
const actionStarGift = update.message.content?.action?.starGift;
|
||||
const action = update.message.content?.action;
|
||||
if (!update.message.isOutgoing && update.message.chatId !== SERVICE_NOTIFICATIONS_USER_ID) return undefined;
|
||||
if (actionStarGift?.type !== 'starGiftUnique') return undefined;
|
||||
if (action?.type !== 'starGiftUnique') return undefined;
|
||||
const actionStarGift = action.gift;
|
||||
|
||||
Object.values(global.byTabId).forEach(({ id: tabId }) => {
|
||||
const tabState = selectTabState(global, tabId);
|
||||
if (tabState.isWaitingForStarGiftUpgrade) {
|
||||
actions.openUniqueGiftBySlug({
|
||||
slug: actionStarGift.gift.slug,
|
||||
slug: actionStarGift.slug,
|
||||
tabId,
|
||||
});
|
||||
|
||||
@ -259,8 +260,8 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
gift: {
|
||||
key: 'GiftUnique',
|
||||
variables: {
|
||||
title: actionStarGift.gift.title,
|
||||
number: actionStarGift.gift.number,
|
||||
title: actionStarGift.title,
|
||||
number: actionStarGift.number,
|
||||
},
|
||||
},
|
||||
peer: getPeerTitle(getTranslationFn(), receiver),
|
||||
|
||||
@ -31,7 +31,6 @@ import {
|
||||
getMessageStatefulContent,
|
||||
getPeerTitle,
|
||||
isChatChannel,
|
||||
isJoinedChannelMessage,
|
||||
} from '../../helpers';
|
||||
import { getMessageSummaryText } from '../../helpers/messageSummary';
|
||||
import { renderMessageSummaryHtml } from '../../helpers/renderMessageSummaryHtml';
|
||||
@ -345,13 +344,6 @@ addActionHandler('focusLastMessage', (global, actions, payload): ActionReturnTyp
|
||||
lastMessageId = pinnedMessageIds[pinnedMessageIds.length - 1];
|
||||
} else {
|
||||
lastMessageId = selectChatLastMessageId(global, chatId);
|
||||
|
||||
const chatMessages = selectChatMessages(global, chatId);
|
||||
// Workaround for scroll to local message 'you joined this channel'
|
||||
const lastChatMessage = Object.values(chatMessages).reverse()[0];
|
||||
if (lastMessageId && isJoinedChannelMessage(lastChatMessage) && lastChatMessage.id > lastMessageId) {
|
||||
lastMessageId = lastChatMessage.id;
|
||||
}
|
||||
}
|
||||
} else if (isSavedDialog) {
|
||||
lastMessageId = selectChatLastMessageId(global, String(threadId), 'saved');
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { ApiMessageActionStarGift, ApiSavedStarGift } from '../../../api/types';
|
||||
import type { ApiInputSavedStarGift, ApiSavedStarGift } from '../../../api/types';
|
||||
import type { ActionReturnType } from '../../types';
|
||||
|
||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||
@ -208,32 +208,35 @@ addActionHandler('openGiftInfoModalFromMessage', (global, actions, payload): Act
|
||||
if (!message || !message.content.action) return;
|
||||
|
||||
const action = message.content.action;
|
||||
if (action.type === 'starGiftUnique') {
|
||||
actions.openGiftInfoModal({ gift: action.starGift?.gift!, tabId });
|
||||
return;
|
||||
}
|
||||
if (action.type !== 'starGift' && action.type !== 'starGiftUnique') return;
|
||||
|
||||
if (action.type !== 'starGift') return;
|
||||
const starGift = action.type === 'starGift' ? action : undefined;
|
||||
const uniqueGift = action.type === 'starGiftUnique' ? action : undefined;
|
||||
|
||||
const starGift = action.starGift! as ApiMessageActionStarGift;
|
||||
const giftReceiverId = action.peerId || (message.isOutgoing ? message.chatId : global.currentUserId!);
|
||||
|
||||
const giftReceiverId = message.isOutgoing ? message.chatId : global.currentUserId!;
|
||||
const inputGift: ApiInputSavedStarGift = action.savedId
|
||||
? { type: 'chat', chatId, savedId: action.savedId }
|
||||
: { type: 'user', messageId };
|
||||
|
||||
const gift = {
|
||||
const gift: ApiSavedStarGift = {
|
||||
date: message.date,
|
||||
gift: starGift.gift,
|
||||
message: starGift.message,
|
||||
starsToConvert: starGift.starsToConvert,
|
||||
isNameHidden: starGift.isNameHidden,
|
||||
isUnsaved: !starGift.isSaved,
|
||||
fromId: message.isOutgoing ? global.currentUserId : message.chatId,
|
||||
messageId: (!message.isOutgoing || chatId === global.currentUserId) ? message.id : undefined,
|
||||
isConverted: starGift.isConverted,
|
||||
upgradeMsgId: starGift.upgradeMsgId,
|
||||
canUpgrade: starGift.canUpgrade,
|
||||
alreadyPaidUpgradeStars: starGift.alreadyPaidUpgradeStars,
|
||||
inputGift: starGift.inputSavedGift,
|
||||
} satisfies ApiSavedStarGift;
|
||||
gift: action.gift,
|
||||
message: starGift?.message,
|
||||
starsToConvert: starGift?.starsToConvert,
|
||||
isNameHidden: starGift?.isNameHidden,
|
||||
isUnsaved: !action.isSaved,
|
||||
fromId: action.fromId,
|
||||
messageId: message.id,
|
||||
isConverted: starGift?.isConverted,
|
||||
upgradeMsgId: starGift?.upgradeMsgId,
|
||||
canUpgrade: starGift?.canUpgrade,
|
||||
alreadyPaidUpgradeStars: starGift?.alreadyPaidUpgradeStars,
|
||||
inputGift,
|
||||
canExportAt: uniqueGift?.canExportAt,
|
||||
savedId: action.savedId,
|
||||
transferStars: uniqueGift?.transferStars,
|
||||
};
|
||||
|
||||
actions.openGiftInfoModal({ peerId: giftReceiverId, gift, tabId });
|
||||
});
|
||||
|
||||
@ -277,6 +277,13 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
|
||||
if (!cached.peers) {
|
||||
cached.peers = initialState.peers;
|
||||
}
|
||||
|
||||
if (!cached.cacheVersion) {
|
||||
cached.cacheVersion = initialState.cacheVersion;
|
||||
// Reset because of the new action message structure
|
||||
cached.messages = initialState.messages;
|
||||
cached.chats.listIds = initialState.chats.listIds;
|
||||
}
|
||||
}
|
||||
|
||||
function updateCache(force?: boolean) {
|
||||
|
||||
@ -10,7 +10,7 @@ import { CONTENT_NOT_SUPPORTED } from '../../config';
|
||||
import trimText from '../../util/trimText';
|
||||
import { renderTextWithEntities } from '../../components/common/helpers/renderTextWithEntities';
|
||||
import {
|
||||
getExpiredMessageContentDescription, getMessageText, getMessageTranscription, isExpiredMessageContent,
|
||||
getMessageText, getMessageTranscription,
|
||||
} from './messages';
|
||||
|
||||
const SPOILER_CHARS = ['⠺', '⠵', '⠞', '⠟'];
|
||||
@ -35,7 +35,7 @@ export function getMessageSummaryText(
|
||||
export function getMessageTextWithSpoilers(message: ApiMessage, statefulContent: StatefulMediaContent | undefined) {
|
||||
const transcription = getMessageTranscription(message);
|
||||
|
||||
const textWithoutTranscription = getMessageText(statefulContent?.story || message);
|
||||
const textWithoutTranscription = getMessageText(statefulContent?.story || message)?.text;
|
||||
if (!textWithoutTranscription) {
|
||||
return transcription;
|
||||
}
|
||||
@ -147,7 +147,7 @@ function getSummaryDescription(
|
||||
const { poll } = statefulContent || {};
|
||||
|
||||
let hasUsedTruncatedText = false;
|
||||
let summary: string | TeactNode | undefined;
|
||||
let summary: TeactNode | undefined;
|
||||
|
||||
const boughtExtendedMedia = paidMedia?.isBought && paidMedia.extendedMedia;
|
||||
const previewExtendedMedia = paidMedia && !paidMedia.isBought
|
||||
@ -199,7 +199,7 @@ function getSummaryDescription(
|
||||
summary = renderTextWithEntities({
|
||||
text: poll.summary.question.text,
|
||||
entities: poll.summary.question.entities,
|
||||
noLineBreaks: true,
|
||||
asPreview: true,
|
||||
});
|
||||
}
|
||||
|
||||
@ -239,13 +239,6 @@ function getSummaryDescription(
|
||||
summary = truncatedText || (message ? lang('ForwardedStory') : lang('Chat.ReplyStory'));
|
||||
}
|
||||
|
||||
if (isExpiredMessageContent(mediaContent)) {
|
||||
const expiredMessageText = getExpiredMessageContentDescription(lang, mediaContent);
|
||||
if (expiredMessageText) {
|
||||
summary = expiredMessageText;
|
||||
}
|
||||
}
|
||||
|
||||
return summary || CONTENT_NOT_SUPPORTED;
|
||||
}
|
||||
|
||||
|
||||
@ -7,7 +7,7 @@ import type {
|
||||
ApiTypeStory,
|
||||
} from '../../api/types';
|
||||
import type {
|
||||
ApiPoll, MediaContainer, MediaContent, StatefulMediaContent,
|
||||
ApiPoll, MediaContainer, StatefulMediaContent,
|
||||
} from '../../api/types/messages';
|
||||
import type { OldLangFn } from '../../hooks/useOldLang';
|
||||
import type { CustomPeer, ThreadId } from '../../types';
|
||||
@ -57,13 +57,13 @@ export function getMessageTranscription(message: ApiMessage) {
|
||||
|
||||
export function hasMessageText(message: MediaContainer) {
|
||||
const {
|
||||
text, sticker, photo, video, audio, voice, document, pollId, webPage, contact, invoice, location,
|
||||
game, action, storyData, giveaway, giveawayResults, isExpiredVoice, paidMedia,
|
||||
action, text, sticker, photo, video, audio, voice, document, pollId, webPage, contact, invoice, location,
|
||||
game, storyData, giveaway, giveawayResults, paidMedia,
|
||||
} = message.content;
|
||||
|
||||
return Boolean(text) || !(
|
||||
sticker || photo || video || audio || voice || document || contact || pollId || webPage || invoice || location
|
||||
|| game || action?.phoneCall || storyData || giveaway || giveawayResults || isExpiredVoice || paidMedia
|
||||
|| game || storyData || giveaway || giveawayResults || paidMedia || action?.type === 'phoneCall'
|
||||
);
|
||||
}
|
||||
|
||||
@ -73,10 +73,10 @@ export function getMessageStatefulContent(global: GlobalState, message: ApiMessa
|
||||
const { peerId: storyPeerId, id: storyId } = message.content.storyData || {};
|
||||
const story = storyId && storyPeerId ? global.stories.byPeerId[storyPeerId]?.byId[storyId] : undefined;
|
||||
|
||||
return groupStatetefulContent({ poll, story });
|
||||
return groupStatefulContent({ poll, story });
|
||||
}
|
||||
|
||||
export function groupStatetefulContent({
|
||||
export function groupStatefulContent({
|
||||
poll,
|
||||
story,
|
||||
} : {
|
||||
@ -90,7 +90,7 @@ export function groupStatetefulContent({
|
||||
}
|
||||
|
||||
export function getMessageText(message: MediaContainer) {
|
||||
return hasMessageText(message) ? message.content.text?.text || CONTENT_NOT_SUPPORTED : undefined;
|
||||
return hasMessageText(message) ? message.content.text || { text: CONTENT_NOT_SUPPORTED } : undefined;
|
||||
}
|
||||
|
||||
export function getMessageCustomShape(message: ApiMessage): boolean {
|
||||
@ -198,7 +198,7 @@ export function isForwardedMessage(message: ApiMessage) {
|
||||
}
|
||||
|
||||
export function isActionMessage(message: ApiMessage) {
|
||||
return Boolean(message.content.action) || isExpiredMessage(message);
|
||||
return Boolean(message.content.action);
|
||||
}
|
||||
|
||||
export function isServiceNotificationMessage(message: ApiMessage) {
|
||||
@ -359,36 +359,14 @@ export function extractMessageText(message: ApiMessage | ApiStory, inChatList =
|
||||
return { text, entities };
|
||||
}
|
||||
|
||||
export function getExpiredMessageDescription(langFn: OldLangFn, message: ApiMessage): string | undefined {
|
||||
return getExpiredMessageContentDescription(langFn, message.content);
|
||||
}
|
||||
export function getExpiredMessageContentDescription(langFn: OldLangFn, mediaContent: MediaContent): string | undefined {
|
||||
const { isExpiredVoice, isExpiredRoundVideo } = mediaContent;
|
||||
if (isExpiredVoice) {
|
||||
return langFn('Message.VoiceMessageExpired');
|
||||
} else if (isExpiredRoundVideo) {
|
||||
return langFn('Message.VideoMessageExpired');
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function isExpiredMessage(message: ApiMessage) {
|
||||
return isExpiredMessageContent(message.content);
|
||||
}
|
||||
|
||||
export function isExpiredMessageContent(content: MediaContent) {
|
||||
const { isExpiredVoice, isExpiredRoundVideo } = content ?? {};
|
||||
return Boolean(isExpiredVoice || isExpiredRoundVideo);
|
||||
return message.content.action?.type === 'expired';
|
||||
}
|
||||
|
||||
export function hasMessageTtl(message: ApiMessage) {
|
||||
return message.content?.ttlSeconds !== undefined;
|
||||
}
|
||||
|
||||
export function isJoinedChannelMessage(message: ApiMessage) {
|
||||
return message.content.action && message.content.action.type === 'joinedChannel';
|
||||
}
|
||||
|
||||
export function getAttachmentMediaType(attachment: ApiAttachment) {
|
||||
if (SUPPORTED_AUDIO_CONTENT_TYPES.has(attachment.mimeType)) {
|
||||
return 'audio';
|
||||
|
||||
@ -317,9 +317,9 @@ export function getStarsTransactionFromGift(message: ApiMessage): ApiStarsTransa
|
||||
const { transactionId, stars } = action;
|
||||
|
||||
return {
|
||||
id: transactionId!,
|
||||
id: transactionId,
|
||||
stars: {
|
||||
amount: stars!,
|
||||
amount: stars,
|
||||
nanos: 0,
|
||||
},
|
||||
peer: {
|
||||
@ -337,17 +337,17 @@ export function getPrizeStarsTransactionFromGiveaway(message: ApiMessage): ApiSt
|
||||
|
||||
if (action?.type !== 'prizeStars') return undefined;
|
||||
|
||||
const { transactionId, stars, targetChatId } = action;
|
||||
const { transactionId, stars, boostPeerId } = action;
|
||||
|
||||
return {
|
||||
id: transactionId!,
|
||||
id: transactionId,
|
||||
stars: {
|
||||
amount: stars!,
|
||||
amount: stars,
|
||||
nanos: 0,
|
||||
},
|
||||
peer: {
|
||||
type: 'peer',
|
||||
id: targetChatId!,
|
||||
id: boostPeerId,
|
||||
},
|
||||
date: message.date,
|
||||
giveawayPostId: message.id,
|
||||
|
||||
@ -70,6 +70,7 @@ export const INITIAL_PERFORMANCE_STATE_MIN: PerformanceType = {
|
||||
};
|
||||
|
||||
export const INITIAL_GLOBAL_STATE: GlobalState = {
|
||||
cacheVersion: 1,
|
||||
isInited: true,
|
||||
attachMenu: { bots: {} },
|
||||
passcode: {},
|
||||
|
||||
@ -253,15 +253,19 @@ export function updateChatMessage<T extends GlobalState>(
|
||||
if (message && messageUpdate.isMediaUnread === false && hasMessageTtl(message)) {
|
||||
if (message.content.voice) {
|
||||
messageUpdate.content = {
|
||||
...messageUpdate.content,
|
||||
voice: undefined,
|
||||
isExpiredVoice: true,
|
||||
action: {
|
||||
mediaType: 'action',
|
||||
type: 'expired',
|
||||
isVoice: true,
|
||||
},
|
||||
};
|
||||
} else if (message.content.video?.isRound) {
|
||||
messageUpdate.content = {
|
||||
...messageUpdate.content,
|
||||
video: undefined,
|
||||
isExpiredRoundVideo: true,
|
||||
action: {
|
||||
mediaType: 'action',
|
||||
type: 'expired',
|
||||
isRoundVideo: true,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@ -646,7 +646,7 @@ export function selectAllowedMessageActionsSlow<T extends GlobalState>(
|
||||
const hasTtl = hasMessageTtl(message);
|
||||
const { content } = message;
|
||||
const isDocumentSticker = isMessageDocumentSticker(message);
|
||||
const isBoostMessage = message.content.action?.type === 'chatBoost';
|
||||
const isBoostMessage = message.content.action?.type === 'boostApply';
|
||||
|
||||
const hasChatPinPermission = (chat.isCreator
|
||||
|| (!isChannel && !isUserRightBanned(chat, 'pinMessages'))
|
||||
@ -935,9 +935,7 @@ export function selectFirstUnreadId<T extends GlobalState>(
|
||||
return (
|
||||
(!lastReadId || id > lastReadId)
|
||||
&& byId[id]
|
||||
// For some reason outgoing topic actions are not marked as read, thus we need to mark them as read
|
||||
// when the edit message hits the viewport
|
||||
&& ((!byId[id].isOutgoing || byId[id].content.action?.isTopicAction) || byId[id].isFromScheduled)
|
||||
&& (!byId[id].isOutgoing || byId[id].isFromScheduled)
|
||||
&& id > lastReadServiceNotificationId
|
||||
);
|
||||
});
|
||||
|
||||
@ -73,6 +73,7 @@ import type { RegularLangFnParameters } from '../../util/localization';
|
||||
import type { TabState } from './tabState';
|
||||
|
||||
export type GlobalState = {
|
||||
cacheVersion: number;
|
||||
isInited: boolean;
|
||||
config?: ApiConfig;
|
||||
appConfig?: ApiAppConfig;
|
||||
|
||||
71
src/hooks/element/useStyleObserver.ts
Normal file
71
src/hooks/element/useStyleObserver.ts
Normal file
@ -0,0 +1,71 @@
|
||||
import { useEffect, useLayoutEffect, useState } from '../../lib/teact/teact';
|
||||
|
||||
import useLastCallback from '../useLastCallback';
|
||||
import useResizeObserver from '../useResizeObserver';
|
||||
|
||||
const UPDATE_DEBOUNCE = 50; // ms
|
||||
|
||||
/**
|
||||
* @param property animateable property
|
||||
*/
|
||||
export default function useStyleObserver(
|
||||
ref: React.RefObject<HTMLElement>,
|
||||
property: string,
|
||||
debounce = UPDATE_DEBOUNCE,
|
||||
isDisabled?: boolean,
|
||||
) {
|
||||
const [value, setValue] = useState<string | undefined>();
|
||||
|
||||
const updateValue = useLastCallback(() => {
|
||||
if (!ref.current || isDisabled) {
|
||||
setValue(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const computedValue = getComputedStyle(ref.current).getPropertyValue(property).trim();
|
||||
setValue(computedValue);
|
||||
});
|
||||
|
||||
// Element does not receive `transitionend` event if parent has `display: none`.
|
||||
// We will receive `resize` event when parent is shown again.
|
||||
useResizeObserver(ref, updateValue, isDisabled);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el || isDisabled) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
el.style.setProperty('transition', `${debounce}ms ${property} linear`, 'important');
|
||||
|
||||
return () => {
|
||||
el.style.removeProperty('transition');
|
||||
};
|
||||
}, [debounce, isDisabled, property, ref]);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
updateValue();
|
||||
|
||||
if (isDisabled) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function handleTransitionEnd(e: TransitionEvent) {
|
||||
if (e.propertyName !== property) return;
|
||||
updateValue();
|
||||
}
|
||||
|
||||
el.addEventListener('transitionend', handleTransitionEnd);
|
||||
|
||||
return () => {
|
||||
el.removeEventListener('transitionend', handleTransitionEnd);
|
||||
};
|
||||
}, [isDisabled, property, ref, updateValue]);
|
||||
|
||||
return value;
|
||||
}
|
||||
@ -1,69 +1,18 @@
|
||||
import { useEffect, useLayoutEffect, useState } from '../../lib/teact/teact';
|
||||
import { useMemo } from '../../lib/teact/teact';
|
||||
|
||||
import { getPropertyHexColor } from '../../util/themeStyle';
|
||||
import useLastCallback from '../useLastCallback';
|
||||
import useResizeObserver from '../useResizeObserver';
|
||||
import { prepareHexColor } from '../../util/themeStyle';
|
||||
import useStyleObserver from '../element/useStyleObserver';
|
||||
|
||||
// Transition required to detect `color` property change.
|
||||
// Duration parameter describes a delay between color change and color state update.
|
||||
// Small values may cause large amount of re-renders.
|
||||
const TRANSITION_PROPERTY = 'color';
|
||||
const TRANSITION_STYLE = `50ms ${TRANSITION_PROPERTY} linear`;
|
||||
const DEBOUNCE = 50; // ms
|
||||
|
||||
export default function useDynamicColorListener(ref: React.RefObject<HTMLElement>,
|
||||
isDisabled?: boolean) {
|
||||
const [hexColor, setHexColor] = useState<string | undefined>();
|
||||
|
||||
const updateColor = useLastCallback(() => {
|
||||
if (!ref.current || isDisabled) {
|
||||
setHexColor(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
const currentHexColor = getPropertyHexColor(getComputedStyle(ref.current), TRANSITION_PROPERTY);
|
||||
setHexColor(currentHexColor);
|
||||
});
|
||||
|
||||
// Element does not receive `transitionend` event if parent has `display: none`.
|
||||
// We will receive `resize` event when parent is shown again.
|
||||
useResizeObserver(ref, updateColor, isDisabled);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el || isDisabled) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
el.style.setProperty('transition', TRANSITION_STYLE, 'important');
|
||||
|
||||
return () => {
|
||||
el.style.removeProperty('transition');
|
||||
};
|
||||
}, [isDisabled, ref]);
|
||||
|
||||
useEffect(() => {
|
||||
const el = ref.current;
|
||||
if (!el) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
updateColor();
|
||||
|
||||
if (isDisabled) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function handleTransitionEnd(e: TransitionEvent) {
|
||||
if (e.propertyName !== TRANSITION_PROPERTY) return;
|
||||
updateColor();
|
||||
}
|
||||
|
||||
el.addEventListener('transitionend', handleTransitionEnd);
|
||||
|
||||
return () => {
|
||||
el.removeEventListener('transitionend', handleTransitionEnd);
|
||||
};
|
||||
}, [isDisabled, ref, updateColor]);
|
||||
// Style observer that returns hex color value of the property
|
||||
export default function useDynamicColorListener(
|
||||
ref: React.RefObject<HTMLElement>,
|
||||
property = 'color',
|
||||
isDisabled?: boolean,
|
||||
) {
|
||||
const value = useStyleObserver(ref, property, DEBOUNCE, isDisabled);
|
||||
const hexColor = useMemo(() => (value ? prepareHexColor(value) : undefined), [value]);
|
||||
|
||||
return hexColor;
|
||||
}
|
||||
|
||||
@ -1,14 +1,14 @@
|
||||
import { useMemo } from '../lib/teact/teact';
|
||||
import { getGlobal } from '../global';
|
||||
|
||||
import type { ApiSticker, MediaContainer } from '../api/types';
|
||||
import type { ApiThumbnail, MediaContainer } from '../api/types';
|
||||
|
||||
import { getMessageMediaThumbDataUri } from '../global/helpers';
|
||||
import { selectTheme } from '../global/selectors';
|
||||
|
||||
export default function useThumbnail(media?: MediaContainer | ApiSticker) {
|
||||
export default function useThumbnail(media?: MediaContainer | ApiThumbnail) {
|
||||
const isMediaContainer = media && 'content' in media;
|
||||
const thumbDataUri = isMediaContainer ? getMessageMediaThumbDataUri(media) : media?.thumbnail?.dataUri;
|
||||
const thumbDataUri = isMediaContainer ? getMessageMediaThumbDataUri(media) : media?.dataUri;
|
||||
|
||||
// TODO Find a way to update thumbnail on theme change
|
||||
const theme = selectTheme(getGlobal());
|
||||
|
||||
@ -300,6 +300,7 @@ export enum MediaViewerOrigin {
|
||||
Album,
|
||||
ScheduledAlbum,
|
||||
SearchResult,
|
||||
ChannelAvatar,
|
||||
SuggestedAvatar,
|
||||
StarsTransaction,
|
||||
PreviewMedia,
|
||||
|
||||
477
src/types/language.d.ts
vendored
477
src/types/language.d.ts
vendored
@ -134,6 +134,7 @@ export interface LangPair {
|
||||
'SentAppCode': undefined;
|
||||
'LoginJustSentSms': undefined;
|
||||
'Code': undefined;
|
||||
'Open': undefined;
|
||||
'LoginHeaderPassword': undefined;
|
||||
'LoginEnterPasswordDescription': undefined;
|
||||
'StartText': undefined;
|
||||
@ -533,6 +534,8 @@ export interface LangPair {
|
||||
'Block': undefined;
|
||||
'DeleteThisChat': undefined;
|
||||
'Caption': undefined;
|
||||
'CropperTitle': undefined;
|
||||
'CropperApply': undefined;
|
||||
'AttachmentMenuPhotoOrVideo': undefined;
|
||||
'AttachDocument': undefined;
|
||||
'Poll': undefined;
|
||||
@ -1040,14 +1043,6 @@ export interface LangPair {
|
||||
'EditProfileNoFirstName': undefined;
|
||||
'AriaEditProfilePhoto': undefined;
|
||||
'LaunchConfetti': undefined;
|
||||
'SettingsAnimations': undefined;
|
||||
'SettingsAnimationsDescription': undefined;
|
||||
'SettingsAnimationsLow': undefined;
|
||||
'SettingsAnimationsMedium': undefined;
|
||||
'SettingsAnimationsHigh': undefined;
|
||||
'Settings12HourFormat': undefined;
|
||||
'Settings24HourFormat': undefined;
|
||||
'SettingsSendCtrlEnterDescription': undefined;
|
||||
'AriaMoreButton': undefined;
|
||||
'RecoveryEmailCode': undefined;
|
||||
'NotificationsWeb': undefined;
|
||||
@ -1169,6 +1164,7 @@ export interface LangPair {
|
||||
'GiftSoldOut': undefined;
|
||||
'GiftMessagePlaceholder': undefined;
|
||||
'GiftHideMyName': undefined;
|
||||
'GiftHideNameDescriptionSelf': undefined;
|
||||
'GiftHideNameDescriptionChannel': undefined;
|
||||
'GiftInfoSent': undefined;
|
||||
'GiftInfoReceived': undefined;
|
||||
@ -1225,14 +1221,6 @@ export interface LangPair {
|
||||
'PremiumGiftDescription': undefined;
|
||||
'StarsReactionLinkText': undefined;
|
||||
'StarsReactionLink': undefined;
|
||||
'ActionStarGiftDisplaying': undefined;
|
||||
'ActionStarGiftChannelDisplaying': undefined;
|
||||
'ActionStarGiftDescriptionUpgrade': undefined;
|
||||
'ActionStarGiftUpgraded': undefined;
|
||||
'ActionStarGiftUnpack': undefined;
|
||||
'GiftTo': undefined;
|
||||
'GiftFrom': undefined;
|
||||
'ReceivedGift': undefined;
|
||||
'SentGift': undefined;
|
||||
'StarsSubscribeInfoLinkText': undefined;
|
||||
'StarsSubscribeInfoLink': undefined;
|
||||
@ -1324,6 +1312,80 @@ export interface LangPair {
|
||||
'CheckPasswordTitle': undefined;
|
||||
'CheckPasswordPlaceholder': undefined;
|
||||
'CheckPasswordDescription': undefined;
|
||||
'ActionFallbackUser': undefined;
|
||||
'ActionFallbackChat': undefined;
|
||||
'ActionFallbackChannel': undefined;
|
||||
'ActionFallbackSomeone': undefined;
|
||||
'ActionUnsupported': undefined;
|
||||
'ActionPinnedNotFoundYou': undefined;
|
||||
'ActionPinnedMediaPhoto': undefined;
|
||||
'ActionPinnedMediaVideo': undefined;
|
||||
'ActionPinnedMediaAudio': undefined;
|
||||
'ActionPinnedMediaVoice': undefined;
|
||||
'ActionPinnedMediaVideoMessage': undefined;
|
||||
'ActionPinnedMediaFile': undefined;
|
||||
'ActionPinnedMediaGif': undefined;
|
||||
'ActionPinnedMediaContact': undefined;
|
||||
'ActionPinnedMediaLocation': undefined;
|
||||
'ActionPinnedMediaSticker': undefined;
|
||||
'ActionPinnedMediaInvoice': undefined;
|
||||
'ActionPinnedMediaStory': undefined;
|
||||
'ActionPinnedMediaAlbum': undefined;
|
||||
'ActionPinnedMediaPoll': undefined;
|
||||
'ActionPinnedMediaGiveaway': undefined;
|
||||
'ActionPinnedMediaGiveawayResults': undefined;
|
||||
'ActionGroupCallStartedChannel': undefined;
|
||||
'ActionExpiredVoice': undefined;
|
||||
'ActionExpiredVideo': undefined;
|
||||
'ActionChannelJoinedYou': undefined;
|
||||
'ActionChannelJoinedByRequestChannelYou': undefined;
|
||||
'ActionUserLeftYou': undefined;
|
||||
'ActionUserJoinedYou': undefined;
|
||||
'ActionJoinedByRequestYou': undefined;
|
||||
'ActionRemovedPhotoYou': undefined;
|
||||
'ActionRemovedPhotoChannel': undefined;
|
||||
'ActionChangedPhotoYou': undefined;
|
||||
'ActionChangedPhotoChannel': undefined;
|
||||
'ActionCreatedChannel': undefined;
|
||||
'ActionScreenshotTakenYou': undefined;
|
||||
'ActionBotAppPlaceholder': undefined;
|
||||
'ActionGiftTextUnknown': undefined;
|
||||
'ActionGiftUniqueSent': undefined;
|
||||
'ActionStarGiftUpgradedSelf': undefined;
|
||||
'ActionStarGiftTransferredSelf': undefined;
|
||||
'ActionStarGiftTransferredUnknown': undefined;
|
||||
'ActionStarGiftNoConvertTextYou': undefined;
|
||||
'ActionStarGiftDisplaying': undefined;
|
||||
'ActionStarGiftChannelDisplaying': undefined;
|
||||
'ActionStarGiftUpgradeTextYou': undefined;
|
||||
'ActionStarGiftUpgraded': undefined;
|
||||
'ActionStarGiftUnpack': undefined;
|
||||
'ActionStarGiftUniqueRibbon': undefined;
|
||||
'ActionStarGiftUniqueModel': undefined;
|
||||
'ActionStarGiftUniqueBackdrop': undefined;
|
||||
'ActionStarGiftUniqueSymbol': undefined;
|
||||
'ActionStarGiftSelf': undefined;
|
||||
'ActionSuggestedPhotoButton': undefined;
|
||||
'ActionSuggestedVideoTitle': undefined;
|
||||
'ActionSuggestedVideoText': undefined;
|
||||
'ActionSuggestedPhotoUpdatedTitle': undefined;
|
||||
'ActionSuggestedPhotoUpdatedDescription': undefined;
|
||||
'ActionAttachMenuBotAllowed': undefined;
|
||||
'ActionWebappBotAllowed': undefined;
|
||||
'ActionTopicClosedInside': undefined;
|
||||
'ActionTopicReopenedInside': undefined;
|
||||
'ActionTopicHiddenInside': undefined;
|
||||
'ActionTopicUnhiddenInside': undefined;
|
||||
'ActionTopicPlaceholder': undefined;
|
||||
'ActionGiveawayResultsSome': undefined;
|
||||
'ActionGiveawayResultsStarsSome': undefined;
|
||||
'ActionGiveawayResultsNone': undefined;
|
||||
'ActionOpenGiftButton': undefined;
|
||||
'ActionViewButton': undefined;
|
||||
'ActionGiveawayResultTitle': undefined;
|
||||
'ActionGiftPremiumText': undefined;
|
||||
'ActionGiftStarsText': undefined;
|
||||
'ActionHistoryCleared': undefined;
|
||||
'UniqueStatusBenefitsDescription': undefined;
|
||||
'UniqueStatusBadgeBenefitTitle': undefined;
|
||||
'UniqueStatusBadgeDescription': undefined;
|
||||
@ -1339,13 +1401,6 @@ export interface LangPair {
|
||||
}
|
||||
|
||||
export interface LangPairWithVariables<V extends unknown = LangVariable> {
|
||||
'ChatServiceGroupUpdatedPinnedMessage1': {
|
||||
'user': V;
|
||||
'message': V;
|
||||
};
|
||||
'MessagePinnedGenericMessage': {
|
||||
'user': V;
|
||||
};
|
||||
'UserTyping': {
|
||||
'user': V;
|
||||
};
|
||||
@ -1791,16 +1846,6 @@ export interface LangPairWithVariables<V extends unknown = LangVariable> {
|
||||
'StarsReactionTerms': {
|
||||
'link': V;
|
||||
};
|
||||
'ActionStarGiftPeerTitle': {
|
||||
'peer': V;
|
||||
'count': V;
|
||||
};
|
||||
'ActionStarGiftOutTitle': {
|
||||
'count': V;
|
||||
};
|
||||
'ActionStarGiftPeerOutDescriptionUpgrade': {
|
||||
'peer': V;
|
||||
};
|
||||
'StarsSubscribeInfo': {
|
||||
'link': V;
|
||||
};
|
||||
@ -1850,6 +1895,318 @@ export interface LangPairWithVariables<V extends unknown = LangVariable> {
|
||||
'MoreSimilarBotsText': {
|
||||
'count': V;
|
||||
};
|
||||
'ActionPinnedText': {
|
||||
'from': V;
|
||||
'text': V;
|
||||
};
|
||||
'ActionPinnedTextYou': {
|
||||
'text': V;
|
||||
};
|
||||
'ActionPinnedNotFound': {
|
||||
'from': V;
|
||||
};
|
||||
'ActionPinnedMedia': {
|
||||
'from': V;
|
||||
'media': V;
|
||||
};
|
||||
'ActionPinnedMediaYou': {
|
||||
'media': V;
|
||||
};
|
||||
'ActionPinnedMediaGame': {
|
||||
'game': V;
|
||||
};
|
||||
'ActionGroupCallStartedGroup': {
|
||||
'from': V;
|
||||
};
|
||||
'ActionGroupCallScheduledGroup': {
|
||||
'from': V;
|
||||
'date': V;
|
||||
};
|
||||
'ActionGroupCallScheduledChannel': {
|
||||
'date': V;
|
||||
};
|
||||
'ActionGroupCallFinishedChannel': {
|
||||
'duration': V;
|
||||
};
|
||||
'ActionGroupCallFinishedGroup': {
|
||||
'from': V;
|
||||
'duration': V;
|
||||
};
|
||||
'ActionAddUser': {
|
||||
'from': V;
|
||||
'user': V;
|
||||
};
|
||||
'ActionAddUserYou': {
|
||||
'user': V;
|
||||
};
|
||||
'ActionAddUsersMany': {
|
||||
'from': V;
|
||||
'users': V;
|
||||
};
|
||||
'ActionAddUsersManyYou': {
|
||||
'users': V;
|
||||
};
|
||||
'ActionAddYou': {
|
||||
'from': V;
|
||||
};
|
||||
'ActionAddYouGroup': {
|
||||
'from': V;
|
||||
};
|
||||
'ActionKickUser': {
|
||||
'from': V;
|
||||
'user': V;
|
||||
};
|
||||
'ActionKickUserYou': {
|
||||
'user': V;
|
||||
};
|
||||
'ActionUserLeft': {
|
||||
'from': V;
|
||||
};
|
||||
'ActionUserJoined': {
|
||||
'from': V;
|
||||
};
|
||||
'ActionUserJoinedByLink': {
|
||||
'from': V;
|
||||
};
|
||||
'ActionJoinedByRequest': {
|
||||
'from': V;
|
||||
};
|
||||
'ActionVideoInvited': {
|
||||
'from': V;
|
||||
'user': V;
|
||||
};
|
||||
'ActionVideoInvitedYou': {
|
||||
'user': V;
|
||||
};
|
||||
'ActionVideoInvitedMany': {
|
||||
'from': V;
|
||||
'users': V;
|
||||
};
|
||||
'ActionVideoInvitedManyYou': {
|
||||
'users': V;
|
||||
};
|
||||
'ActionUserRegistered': {
|
||||
'from': V;
|
||||
};
|
||||
'ActionRemovedPhoto': {
|
||||
'from': V;
|
||||
};
|
||||
'ActionChangedPhoto': {
|
||||
'from': V;
|
||||
};
|
||||
'ActionChangedTitle': {
|
||||
'from': V;
|
||||
'title': V;
|
||||
};
|
||||
'ActionChangedTitleYou': {
|
||||
'title': V;
|
||||
};
|
||||
'ActionChangedTitleChannel': {
|
||||
'title': V;
|
||||
};
|
||||
'ActionCreatedChat': {
|
||||
'from': V;
|
||||
'title': V;
|
||||
};
|
||||
'ActionPaymentDone': {
|
||||
'amount': V;
|
||||
'user': V;
|
||||
};
|
||||
'ActionPaymentDoneFor': {
|
||||
'amount': V;
|
||||
'user': V;
|
||||
'invoice': V;
|
||||
};
|
||||
'ActionPaymentInitRecurringFor': {
|
||||
'amount': V;
|
||||
'user': V;
|
||||
'invoice': V;
|
||||
};
|
||||
'ActionPaymentInitRecurring': {
|
||||
'amount': V;
|
||||
'user': V;
|
||||
};
|
||||
'ActionPaymentUsedRecurring': {
|
||||
'amount': V;
|
||||
};
|
||||
'ActionScreenshotTaken': {
|
||||
'from': V;
|
||||
};
|
||||
'ActionBotAllowedFromDomain': {
|
||||
'domain': V;
|
||||
};
|
||||
'ActionBotAllowedFromApp': {
|
||||
'app': V;
|
||||
};
|
||||
'ActionGiftTextCost': {
|
||||
'from': V;
|
||||
'cost': V;
|
||||
};
|
||||
'ActionGiftTextCostYou': {
|
||||
'cost': V;
|
||||
};
|
||||
'ActionGiftTextCostAnonymous': {
|
||||
'cost': V;
|
||||
};
|
||||
'ActionCostCrypto': {
|
||||
'price': V;
|
||||
'cryptoPrice': V;
|
||||
};
|
||||
'ActionWebviewDataDone': {
|
||||
'text': V;
|
||||
};
|
||||
'ActionGiftUniqueReceived': {
|
||||
'user': V;
|
||||
};
|
||||
'ActionStarGiftReceived': {
|
||||
'user': V;
|
||||
'cost': V;
|
||||
};
|
||||
'ActionStarGiftSent': {
|
||||
'cost': V;
|
||||
};
|
||||
'ActionStarGiftUpgradedUser': {
|
||||
'user': V;
|
||||
};
|
||||
'ActionStarGiftUpgradedChannel': {
|
||||
'user': V;
|
||||
'channel': V;
|
||||
};
|
||||
'ActionStarGiftUpgradedChannelYou': {
|
||||
'channel': V;
|
||||
};
|
||||
'ActionStarGiftUpgradedMine': {
|
||||
'user': V;
|
||||
};
|
||||
'ActionStarGiftTransferred': {
|
||||
'user': V;
|
||||
};
|
||||
'ActionStarGiftTransferredChannel': {
|
||||
'user': V;
|
||||
'channel': V;
|
||||
};
|
||||
'ActionStarGiftTransferredChannelYou': {
|
||||
'channel': V;
|
||||
};
|
||||
'ActionStarGiftTransferredMine': {
|
||||
'user': V;
|
||||
};
|
||||
'ActionStarGiftTransferredUnknownChannel': {
|
||||
'channel': V;
|
||||
};
|
||||
'ActionStarGiftReceivedAnonymous': {
|
||||
'cost': V;
|
||||
};
|
||||
'ActionStarGiftSentChannel': {
|
||||
'user': V;
|
||||
'channel': V;
|
||||
'cost': V;
|
||||
};
|
||||
'ActionStarGiftSentChannelYou': {
|
||||
'channel': V;
|
||||
'cost': V;
|
||||
};
|
||||
'ActionStarGiftSelfBought': {
|
||||
'cost': V;
|
||||
};
|
||||
'ActionStarGiftTo': {
|
||||
'peer': V;
|
||||
};
|
||||
'ActionStarGiftFrom': {
|
||||
'peer': V;
|
||||
};
|
||||
'ActionStarGiftConvertText': {
|
||||
'peer': V;
|
||||
'amount': V;
|
||||
};
|
||||
'ActionStarGiftConvertTextYou': {
|
||||
'amount': V;
|
||||
};
|
||||
'ActionStarGiftNoConvertText': {
|
||||
'peer': V;
|
||||
};
|
||||
'ActionStarGiftConvertedText': {
|
||||
'peer': V;
|
||||
'amount': V;
|
||||
};
|
||||
'ActionStarGiftConvertedTextYou': {
|
||||
'amount': V;
|
||||
};
|
||||
'ActionStarGiftChannelText': {
|
||||
'amount': V;
|
||||
};
|
||||
'ActionStarGiftUpgradeText': {
|
||||
'peer': V;
|
||||
};
|
||||
'ActionStarGiftLimitedRibbon': {
|
||||
'total': V;
|
||||
};
|
||||
'ActionSuggestedPhotoYou': {
|
||||
'user': V;
|
||||
};
|
||||
'ActionSuggestedPhoto': {
|
||||
'user': V;
|
||||
};
|
||||
'ActionTopicCreated': {
|
||||
'topic': V;
|
||||
};
|
||||
'ActionTopicClosed': {
|
||||
'from': V;
|
||||
'topic': V;
|
||||
};
|
||||
'ActionTopicReopened': {
|
||||
'from': V;
|
||||
'topic': V;
|
||||
};
|
||||
'ActionTopicHidden': {
|
||||
'topic': V;
|
||||
};
|
||||
'ActionTopicUnhidden': {
|
||||
'topic': V;
|
||||
};
|
||||
'ActionTopicRenamed': {
|
||||
'from': V;
|
||||
'link': V;
|
||||
'title': V;
|
||||
};
|
||||
'ActionTopicIconChanged': {
|
||||
'from': V;
|
||||
'link': V;
|
||||
'emoji': V;
|
||||
};
|
||||
'ActionTopicIconRemoved': {
|
||||
'from': V;
|
||||
'link': V;
|
||||
};
|
||||
'ActionTopicIconAndRenamed': {
|
||||
'from': V;
|
||||
'link': V;
|
||||
'topic': V;
|
||||
};
|
||||
'ActionGiveawayStartedGroup': {
|
||||
'from': V;
|
||||
};
|
||||
'ActionGiveawayStarted': {
|
||||
'from': V;
|
||||
};
|
||||
'ActionGiveawayStarsStartedGroup': {
|
||||
'from': V;
|
||||
'amount': V;
|
||||
};
|
||||
'ActionGiveawayStarsStarted': {
|
||||
'from': V;
|
||||
'amount': V;
|
||||
};
|
||||
'ActionPaymentRefunded': {
|
||||
'peer': V;
|
||||
'amount': V;
|
||||
};
|
||||
'ActionMigratedFrom': {
|
||||
'chat': V;
|
||||
};
|
||||
'ActionMigratedTo': {
|
||||
'chat': V;
|
||||
};
|
||||
'UniqueStatusWearTitle': {
|
||||
'gift': V;
|
||||
};
|
||||
@ -2043,13 +2400,6 @@ export interface LangPairPluralWithVariables<V extends unknown = LangVariable> {
|
||||
'PrizeCredits2': {
|
||||
'count': V;
|
||||
};
|
||||
'ActionStarGiftPeerOutDescription': {
|
||||
'peer': V;
|
||||
'count': V;
|
||||
};
|
||||
'ActionStarGiftDescription2': {
|
||||
'count': V;
|
||||
};
|
||||
'StarsSubscribeText': {
|
||||
'chat': V;
|
||||
'amount': V;
|
||||
@ -2074,6 +2424,53 @@ export interface LangPairPluralWithVariables<V extends unknown = LangVariable> {
|
||||
'FolderLinkNotificationUpdatedSubtitle': {
|
||||
'count': V;
|
||||
};
|
||||
'ActionGameScore': {
|
||||
'from': V;
|
||||
'count': V;
|
||||
'game': V;
|
||||
};
|
||||
'ActionGameScoreYou': {
|
||||
'count': V;
|
||||
'game': V;
|
||||
};
|
||||
'ActionGameScoreNoGame': {
|
||||
'from': V;
|
||||
'count': V;
|
||||
};
|
||||
'ActionGameScoreNoGameYou': {
|
||||
'count': V;
|
||||
};
|
||||
'ActionGiveawayResults': {
|
||||
'count': V;
|
||||
};
|
||||
'ActionGiveawayResultsStars': {
|
||||
'count': V;
|
||||
};
|
||||
'ActionGiveawayResultPremiumText': {
|
||||
'channel': V;
|
||||
'months': V;
|
||||
};
|
||||
'ActionGiftCodePremiumText': {
|
||||
'channel': V;
|
||||
'months': V;
|
||||
};
|
||||
'ActionGiveawayResultStarsText': {
|
||||
'channel': V;
|
||||
'amount': V;
|
||||
};
|
||||
'ActionGiftPremiumTitle': {
|
||||
'months': V;
|
||||
};
|
||||
'ActionGiftStarsTitle': {
|
||||
'amount': V;
|
||||
};
|
||||
'ActionBoostApplyYou': {
|
||||
'count': V;
|
||||
};
|
||||
'ActionBoostApply': {
|
||||
'from': V;
|
||||
'count': V;
|
||||
};
|
||||
}
|
||||
export type RegularLangKey = keyof LangPair;
|
||||
export type RegularLangKeyWithVariables = keyof LangPairWithVariables;
|
||||
|
||||
@ -1,5 +1,22 @@
|
||||
type Part = {
|
||||
type: 'literal' | 'element';
|
||||
value: string;
|
||||
};
|
||||
|
||||
export function getBasicListFormat() {
|
||||
return {
|
||||
format: (items: string[]) => items.join(', '),
|
||||
formatToParts: (items: string[]): Part[] => {
|
||||
const result: Part[] = [];
|
||||
|
||||
items.forEach((item, i) => {
|
||||
if (i > 0) {
|
||||
result.push({ type: 'literal', value: ', ' });
|
||||
}
|
||||
result.push({ type: 'element', value: item });
|
||||
});
|
||||
|
||||
return result;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@ -192,6 +192,12 @@ export function getColorLuma(rgbColor: [number, number, number]) {
|
||||
const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b;
|
||||
return luma;
|
||||
}
|
||||
// https://stackoverflow.com/a/64090995
|
||||
export function hsl2rgb([h, s, l]: [number, number, number]): [number, number, number] {
|
||||
let a = s * Math.min(l, 1 - l);
|
||||
let f = (n: number, k = (n + h / 30) % 12) => l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
|
||||
return [f(0), f(8), f(4)];
|
||||
}
|
||||
|
||||
// Function was adapted from https://github.com/telegramdesktop/tdesktop/blob/35ff621b5b52f7e3553fb0f990ea13ade7101b8e/Telegram/SourceFiles/data/data_wall_paper.cpp#L518
|
||||
export function getPatternColor(rgbColor: [number, number, number]) {
|
||||
@ -202,7 +208,9 @@ export function getPatternColor(rgbColor: [number, number, number]) {
|
||||
? Math.max(0, value * 0.65)
|
||||
: Math.max(0, Math.min(1, 1 - value * 0.65));
|
||||
|
||||
return `hsla(${hue * 360}, ${saturation * 100}%, ${value * 100}%, .4)`;
|
||||
const rgb = hsl2rgb([hue * 360, saturation, value]);
|
||||
const hex = rgb2hex(rgb.map((c) => Math.floor(c * 255)) as [number, number, number]);
|
||||
return `#${hex}66`;
|
||||
}
|
||||
|
||||
/* eslint-disable no-bitwise */
|
||||
|
||||
@ -11,6 +11,10 @@ export default function readStrings(data: string): Record<string, string> {
|
||||
console.warn('Bad formatting in line:', line);
|
||||
continue;
|
||||
}
|
||||
if (result[key]) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn('Duplicate key:', key);
|
||||
}
|
||||
result[key] = value;
|
||||
}
|
||||
return result;
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user