diff --git a/src/api/gramjs/apiBuilders/messageActions.ts b/src/api/gramjs/apiBuilders/messageActions.ts
new file mode 100644
index 000000000..f624ccbc7
--- /dev/null
+++ b/src/api/gramjs/apiBuilders/messageActions.ts
@@ -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';
+}
diff --git a/src/api/gramjs/apiBuilders/messageContent.ts b/src/api/gramjs/apiBuilders/messageContent.ts
index 5e1887c5f..6154d84fe 100644
--- a/src/api/gramjs/apiBuilders/messageContent.ts
+++ b/src/api/gramjs/apiBuilders/messageContent.ts
@@ -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 {
diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts
index f93bcb725..b7d92342f 100644
--- a/src/api/gramjs/apiBuilders/messages.ts
+++ b/src/api/gramjs/apiBuilders/messages.ts
@@ -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 | 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;
diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts
index baea62719..6020ed85c 100644
--- a/src/api/gramjs/methods/chats.ts
+++ b/src/api/gramjs/methods/chats.ts
@@ -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;
diff --git a/src/api/gramjs/methods/users.ts b/src/api/gramjs/methods/users.ts
index 8cb868c43..620e67896 100644
--- a/src/api/gramjs/methods/users.ts
+++ b/src/api/gramjs/methods/users.ts
@@ -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,
};
}
diff --git a/src/api/types/calls.ts b/src/api/types/calls.ts
index 3a1abb0de..85af8a151 100644
--- a/src/api/types/calls.ts
+++ b/src/api/types/calls.ts
@@ -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';
diff --git a/src/api/types/chats.ts b/src/api/types/chats.ts
index 7165de167..b09e0a7e4 100644
--- a/src/api/types/chats.ts
+++ b/src/api/types/chats.ts
@@ -122,7 +122,7 @@ export interface ApiChatFullInfo {
};
joinInfo?: {
joinedDate: number;
- inviter?: string;
+ inviterId?: string;
isViaRequest?: boolean;
};
linkedChatId?: string;
diff --git a/src/api/types/messageActions.ts b/src/api/types/messageActions.ts
new file mode 100644
index 000000000..8b2d64573
--- /dev/null
+++ b/src/api/types/messageActions.ts
@@ -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;
+ duration?: number;
+}
+
+export interface ApiMessageActionInviteToGroupCall extends ActionMediaType {
+ type: 'inviteToGroupCall';
+ call: Pick;
+ userIds: string[];
+}
+
+export interface ApiMessageActionGroupCallScheduled extends ActionMediaType {
+ type: 'groupCallScheduled';
+ call: Pick;
+ 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;
diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts
index 8e89c654d..13f887ae1 100644
--- a/src/api/types/messages.ts
+++ b/src/api/types/messages.ts
@@ -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;
- 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 = {
diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings
index 8e2c5cc47..e0edff25d 100644
--- a/src/assets/localization/fallback.strings
+++ b/src/assets/localization/fallback.strings
@@ -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";
diff --git a/src/components/common/CustomEmoji.tsx b/src/components/common/CustomEmoji.tsx
index 678983a29..fc7e76bea 100644
--- a/src/components/common/CustomEmoji.tsx
+++ b/src/components/common/CustomEmoji.tsx
@@ -88,7 +88,7 @@ const CustomEmoji: FC = ({
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;
diff --git a/src/components/common/MessageSummary.tsx b/src/components/common/MessageSummary.tsx
index b25fdd9d3..8f2527f63 100644
--- a/src/components/common/MessageSummary.tsx
+++ b/src/components/common/MessageSummary.tsx
@@ -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 ;
+ }
+
return (
{
+ return (
+
+ {data.map(([key, value]) => (
+ <>
+
{key}
+
{value}
+ >
+ ))}
+
+ );
+};
+
+export default memo(MiniTable);
diff --git a/src/components/common/StickerButton.tsx b/src/components/common/StickerButton.tsx
index 28480b8be..208e6b3a5 100644
--- a/src/components/common/StickerButton.tsx
+++ b/src/components/common/StickerButton.tsx
@@ -103,7 +103,7 @@ const StickerButton = (null);
const lang = useOldLang();
const hasCustomColor = sticker.shouldUseTextColor;
- const customColor = useDynamicColorListener(ref, !hasCustomColor);
+ const customColor = useDynamicColorListener(ref, undefined, !hasCustomColor);
const {
id, stickerSetInfo,
diff --git a/src/components/common/StickerView.tsx b/src/components/common/StickerView.tsx
index 20acc75bf..356e4c371 100644
--- a/src/components/common/StickerView.tsx
+++ b/src/components/common/StickerView.tsx
@@ -124,7 +124,7 @@ const StickerView: FC = ({
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;
diff --git a/src/components/common/TopicDefaultIcon.tsx b/src/components/common/TopicDefaultIcon.tsx
index 1b0daffbf..f8c96f5d8 100644
--- a/src/components/common/TopicDefaultIcon.tsx
+++ b/src/components/common/TopicDefaultIcon.tsx
@@ -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;
diff --git a/src/components/common/WebLink.tsx b/src/components/common/WebLink.tsx
index 0d0b91a4a..c85634bfd 100644
--- a/src/components/common/WebLink.tsx
+++ b/src/components/common/WebLink.tsx
@@ -53,7 +53,7 @@ const WebLink: FC = ({
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;
diff --git a/src/components/common/embedded/EmbeddedMessage.tsx b/src/components/common/embedded/EmbeddedMessage.tsx
index 5c6df6eff..ba46243ce 100644
--- a/src/components/common/embedded/EmbeddedMessage.tsx
+++ b/src/components/common/embedded/EmbeddedMessage.tsx
@@ -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 = ({
return renderTextWithEntities({
text: replyInfo.quoteText.text,
entities: replyInfo.quoteText.entities,
- noLineBreaks: isInComposer,
+ asPreview: true,
emojiSize: EMOJI_SIZE,
});
}
@@ -142,17 +140,6 @@ const EmbeddedMessage: FC = ({
return customText || renderMediaContentType(containedMedia) || NBSP;
}
- if (isActionMessage(message)) {
- return (
-
- );
- }
-
return (
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
- ?
- : '';
- 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 ?
- : topic ? : '...',
- );
- 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 = (
-
- );
-
- if (isEmbedded) {
- return messageSummary;
- }
-
- return (
- {messageSummary}
- );
-}
-
-function renderGroupCallContent(groupCall: Partial, text: TextPart[]): string | TextPart | undefined {
- return (
-
- {text}
-
- );
-}
-
-function renderUserContent(sender: ApiUser, noLinks?: boolean): string | TextPart | undefined {
- const text = trimText(getUserFullName(sender), MAX_LENGTH);
-
- if (noLinks) {
- return renderText(text!);
- }
-
- return {sender && renderText(text!)};
-}
-
-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 {chat && renderText(text!)};
-}
-
-function renderMigratedContent(chatId: string, noLinks?: boolean): string | TextPart | undefined {
- const text = 'another chat';
-
- if (noLinks) {
- return text;
- }
-
- return {text};
-}
-
-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();
-}
diff --git a/src/components/common/helpers/renderMessageText.ts b/src/components/common/helpers/renderMessageText.ts
index 24965ad16..738df237b 100644
--- a/src/components/common/helpers/renderMessageText.ts
+++ b/src/components/common/helpers/renderMessageText.ts
@@ -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);
diff --git a/src/components/common/helpers/renderTextWithEntities.tsx b/src/components/common/helpers/renderTextWithEntities.tsx
index 64295c17d..6e86fed0e 100644
--- a/src/components/common/helpers/renderTextWithEntities.tsx
+++ b/src/components/common/helpers/renderTextWithEntities.tsx
@@ -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 {text};
diff --git a/src/components/left/main/Chat.tsx b/src/components/left/main/Chat.tsx
index 47fc37120..4144f2c11 100644
--- a/src/components/left/main/Chat.tsx
+++ b/src/components/left/main/Chat.tsx
@@ -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 = ({
isMuted,
user,
userStatus,
- actionTargetUserIds,
lastMessageSender,
lastMessageOutgoingStatus,
- actionTargetMessage,
- actionTargetChatId,
offsetTop,
draft,
withInterfaceAnimations,
@@ -191,10 +183,7 @@ const Chat: FC = ({
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(
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(
chat,
isMuted: selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global)),
lastMessageSender,
- actionTargetUserIds,
- actionTargetChatId,
- actionTargetMessage,
draft: selectDraft(global, chatId, MAIN_THREAD_ID),
isSelected,
isSelectedForum,
diff --git a/src/components/left/main/Topic.tsx b/src/components/left/main/Topic.tsx
index d359bc975..d5bc2293f 100644
--- a/src/components/left/main/Topic.tsx
+++ b/src/components/left/main/Topic.tsx
@@ -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 = ({
lastMessageOutgoingStatus,
observeIntersection,
canDelete,
- actionTargetMessage,
- actionTargetUserIds,
- actionTargetChatId,
lastMessageSender,
animationType,
withInterfaceAnimations,
@@ -136,16 +129,13 @@ const Topic: FC = ({
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(
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(
return {
chat,
lastMessage,
- actionTargetUserIds,
- actionTargetChatId,
- actionTargetMessage,
lastMessageSender,
typingStatus,
canDelete: selectCanDeleteTopic(global, chatId, topic.id),
diff --git a/src/components/left/main/hooks/useChatListEntry.tsx b/src/components/left/main/hooks/useChatListEntry.tsx
index bde9a8c81..dadbee0a0 100644
--- a/src/components/left/main/hooks/useChatListEntry.tsx
+++ b/src/components/left/main/hooks/useChatListEntry.tsx
@@ -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(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,
})}
@@ -146,34 +117,6 @@ export default function useChatListEntry({
return undefined;
}
- if (isExpiredMessage(lastMessage)) {
- return (
-
- {getExpiredMessageDescription(oldLang, lastMessage)}
-
- );
- }
-
- if (isAction) {
- return (
-
- {renderActionMessageText(
- oldLang,
- lastMessage,
- lastMessageSender && !isApiPeerChat(lastMessageSender) ? lastMessageSender : undefined,
- lastMessageSender && isApiPeerChat(lastMessageSender) ? lastMessageSender : chat,
- actionTargetUsers,
- actionTargetMessage,
- actionTargetChatId,
- lastMessageTopic,
- { isEmbedded: true },
- undefined,
- undefined,
- )}
-
- );
- }
-
const senderName = getMessageSenderName(oldLang, chatId, lastMessageSender);
return (
@@ -190,9 +133,8 @@ export default function useChatListEntry({
);
}, [
- 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() {
diff --git a/src/components/main/premium/PremiumFeatureModal.tsx b/src/components/main/premium/PremiumFeatureModal.tsx
index 4832a2104..f8b2c9c68 100644
--- a/src/components/main/premium/PremiumFeatureModal.tsx
+++ b/src/components/main/premium/PremiumFeatureModal.tsx
@@ -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 = ({
onBack,
onClickSubscribe,
}) => {
- const lang = useOldLang();
+ const oldLang = useOldLang();
+ const lang = useLang();
// eslint-disable-next-line no-null/no-null
const scrollContainerRef = useRef(null);
const [currentSlideIndex, setCurrentSlideIndex] = useState(PREMIUM_FEATURE_SECTIONS.indexOf(initialSection));
@@ -148,7 +150,9 @@ const PremiumFeatureModal: FC = ({
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 = ({
className={buildClassName(styles.backButton, currentSlideIndex !== 0 && styles.whiteBackButton)}
color={currentSlideIndex === 0 ? 'translucent' : 'translucent-white'}
onClick={onBack}
- ariaLabel={lang('Back')}
+ ariaLabel={oldLang('Back')}
>
@@ -234,7 +238,7 @@ const PremiumFeatureModal: FC = ({
return (
- {lang(PREMIUM_FEATURE_TITLES.double_limits)}
+ {oldLang(PREMIUM_FEATURE_TITLES.double_limits)}
{PREMIUM_LIMITS_ORDER.map((limit, i) => {
@@ -242,8 +246,8 @@ const PremiumFeatureModal: FC
= ({
const premiumLimit = limits?.[limit][1].toString();
return (
= ({
- {lang(PREMIUM_FEATURE_TITLES.premium_stickers)}
+ {oldLang(PREMIUM_FEATURE_TITLES.premium_stickers)}
- {renderText(lang(PREMIUM_FEATURE_DESCRIPTIONS.premium_stickers), ['br'])}
+ {renderText(oldLang(PREMIUM_FEATURE_DESCRIPTIONS.premium_stickers), ['br'])}
);
@@ -294,10 +298,10 @@ const PremiumFeatureModal: FC = ({
/>
- {lang(PREMIUM_FEATURE_TITLES[promo.videoSections[i]!])}
+ {oldLang(PREMIUM_FEATURE_TITLES[promo.videoSections[i]!])}
- {renderText(lang(PREMIUM_FEATURE_DESCRIPTIONS[promo.videoSections[i]!]), ['br'])}
+ {renderText(oldLang(PREMIUM_FEATURE_DESCRIPTIONS[promo.videoSections[i]!]), ['br'])}
);
diff --git a/src/components/main/premium/PremiumMainModal.tsx b/src/components/main/premium/PremiumMainModal.tsx
index 041004326..576674ffe 100644
--- a/src/components/main/premium/PremiumMainModal.tsx
+++ b/src/components/main/premium/PremiumMainModal.tsx
@@ -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 = ({
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(initialSection);
const [selectedSubscriptionOption, setSubscriptionOption] = useState();
@@ -262,11 +264,11 @@ const PremiumMainModal: FC = ({
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 = ({
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 = ({
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 = ({
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 = ({
}
return (
-
+
{renderTextWithEntities({
text: promo.statusText,
entities: promo.statusEntities,
@@ -369,7 +371,7 @@ const PremiumMainModal: FC
= ({
color="translucent"
// eslint-disable-next-line react/jsx-no-bind
onClick={() => closePremiumModal()}
- ariaLabel={lang('Close')}
+ ariaLabel={oldLang('Close')}
>
@@ -393,7 +395,7 @@ const PremiumMainModal: FC = ({
{!isPremium && !isGift && renderSubscriptionOptions()}
- {lang('TelegramPremium')}
+ {oldLang('TelegramPremium')}
@@ -401,11 +403,11 @@ const PremiumMainModal: FC
= ({
return (
= ({
})}
- {renderText(lang('AboutPremiumDescription'), ['simple_markdown'])}
+ {renderText(oldLang('AboutPremiumDescription'), ['simple_markdown'])}
- {renderText(lang('AboutPremiumDescription2'), ['simple_markdown'])}
+ {renderText(oldLang('AboutPremiumDescription2'), ['simple_markdown'])}
{renderFooterText()}
@@ -430,7 +432,7 @@ const PremiumMainModal: FC = ({
{!isPremium && selectedSubscriptionOption && (
)}
diff --git a/src/components/main/premium/PremiumSubscriptionOption.tsx b/src/components/main/premium/PremiumSubscriptionOption.tsx
index 71b9bddfc..5b6909d38 100644
--- a/src/components/main/premium/PremiumSubscriptionOption.tsx
+++ b/src/components/main/premium/PremiumSubscriptionOption.tsx
@@ -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 = ({
option, checked, fullMonthlyAmount,
onChange, className, isGiveaway,
}) => {
- const lang = useOldLang();
+ const oldLang = useOldLang();
const {
months, amount, currency,
@@ -52,7 +52,7 @@ const PremiumSubscriptionOption: FC = ({
(checked && !isGiveaway) && styles.active,
className,
)}
- dir={lang.isRtl ? 'rtl' : undefined}
+ dir={oldLang.isRtl ? 'rtl' : undefined}
>
= ({
{Boolean(discount) && (
−{discount}%
)}
- {lang('Months', months)}
+ {oldLang('Months', months)}
- {(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))}
- {formatCurrency(amount, currency, lang.code)}
+ {formatCurrencyAsString(amount, currency, oldLang.code)}
diff --git a/src/components/mediaViewer/helpers/getViewableMedia.ts b/src/components/mediaViewer/helpers/getViewableMedia.ts
index 98478434d..1155ed741 100644
--- a/src/components/mediaViewer/helpers/getViewableMedia.ts
+++ b/src/components/mediaViewer/helpers/getViewableMedia.ts
@@ -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,
};
}
diff --git a/src/components/mediaViewer/helpers/ghostAnimation.ts b/src/components/mediaViewer/helpers/ghostAnimation.ts
index 16a59b59f..04acf2041 100644
--- a/src/components/mediaViewer/helpers/ghostAnimation.ts
+++ b/src/components/mediaViewer/helpers/ghostAnimation.ts
@@ -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;
diff --git a/src/components/middle/ActionMessage.tsx b/src/components/middle/ActionMessage.tsx
deleted file mode 100644
index 903dbe094..000000000
--- a/src/components/middle/ActionMessage.tsx
+++ /dev/null
@@ -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
= ({
- 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(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) => {
- 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 ;
- }
-
- if (isEmbedded) {
- return {renderContent()};
- }
-
- function renderGift() {
- const giftMessage = message.content.action?.message;
- return (
-
-
- {oldLang('ActionGiftPremiumTitle')}
-
- {oldLang('ActionGiftPremiumSubtitle', oldLang('Months', message.content.action?.months, 'i'))}
-
- {giftMessage && (
-
- {renderTextWithEntities({ text: giftMessage.text, entities: giftMessage.entities })}
-
- )}
-
-
-
- {oldLang('ActionGiftPremiumView')}
-
-
- );
- }
-
- function renderGiftCode() {
- const isFromGiveaway = message.content.action?.isGiveaway;
- const isUnclaimed = message.content.action?.isUnclaimed;
- const giftMessage = message.content.action?.message;
- return (
-
-
-
- {oldLang(isUnclaimed ? 'BoostingUnclaimedPrize' : 'BoostingCongratulations')}
-
-
- {targetChat && renderText(
- oldLang(
- isFromGiveaway ? 'BoostingReceivedGiftFrom' : isUnclaimed
- ? 'BoostingReceivedPrizeFrom' : 'BoostingYouHaveUnclaimedPrize',
- getChatTitle(oldLang, targetChat),
- ),
- ['simple_markdown'],
- )}
-
-
- {renderText(oldLang(
- 'BoostingUnclaimedPrizeDuration',
- oldLang('Months', message.content.action?.months, 'i'),
- ), ['simple_markdown'])}
-
-
- {giftMessage && (
-
- {renderTextWithEntities({ text: giftMessage.text, entities: giftMessage.entities })}
-
- )}
-
-
- {oldLang('BoostingReceivedGiftOpenBtn')}
-
-
- );
- }
-
- function renderStarsGift() {
- return (
-
-
-
- {formatInteger(message.content.action!.stars!)}
- {oldLang('Stars')}
-
-
- {renderText(
- oldLang(!message.isOutgoing
- ? 'ActionGiftStarsSubtitleYou' : 'ActionGiftStarsSubtitle', getChatTitle(oldLang, targetChat!)),
- ['simple_markdown'],
- )}
-
-
-
- {oldLang('ActionGiftPremiumView')}
-
-
- );
- }
-
- 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 (
-
-
{lang('GiftTo')}
- {starGift.type === 'starGift' && (
-
- )}
-
{getPeerTitle(lang, targetPeer)}
-
- );
- }
-
- return (
-
-
{lang('GiftFrom')}
- {starGift.type === 'starGift' && (
-
- )}
-
{getPeerTitle(lang, fromPeer || senderUser!)}
-
- );
- }
-
- 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 (
-
-
-
-
- {renderStarGiftUserCaption()}
-
- {renderStarGiftUserDescription()}
-
-
-
-
- {starGift.alreadyPaidUpgradeStars && (!message.isOutgoing || targetUsers?.[0]?.isSelf)
- ? lang('ActionStarGiftUnpack') : oldLang('ActionGiftPremiumView')}
-
- {starGift.gift.availabilityTotal && (
-
- )}
-
- );
- }
-
- 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 (
-
-
-
-
-
- {renderStarGiftUserCaption()}
-
- {starGift.gift.title} #{starGift.gift.number}
-
-
-
- {oldLang('Gift2AttributeModel')}
-
-
- {model.name}
-
-
- {oldLang('Gift2AttributeBackdrop')}
-
-
- {backdrop.name}
-
-
- {oldLang('Gift2AttributeSymbol')}
-
-
- {pattern.name}
-
-
-
-
-
- {oldLang('Gift2UniqueView')}
-
-
-
- );
- }
-
- function renderPrizeStars() {
- const isUnclaimed = message.content.action?.isUnclaimed;
-
- return (
-
-
-
- {oldLang(isUnclaimed ? 'BoostingUnclaimedPrize' : 'BoostingCongratulations')}
-
-
- {targetChat && renderText(oldLang(isUnclaimed
- ? 'BoostingReceivedPrizeFrom' : 'BoostingYouHaveUnclaimedPrize', getChatTitle(oldLang, targetChat)),
- ['simple_markdown'])}
-
-
- {renderText(lang(
- 'PrizeCredits2', {
- count: (
- {formatInteger(message.content.action?.stars!)}
- ),
- }, {
- withNodes: true,
- pluralValue: message.content.action?.stars!,
- },
- ), ['simple_markdown'])}
-
- {
- oldLang('ActionGiftPremiumView')
- }
-
-
- );
- }
-
- 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 (
-
- {!isSuggestedAvatar && !isGiftCode && !isJoinedMessage && !isUpdatedAvatar && (
-
{renderContent()}
- )}
- {isPremiumGift && renderGift()}
- {isGiftCode && renderGiftCode()}
- {isStarsGift && renderStarsGift()}
- {isStarGift && renderStarGift()}
- {isStarGiftUnique && renderStarGiftUnique()}
- {isPrizeStars && renderPrizeStars()}
- {isSuggestedAvatar && (
-
- )}
- {isUpdatedAvatar && (
-
- )}
- {isJoinedMessage &&
}
- {contextMenuAnchor && (
-
- )}
- {withServiceReactions && (
-
- )}
-
- );
-};
-
-export default memo(withGlobal(
- (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));
diff --git a/src/components/middle/ActionMessageSuggestedAvatar.tsx b/src/components/middle/ActionMessageSuggestedAvatar.tsx
deleted file mode 100644
index ed067019a..000000000
--- a/src/components/middle/ActionMessageSuggestedAvatar.tsx
+++ /dev/null
@@ -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 = ({
- message,
- renderContent,
-}) => {
- const {
- openMediaViewer, uploadProfilePhoto, showNotification,
- } = getActions();
-
- const { isOutgoing } = message;
-
- const lang = useOldLang();
- const [cropModalBlob, setCropModalBlob] = useState();
- 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 (
-
-
- {renderContent()}
-
- {lang(isVideo ? 'ViewVideoAction' : 'ViewPhotoAction')}
-
-
-
- );
-};
-
-export default memo(ActionMessageSuggestedAvatar);
diff --git a/src/components/middle/ActionMessageUpdatedAvatar.tsx b/src/components/middle/ActionMessageUpdatedAvatar.tsx
deleted file mode 100644
index a885e060a..000000000
--- a/src/components/middle/ActionMessageUpdatedAvatar.tsx
+++ /dev/null
@@ -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 = ({
- 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 (
- <>
- {renderContent()}
-
-
-
- >
- );
-};
-
-export default memo(ActionMessageUpdatedAvatar);
diff --git a/src/components/middle/MessageList.scss b/src/components/middle/MessageList.scss
index ce6f38873..1d4cc8251 100644
--- a/src/components/middle/MessageList.scss
+++ b/src/components/middle/MessageList.scss
@@ -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;
}
diff --git a/src/components/middle/MessageList.tsx b/src/components/middle/MessageList.tsx
index ad9c6ae1b..7c756629d 100644
--- a/src/components/middle/MessageList.tsx
+++ b/src/components/middle/MessageList.tsx
@@ -290,11 +290,10 @@ const MessageList: FC = ({
isOutgoing: false,
content: {
action: {
- type: 'joinedChannel',
mediaType: 'action',
- text: '',
- translationValues: [],
- targetChatId: message.chatId,
+ type: 'channelJoined',
+ inviterId: channelJoinInfo?.inviterId,
+ isViaRequest: channelJoinInfo?.isViaRequest || undefined,
},
},
} satisfies ApiMessage);
diff --git a/src/components/middle/MessageListContent.tsx b/src/components/middle/MessageListContent.tsx
index ddf941a77..3e2754fb7 100644
--- a/src/components/middle/MessageListContent.tsx
+++ b/src/components/middle/MessageListContent.tsx
@@ -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 = ({
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 = ({
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}
/>,
]);
diff --git a/src/components/middle/MiddleColumn.tsx b/src/components/middle/MiddleColumn.tsx
index 81d9e96af..711dd2c3c 100644
--- a/src/components/middle/MiddleColumn.tsx
+++ b/src/components/middle/MiddleColumn.tsx
@@ -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,
diff --git a/src/components/middle/composer/StickerSetCover.tsx b/src/components/middle/composer/StickerSetCover.tsx
index 87022e816..1274a861f 100644
--- a/src/components/middle/composer/StickerSetCover.tsx
+++ b/src/components/middle/composer/StickerSetCover.tsx
@@ -53,7 +53,7 @@ const StickerSetCover: FC = ({
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);
diff --git a/src/components/middle/composer/hooks/useInputCustomEmojis.ts b/src/components/middle/composer/hooks/useInputCustomEmojis.ts
index 2f0f1c6f1..55307f2c4 100644
--- a/src/components/middle/composer/hooks/useInputCustomEmojis.ts
+++ b/src/components/middle/composer/hooks/useInputCustomEmojis.ts
@@ -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
diff --git a/src/components/middle/message/Message.tsx b/src/components/middle/message/Message.tsx
index d15bcc200..09b56450d 100644
--- a/src/components/middle/message/Message.tsx
+++ b/src/components/middle/message/Message.tsx
@@ -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 = ({
return;
}
- setTimeout(markShown, appearanceOrder * APPEARANCE_DELAY);
+ setTimeout(markShown, appearanceOrder * MESSAGE_APPEARANCE_DELAY);
}, [appearanceOrder, markShown, noAppearanceAnimation]);
useShowTransition({
@@ -742,7 +741,7 @@ const Message: FC = ({
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 = ({
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
diff --git a/src/components/middle/message/MessageContextMenu.tsx b/src/components/middle/message/MessageContextMenu.tsx
index c9dd67ca9..8639d5c4a 100644
--- a/src/components/middle/message/MessageContextMenu.tsx
+++ b/src/components/middle/message/MessageContextMenu.tsx
@@ -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 = ({
const copyOptions = getMessageCopyOptions(
message,
- groupStatetefulContent({ poll, story }),
+ groupStatefulContent({ poll, story }),
targetHref,
canCopy,
handleAfterCopy,
diff --git a/src/components/middle/message/MessagePhoneCall.tsx b/src/components/middle/message/MessagePhoneCall.tsx
index 273bed0c9..c05b714ab 100644
--- a/src/components/middle/message/MessagePhoneCall.tsx
+++ b/src/components/middle/message/MessagePhoneCall.tsx
@@ -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 = ({
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 = ({
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 = ({
-
{lang(reasonText)}
+
{lang(getCallMessageKey(phoneCall, message.isOutgoing))}
{
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, ,
);
- }, [lang, paidMedia]);
+ }, [oldLang, paidMedia]);
const handleClick = useLastCallback((e: React.MouseEvent) => {
openInvoice({
@@ -73,7 +75,9 @@ const PaidMediaOverlay = ({
)}
{paidMedia.isBought && (
- {isOutgoing ? formatCurrency(paidMedia.starsAmount, STARS_CURRENCY_CODE) : lang('Chat.PaidMedia.Purchased')}
+ {isOutgoing
+ ? formatStarsAsIcon(lang, paidMedia.starsAmount)
+ : oldLang('Chat.PaidMedia.Purchased')}
)}
diff --git a/src/components/middle/message/SimilarChannels.module.scss b/src/components/middle/message/SimilarChannels.module.scss
index c27d4b2dc..8d8f4a214 100644
--- a/src/components/middle/message/SimilarChannels.module.scss
+++ b/src/components/middle/message/SimilarChannels.module.scss
@@ -18,10 +18,6 @@
fill: var(--color-background);
}
-.join-text {
- cursor: pointer;
-}
-
.header {
padding: 0.375rem 0.375rem 0 0.75rem;
display: flex;
diff --git a/src/components/middle/message/SimilarChannels.tsx b/src/components/middle/message/SimilarChannels.tsx
index 479b7d8f1..35ba4f7c2 100644
--- a/src/components/middle/message/SimilarChannels.tsx
+++ b/src/components/middle/message/SimilarChannels.tsx
@@ -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
(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 (
-
-
- {lang('ChannelJoined')}
-
-
{shouldRenderSkeleton &&
}
{shouldRenderChannels && (
= ({
message,
@@ -98,8 +96,6 @@ const WebPage: FC
= ({
const { isMobile } = useAppLayout();
// eslint-disable-next-line no-null/no-null
const stickersRef = useRef(null);
- // eslint-disable-next-line no-null/no-null
- const giftStickersRef = useRef(null);
const oldLang = useOldLang();
const lang = useLang();
@@ -125,7 +121,7 @@ const WebPage: FC = ({
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 = ({
);
}
- 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 (
- handleOpenTelegramLink()}
- >
-
-
-
-
-
-
-
- );
- }
-
return (
= ({
)}
{isGift && !inPreview && (
- renderStarGiftUnique()
+
)}
{isArticle && (
{
+ // eslint-disable-next-line no-null/no-null
+ const stickerRef = useRef
(null);
+ const {
+ backdrop, model, pattern,
+ } = getGiftAttributes(gift)!;
+
+ const backgroundColors = [backdrop!.centerColor, backdrop!.edgeColor];
+
+ return (
+
+ );
+};
+
+export default memo(WebPageUniqueGift);
diff --git a/src/components/middle/message/actions/ChannelPhoto.tsx b/src/components/middle/message/actions/ChannelPhoto.tsx
new file mode 100644
index 000000000..3ebb1fe1c
--- /dev/null
+++ b/src/components/middle/message/actions/ChannelPhoto.tsx
@@ -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 (
+
+ );
+};
+
+export default memo(ChannelPhotoAction);
diff --git a/src/components/middle/message/actions/Gift.tsx b/src/components/middle/message/actions/Gift.tsx
new file mode 100644
index 000000000..c6b2b9f03
--- /dev/null
+++ b/src/components/middle/message/actions/Gift.tsx
@@ -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(null);
+ const lang = useLang();
+ const message = action.type === 'giftPremium' ? action.message : undefined;
+
+ return (
+
+
+ {sticker && (
+
+ )}
+
+
+
+ {action.type === 'giftPremium' ? (
+ lang('ActionGiftPremiumTitle', { months: action.months }, { pluralValue: action.months })
+ ) : (
+ lang('ActionGiftStarsTitle', { amount: action.stars }, { pluralValue: action.stars })
+ )}
+
+
+ {message && renderTextWithEntities(message)}
+ {!message && (lang(action.type === 'giftPremium' ? 'ActionGiftPremiumText' : 'ActionGiftStarsText'))}
+
+
+
+
+ {lang('ActionViewButton')}
+
+
+ );
+};
+
+export default memo(withGlobal(
+ (global, { action }): StateProps => {
+ const sticker = action.type === 'giftPremium'
+ ? selectGiftStickerForDuration(global, action.months)
+ : selectGiftStickerForStars(global, action.stars);
+ const canPlayAnimatedEmojis = selectCanPlayAnimatedEmojis(global);
+
+ return {
+ sticker,
+ canPlayAnimatedEmojis,
+ };
+ },
+)(GiftAction));
diff --git a/src/components/middle/message/actions/GiveawayPrize.tsx b/src/components/middle/message/actions/GiveawayPrize.tsx
new file mode 100644
index 000000000..d932f6ba7
--- /dev/null
+++ b/src/components/middle/message/actions/GiveawayPrize.tsx
@@ -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(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 (
+
+
+ {sticker && (
+
+ )}
+
+
+
{lang('ActionGiveawayResultTitle')}
+
+ {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'],
+ },
+ )
+ )}
+
+
+
+
+ {lang(action.type === 'giftCode' ? 'ActionOpenGiftButton' : 'ActionViewButton')}
+
+
+ );
+};
+
+export default memo(withGlobal(
+ (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));
diff --git a/src/components/middle/message/actions/StarGift.tsx b/src/components/middle/message/actions/StarGift.tsx
new file mode 100644
index 000000000..db4438bf6
--- /dev/null
+++ b/src/components/middle/message/actions/StarGift.tsx
@@ -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(null);
+ // eslint-disable-next-line no-null/no-null
+ const stickerRef = useRef(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 (
+
+
+ {sticker && (
+
+ )}
+
+ {action.gift.availabilityTotal && (
+
+ )}
+
+
+ {isSelf ? lang('ActionStarGiftSelf') : lang(
+ isOutgoing ? 'ActionStarGiftTo' : 'ActionStarGiftFrom',
+ {
+ peer: renderPeerLink(peer?.id, peerTitle || fallbackPeerTitle),
+ },
+ {
+ withNodes: true,
+ },
+ )}
+
+
+ {action.message && renderTextWithEntities(action.message)}
+ {!action.message && giftDescription}
+
+
+
+
+ {action.alreadyPaidUpgradeStars && !action.isUpgraded && !isOutgoing
+ ? lang('ActionStarGiftUnpack') : lang('ActionViewButton')}
+
+
+ );
+};
+
+export default memo(withGlobal(
+ (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));
diff --git a/src/components/middle/message/actions/StarGiftUnique.tsx b/src/components/middle/message/actions/StarGiftUnique.tsx
new file mode 100644
index 000000000..d80058de7
--- /dev/null
+++ b/src/components/middle/message/actions/StarGiftUnique.tsx
@@ -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(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 (
+
+
+
+
+
+ {sticker && (
+
+ )}
+
+
+
+
+ {isSelf ? lang('ActionStarGiftSelf') : lang(
+ isOutgoing ? 'ActionStarGiftTo' : 'ActionStarGiftFrom',
+ {
+ peer: renderPeerLink(peer?.id, peerTitle || fallbackPeerTitle),
+ },
+ {
+ withNodes: true,
+ },
+ )}
+
+
+ {lang('GiftUnique', { title: action.gift.title, number: action.gift.number })}
+
+
+
+
+
+ {lang('ActionViewButton')}
+
+
+ );
+};
+
+export default memo(withGlobal(
+ (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));
diff --git a/src/components/middle/message/actions/SuggestedPhoto.tsx b/src/components/middle/message/actions/SuggestedPhoto.tsx
new file mode 100644
index 000000000..a986042f3
--- /dev/null
+++ b/src/components/middle/message/actions/SuggestedPhoto.tsx
@@ -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();
+ 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 (
+
+
+
+ {text}
+
+
+ {lang('ActionSuggestedPhotoButton')}
+
+
+
+
+ );
+};
+
+export default memo(withGlobal(
+ (global, { message }): StateProps => {
+ const peer = selectPeer(global, message.chatId);
+
+ return {
+ peer,
+ };
+ },
+)(SuggestedPhotoAction));
diff --git a/src/components/middle/message/helpers/messageActions.tsx b/src/components/middle/message/helpers/messageActions.tsx
new file mode 100644
index 000000000..ab10d393d
--- /dev/null
+++ b/src/components/middle/message/helpers/messageActions.tsx
@@ -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 = `${K & string}You` extends keyof T ? T[`${K & string}You`] : never;
+type VariablesForKey =
+ K extends RegularLangKeyWithVariables
+ ? LangPairWithVariables[K] | SuffixKey
+ : K extends PluralLangKeyWithVariables
+ ? LangPairPluralWithVariables[K] | SuffixKey
+ : undefined;
+
+export function translateWithOutgoing(
+ lang: LangFn,
+ key: K,
+ isOutgoing: boolean,
+ variables: VariablesForKey,
+ 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 (
+ {
+ e.stopPropagation();
+ getActions().openChat({ id: peerId });
+ }}
+ // box-decoration-break: clone; is broken when child has `dir` attribute
+ withMultilineFix={IS_SAFARI}
+ >
+ {renderText(text)}
+
+ );
+}
+
+export function renderMessageLink(targetMessage: ApiMessage, text: TeactNode, asPreview?: boolean) {
+ if (asPreview) return text;
+ return (
+ {
+ e.stopPropagation();
+ getActions().focusMessage({ chatId: targetMessage.chatId, messageId: targetMessage.id });
+ }}
+ withMultilineFix={IS_SAFARI}
+ >
+ {text}
+
+ );
+}
+
+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';
+ }
+}
diff --git a/src/components/middle/message/hooks/useFluidBackgroundFilter.tsx b/src/components/middle/message/hooks/useFluidBackgroundFilter.tsx
new file mode 100644
index 000000000..f975b4f7a
--- /dev/null
+++ b/src/components/middle/message/hooks/useFluidBackgroundFilter.tsx
@@ -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();
+
+class SvgFluidBackgroundFilter {
+ public filterId: string;
+
+ private referenceCount = 0;
+
+ constructor(public color: string) {
+ this.filterId = `fluid-background-filter-${color.slice(1)}`;
+
+ addSvgDefinition((
+
+
+
+
+
+
+
+ ), 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});`;
+}
diff --git a/src/components/middle/message/reactions/ReactionButton.module.scss b/src/components/middle/message/reactions/ReactionButton.module.scss
index 0e9714c91..0c9517f6e 100644
--- a/src/components/middle/message/reactions/ReactionButton.module.scss
+++ b/src/components/middle/message/reactions/ReactionButton.module.scss
@@ -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 {
diff --git a/src/components/middle/message/reactions/ReactionButton.tsx b/src/components/middle/message/reactions/ReactionButton.tsx
index 5493e8d6e..e94e305ac 100644
--- a/src/components/middle/message/reactions/ReactionButton.tsx
+++ b/src/components/middle/message/reactions/ReactionButton.tsx
@@ -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,
diff --git a/src/components/middle/message/reactions/Reactions.scss b/src/components/middle/message/reactions/Reactions.scss
index 9ff0c0dcf..d5025d634 100644
--- a/src/components/middle/message/reactions/Reactions.scss
+++ b/src/components/middle/message/reactions/Reactions.scss
@@ -21,7 +21,7 @@
&.is-service {
justify-content: center;
max-width: 19rem;
- margin: 0.3125rem auto;
+ margin-top: 0.3125rem;
}
.own &.is-outside {
diff --git a/src/components/middle/message/reactions/Reactions.tsx b/src/components/middle/message/reactions/Reactions.tsx
index f199c7884..143224f38 100644
--- a/src/components/middle/message/reactions/Reactions.tsx
+++ b/src/components/middle/message/reactions/Reactions.tsx
@@ -215,6 +215,7 @@ const Reactions: FC = ({
containerId={messageKey}
isOwnMessage={message.isOutgoing}
recentReactors={recentReactors}
+ isOutside={isOutside}
reaction={reaction}
onClick={handleClick}
onPaidClick={handlePaidClick}
diff --git a/src/components/modals/gift/GiftComposer.module.scss b/src/components/modals/gift/GiftComposer.module.scss
index 8cde3845e..f29869493 100644
--- a/src/components/modals/gift/GiftComposer.module.scss
+++ b/src/components/modals/gift/GiftComposer.module.scss
@@ -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 {
diff --git a/src/components/modals/gift/GiftComposer.tsx b/src/components/modals/gift/GiftComposer.tsx
index 1c82eeebb..9b8cddcab 100644
--- a/src/components/modals/gift/GiftComposer.tsx
+++ b/src/components/modals/gift/GiftComposer.tsx
@@ -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({
/>
)}
- {isStarGift && (
+ {isStarGift && gift.upgradeStars && (
{isPeerUser
? lang('GiftMakeUniqueDescription', {
@@ -237,7 +223,9 @@ function GiftComposer({
)}
{isStarGift && (
- {isPeerUser ? lang('GiftHideNameDescription', { receiver: title }) : lang('GiftHideNameDescriptionChannel')}
+ {isSelf ? lang('GiftHideNameDescriptionSelf')
+ : isPeerUser ? lang('GiftHideNameDescription', { receiver: title })
+ : lang('GiftHideNameDescriptionChannel')}
)}
@@ -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 (
@@ -264,6 +252,7 @@ function GiftComposer({
)}
{renderOptionsSection()}
diff --git a/src/components/modals/gift/upgrade/GiftUpgradeModal.tsx b/src/components/modals/gift/upgrade/GiftUpgradeModal.tsx
index aa2697872..cdf0ba606 100644
--- a/src/components/modals/gift/upgrade/GiftUpgradeModal.tsx
+++ b/src/components/modals/gift/upgrade/GiftUpgradeModal.tsx
@@ -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 (
= ({
- {formatCurrency(option.amount, option.currency, oldLang.code)}
+ {formatCurrency(lang, option.amount, option.currency)}
{(isActiveOption || (selectedStarOption && 'winners' in selectedStarOption)) && perUserStarCount && (
diff --git a/src/components/payment/Checkout.tsx b/src/components/payment/Checkout.tsx
index d80bffa29..5f3f9fd7d 100644
--- a/src/components/payment/Checkout.tsx
+++ b/src/components/payment/Checkout.tsx
@@ -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 = ({
}) => {
const { setPaymentStep } = getActions();
- const lang = useOldLang();
+ const oldLang = useOldLang();
+ const lang = useLang();
const isInteractive = Boolean(dispatch);
const {
@@ -117,7 +120,7 @@ const Checkout: FC = ({
{title}
- {formatCurrency(tipAmount!, invoice.currency, lang.code)}
+ {formatCurrency(lang, tipAmount!, invoice.currency)}
@@ -127,7 +130,7 @@ const Checkout: FC = ({
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 })}
))}
@@ -136,7 +139,7 @@ const Checkout: FC = ({
}
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 = ({
function renderTos(url: string) {
return (
= ({
{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)
)}
{!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
= ({
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 (
@@ -258,7 +261,7 @@ function renderPaymentItem(
{title}
- {formatCurrency(value, currency, langCode)}
+ {formatCurrency(lang, value, currency)}
);
diff --git a/src/components/payment/Shipping.tsx b/src/components/payment/Shipping.tsx
index d6eef30a7..c370583c4 100644
--- a/src/components/payment/Shipping.tsx
+++ b/src/components/payment/Shipping.tsx
@@ -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 = ({
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 = ({
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 (