[Refactoring] Action Message (#5578)

This commit is contained in:
zubiden 2025-03-01 17:59:16 +01:00 committed by Alexander Zinchuk
parent 5b2c325279
commit 1b66f75853
107 changed files with 4486 additions and 2859 deletions

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -122,7 +122,7 @@ export interface ApiChatFullInfo {
};
joinInfo?: {
joinedDate: number;
inviter?: string;
inviterId?: string;
isViaRequest?: boolean;
};
linkedChatId?: string;

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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')}
> &minus;{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>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,10 +18,6 @@
fill: var(--color-background);
}
.join-text {
cursor: pointer;
}
.header {
padding: 0.375rem 0.375rem 0 0.75rem;
display: flex;

View File

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

View File

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

View File

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

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

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

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

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

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

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

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

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

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

View File

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

View File

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

View File

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

View File

@ -21,7 +21,7 @@
&.is-service {
justify-content: center;
max-width: 19rem;
margin: 0.3125rem auto;
margin-top: 0.3125rem;
}
.own &.is-outside {

View File

@ -215,6 +215,7 @@ const Reactions: FC<OwnProps> = ({
containerId={messageKey}
isOwnMessage={message.isOutgoing}
recentReactors={recentReactors}
isOutside={isOutside}
reaction={reaction}
onClick={handleClick}
onPaidClick={handlePaidClick}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -70,6 +70,7 @@ export const INITIAL_PERFORMANCE_STATE_MIN: PerformanceType = {
};
export const INITIAL_GLOBAL_STATE: GlobalState = {
cacheVersion: 1,
isInited: true,
attachMenu: { bots: {} },
passcode: {},

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@ -300,6 +300,7 @@ export enum MediaViewerOrigin {
Album,
ScheduledAlbum,
SearchResult,
ChannelAvatar,
SuggestedAvatar,
StarsTransaction,
PreviewMedia,

View File

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

View File

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

View File

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

View File

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