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>(new Map()); diff --git a/src/components/middle/helpers/groupMessages.ts b/src/components/middle/helpers/groupMessages.ts index 38ff741e1..ca96ebba8 100644 --- a/src/components/middle/helpers/groupMessages.ts +++ b/src/components/middle/helpers/groupMessages.ts @@ -92,8 +92,8 @@ export function groupMessages( || message.senderId !== nextMessage.senderId || message.isOutgoing !== nextMessage.isOutgoing || message.postAuthorTitle !== nextMessage.postAuthorTitle - || (isActionMessage(message) && !message.content.action?.phoneCall) - || (isActionMessage(nextMessage) && !nextMessage.content.action?.phoneCall) + || (isActionMessage(message) && message.content.action?.type !== 'phoneCall') + || (isActionMessage(nextMessage) && nextMessage.content.action?.type !== 'phoneCall') || message.inlineButtons || nextMessage.inlineButtons || (nextMessage.date - message.date) > GROUP_INTERVAL_SECONDS diff --git a/src/components/middle/message/ActionMessage.module.scss b/src/components/middle/message/ActionMessage.module.scss new file mode 100644 index 000000000..8a74178b2 --- /dev/null +++ b/src/components/middle/message/ActionMessage.module.scss @@ -0,0 +1,184 @@ +.root { + display: grid; + grid-template-columns: minmax(0, 1fr); + justify-items: center; + + padding-block: 0.125rem; + + word-break: break-word; + + :global(.star-amount-icon) { + vertical-align: text-bottom; + margin-inline: 0; + } +} + +.contentBox { + display: flex; + flex-direction: column; + align-items: center; + gap: 0.5rem; + max-width: 15rem; + + padding: 0.5rem 0.75rem; + border-radius: var(--border-radius-messages); + + background-color: var(--action-message-bg); + color: white; + + position: relative; + z-index: 0; +} + +.textContent { + display: inline-block; + padding: 0.1875rem 0.5rem; + background-color: var(--action-message-bg); + color: white; + border-radius: var(--border-radius-messages); + max-width: 100%; + + position: relative; + z-index: 0; +} + +.hasFullContent { + & > .textContent, + & > .inlineWrapper { + margin-bottom: 0.5rem; + } +} + +.singleLine .textContent { + display: inline-flex; + white-space: pre; +} + +.strong { + font-weight: var(--font-weight-medium); +} + +.messageLink { + overflow: hidden; + min-width: 0; +} + +.singleLine, .messageLink { + text-overflow: ellipsis; + white-space: nowrap; +} + +.inlineWrapper { + grid-area: 1 / 1; + max-width: 100%; +} + +.contextContainer { + grid-area: 1 / 1; +} + +.fluidMultiline { + background-color: transparent; + + // These two elements should align perfectly + .fluidBackground, .textContent { + display: inline; + text-wrap: pretty; + padding: 0.1875rem 0.5rem; + box-decoration-break: clone; + border-radius: var(--border-radius-messages); + } + + .fluidBackground { + pointer-events: none; + // Solid color for filter + background-color: black; + color: black; + } + + .textContent { + background-color: transparent !important; + } +} + +.info { + font-size: 0.9375rem; + position: relative; +} + +.stickerWrapper { + position: relative; +} + +.channelPhoto { + --radius: var(--border-radius-messages); +} + +.suggestedAvatar { + margin-top: 0.25rem 0.125rem; +} + +.suggestedText { + text-wrap: balance; +} + +.actionButton { + position: relative; + display: inline-block; + border-radius: 1.25rem; + padding: 0.5rem 1.25rem; + background-color: var(--action-message-bg); + font-weight: var(--font-weight-semibold); + transition: opacity 0.15s; + + cursor: var(--custom-cursor, pointer); + + &:hover, + &:focus { + opacity: 0.8; + } +} + +.title { + margin-bottom: 0; + font-size: inherit; +} + +.subtitle { + font-size: 0.8125rem; + text-wrap: balance; +} + +.starGift { + width: 13.75rem; +} + +.uniqueGift { + margin-block: 0.25rem; + + &::before { + content: ""; + position: absolute; + inset: -0.25rem; + background: var(--action-message-bg); + border-radius: calc(var(--border-radius-messages) + 0.25rem); + z-index: -1; + } +} + +.uniqueBackgroundWrapper { + position: absolute; + inset: 0; + overflow: hidden; + border-radius: inherit; +} + +.uniqueBackground { + position: absolute; + inset: 0; + top: -6rem; +} + +.uniqueValue { + color: white; +} diff --git a/src/components/middle/message/ActionMessage.tsx b/src/components/middle/message/ActionMessage.tsx new file mode 100644 index 000000000..cf5196b95 --- /dev/null +++ b/src/components/middle/message/ActionMessage.tsx @@ -0,0 +1,486 @@ +import React, { + memo, useEffect, useMemo, useRef, useUnmountCleanup, +} from '../../../lib/teact/teact'; +import { getActions, withGlobal } from '../../../global'; + +import type { ApiMessageAction } from '../../../api/types/messageActions'; +import type { + FocusDirection, + ThreadId, +} from '../../../types'; +import type { Signal } from '../../../util/signals'; +import { type ApiMessage, type ApiPeer, MAIN_THREAD_ID } from '../../../api/types'; +import { MediaViewerOrigin } from '../../../types'; + +import { MESSAGE_APPEARANCE_DELAY } from '../../../config'; +import { getMessageHtmlId } from '../../../global/helpers'; +import { getMessageReplyInfo } from '../../../global/helpers/replies'; +import { + selectChat, + selectChatMessage, + selectIsCurrentUserPremium, + selectIsInSelectMode, + selectIsMessageFocused, + selectSender, + selectTabState, + selectTheme, +} from '../../../global/selectors'; +import buildClassName from '../../../util/buildClassName'; +import { isLocalMessageId } from '../../../util/keys/messageKey'; +import { isElementInViewport } from '../../../util/visibility/isElementInViewport'; +import { IS_ANDROID, IS_ELECTRON, IS_FLUID_BACKGROUND_SUPPORTED } from '../../../util/windowEnvironment'; +import { preventMessageInputBlur } from '../helpers/preventMessageInputBlur'; + +import useAppLayout from '../../../hooks/useAppLayout'; +import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers'; +import useEnsureMessage from '../../../hooks/useEnsureMessage'; +import useFlag from '../../../hooks/useFlag'; +import { type ObserveFn, useOnIntersect } from '../../../hooks/useIntersectionObserver'; +import useLastCallback from '../../../hooks/useLastCallback'; +import useMessageResizeObserver from '../../../hooks/useResizeMessageObserver'; +import useShowTransition from '../../../hooks/useShowTransition'; +import { type OnIntersectPinnedMessage } from '../hooks/usePinnedMessage'; +import useFluidBackgroundFilter from './hooks/useFluidBackgroundFilter'; +import useFocusMessage from './hooks/useFocusMessage'; + +import ActionMessageText from './ActionMessageText'; +import ChannelPhoto from './actions/ChannelPhoto'; +import Gift from './actions/Gift'; +import PremiumGiftCode from './actions/GiveawayPrize'; +import StarGift from './actions/StarGift'; +import StarGiftUnique from './actions/StarGiftUnique'; +import SuggestedPhoto from './actions/SuggestedPhoto'; +import ContextMenuContainer from './ContextMenuContainer'; +import Reactions from './reactions/Reactions'; +import SimilarChannels from './SimilarChannels'; + +import styles from './ActionMessage.module.scss'; + +type OwnProps = { + message: ApiMessage; + threadId: ThreadId; + appearanceOrder: number; + isJustAdded?: boolean; + isLastInList?: boolean; + memoFirstUnreadIdRef?: { current: number | undefined }; + getIsMessageListReady?: Signal; + onIntersectPinnedMessage?: OnIntersectPinnedMessage; + observeIntersectionForBottom?: ObserveFn; + observeIntersectionForLoading?: ObserveFn; + observeIntersectionForPlaying?: ObserveFn; +}; + +type StateProps = { + sender?: ApiPeer; + currentUserId?: string; + isInsideTopic?: boolean; + isFocused?: boolean; + focusDirection?: FocusDirection; + noFocusHighlight?: boolean; + replyMessage?: ApiMessage; + patternColor?: string; + isCurrentUserPremium?: boolean; + isInSelectMode?: boolean; + hasUnreadReaction?: boolean; +}; + +const SINGLE_LINE_ACTIONS: Set = new Set([ + 'pinMessage', + 'chatEditPhoto', + 'chatDeletePhoto', + 'unsupported', +]); +const HIDDEN_TEXT_ACTIONS: Set = new Set(['giftCode', 'prizeStars', 'suggestProfilePhoto']); + +const ActionMessage = ({ + message, + threadId, + sender, + currentUserId, + appearanceOrder, + isJustAdded, + isLastInList, + memoFirstUnreadIdRef, + getIsMessageListReady, + isInsideTopic, + isFocused, + focusDirection, + noFocusHighlight, + replyMessage, + patternColor, + isCurrentUserPremium, + isInSelectMode, + hasUnreadReaction, + onIntersectPinnedMessage, + observeIntersectionForBottom, + observeIntersectionForLoading, + observeIntersectionForPlaying, +}: OwnProps & StateProps) => { + const { + requestConfetti, + openMediaViewer, + getReceipt, + checkGiftCode, + openPrizeStarsTransactionFromGiveaway, + openPremiumModal, + openStarsTransactionFromGift, + openGiftInfoModalFromMessage, + toggleChannelRecommendations, + animateUnreadReaction, + markMentionsRead, + } = getActions(); + + // eslint-disable-next-line no-null/no-null + const ref = useRef(null); + + const { id, chatId } = message; + const action = message.content.action!; + const isLocal = isLocalMessageId(id); + + const isTextHidden = HIDDEN_TEXT_ACTIONS.has(action.type); + const isSingleLine = SINGLE_LINE_ACTIONS.has(action.type); + const isFluidMultiline = IS_FLUID_BACKGROUND_SUPPORTED && !isSingleLine; + + const messageReplyInfo = getMessageReplyInfo(message); + const { replyToMsgId, replyToPeerId } = messageReplyInfo || {}; + + const withServiceReactions = Boolean(message.areReactionsPossible && message?.reactions?.results?.length); + + const shouldSkipRender = isInsideTopic && action.type === 'topicCreate'; + + const { isTouchScreen } = useAppLayout(); + + useOnIntersect(ref, !shouldSkipRender ? observeIntersectionForBottom : undefined); + + useMessageResizeObserver(ref, !shouldSkipRender && isLastInList && action.type !== 'channelJoined'); + + useEnsureMessage( + replyToPeerId || chatId, + replyToMsgId, + replyMessage, + id, + ); + useFocusMessage({ + elementRef: ref, + chatId: message.chatId, + isFocused, + focusDirection, + noFocusHighlight, + isJustAdded, + }); + + useUnmountCleanup(() => { + if (message.isPinned) { + onIntersectPinnedMessage?.({ viewportPinnedIdsToRemove: [message.id] }); + } + }); + + const { + isContextMenuOpen, contextMenuAnchor, + handleBeforeContextMenu, handleContextMenu, + handleContextMenuClose, handleContextMenuHide, + } = useContextMenuHandlers( + ref, + isTouchScreen && isInSelectMode, + !IS_ELECTRON, + IS_ANDROID, + getIsMessageListReady, + ); + const isContextMenuShown = contextMenuAnchor !== undefined; + + const handleMouseDown = (e: React.MouseEvent) => { + preventMessageInputBlur(e); + handleBeforeContextMenu(e); + }; + + const noAppearanceAnimation = appearanceOrder <= 0; + const [isShown, markShown] = useFlag(noAppearanceAnimation); + useEffect(() => { + if (noAppearanceAnimation) { + return; + } + + setTimeout(markShown, appearanceOrder * MESSAGE_APPEARANCE_DELAY); + }, [appearanceOrder, markShown, noAppearanceAnimation]); + + const { ref: refWithTransition } = useShowTransition({ + isOpen: isShown, + noOpenTransition: noAppearanceAnimation, + noCloseTransition: true, + className: false, + ref, + }); + + useEffect(() => { + const bottomMarker = ref.current; + if (!bottomMarker || !isElementInViewport(bottomMarker)) return; + + if (hasUnreadReaction) { + animateUnreadReaction({ messageIds: [id] }); + } + + if (message.hasUnreadMention) { + markMentionsRead({ messageIds: [id] }); + } + }, [hasUnreadReaction, id, animateUnreadReaction, message.hasUnreadMention]); + + useEffect(() => { + if (action.type !== 'giftPremium') return; + if ((memoFirstUnreadIdRef?.current && id >= memoFirstUnreadIdRef.current) || isLocal) { + requestConfetti({}); + } + }, [action.type, id, isLocal, memoFirstUnreadIdRef]); + + const fluidBackgroundStyle = useFluidBackgroundFilter(isFluidMultiline ? patternColor : undefined); + + const handleClick = useLastCallback(() => { + switch (action.type) { + case 'paymentSent': + case 'paymentRefunded': { + getReceipt({ + chatId: message.chatId, + messageId: message.id, + }); + break; + } + + case 'chatEditPhoto': { + openMediaViewer({ + chatId: message.chatId, + messageId: message.id, + threadId, + origin: MediaViewerOrigin.ChannelAvatar, + }); + break; + } + + case 'giftCode': { + checkGiftCode({ slug: action.slug, message: { chatId: message.chatId, messageId: message.id } }); + break; + } + + case 'prizeStars': { + openPrizeStarsTransactionFromGiveaway({ + chatId: message.chatId, + messageId: message.id, + }); + break; + } + + case 'giftPremium': { + openPremiumModal({ + isGift: true, + fromUserId: sender?.id, + toUserId: sender && sender.id === currentUserId ? chatId : currentUserId, + monthsAmount: action.months, + }); + break; + } + + case 'giftStars': { + openStarsTransactionFromGift({ + chatId: message.chatId, + messageId: message.id, + }); + break; + } + + case 'starGift': + case 'starGiftUnique': { + openGiftInfoModalFromMessage({ + chatId: message.chatId, + messageId: message.id, + }); + break; + } + + case 'channelJoined': { + toggleChannelRecommendations({ chatId }); + break; + } + } + }); + + const fullContent = useMemo(() => { + switch (action.type) { + case 'chatEditPhoto': { + if (!action.photo) return undefined; + return ( + + ); + } + + case 'suggestProfilePhoto': + return ( + + ); + + case 'prizeStars': + case 'giftCode': + return ( + + ); + + case 'giftPremium': + case 'giftStars': + return ( + + ); + + case 'starGift': + return ( + + ); + + case 'starGiftUnique': + return ( + + ); + + case 'channelJoined': + return ( + + ); + + default: + return undefined; + } + }, [action, observeIntersectionForLoading, message, observeIntersectionForPlaying]); + + if ((isInsideTopic && action.type === 'topicCreate') || action.type === 'phoneCall') { + return undefined; + } + + return ( +
+ {!isTextHidden && ( + <> + {isFluidMultiline && ( +
+ + + +
+ )} +
+ + + +
+ + )} + {fullContent} + {contextMenuAnchor && ( + + )} + {withServiceReactions && ( + + )} +
+ ); +}; + +export default memo(withGlobal( + (global, { message, threadId }): StateProps => { + const { settings: { themes } } = global; + const tabState = selectTabState(global); + const chat = selectChat(global, message.chatId); + + const sender = selectSender(global, message); + + const isInsideTopic = chat?.isForum && threadId !== MAIN_THREAD_ID; + + const { replyToMsgId, replyToPeerId } = getMessageReplyInfo(message) || {}; + const replyMessage = replyToMsgId + ? selectChatMessage(global, replyToPeerId || message.chatId, replyToMsgId) : undefined; + + const isFocused = threadId ? selectIsMessageFocused(global, message, threadId) : false; + const { + direction: focusDirection, + noHighlight: noFocusHighlight, + } = (isFocused && tabState.focusedMessage) || {}; + + const isCurrentUserPremium = selectIsCurrentUserPremium(global); + + const hasUnreadReaction = chat?.unreadReactions?.includes(message.id); + + return { + sender, + currentUserId: global.currentUserId, + isCurrentUserPremium, + isFocused, + focusDirection, + noFocusHighlight, + isInsideTopic, + replyMessage, + isInSelectMode: selectIsInSelectMode(global), + patternColor: themes[selectTheme(global)]?.patternColor, + hasUnreadReaction, + }; + }, +)(ActionMessage)); diff --git a/src/components/middle/message/ActionMessageText.tsx b/src/components/middle/message/ActionMessageText.tsx new file mode 100644 index 000000000..a58afb410 --- /dev/null +++ b/src/components/middle/message/ActionMessageText.tsx @@ -0,0 +1,725 @@ +import React, { memo, type TeactNode } from '../../../lib/teact/teact'; +import { getActions, getGlobal, withGlobal } from '../../../global'; + +import type { ApiChat, ApiMessage, ApiPeer } from '../../../api/types'; + +import { GENERAL_TOPIC_ID, SERVICE_NOTIFICATIONS_USER_ID, TME_LINK_PREFIX } from '../../../config'; +import { + getMessageInvoice, getMessageText, getPeerTitle, isChatChannel, +} from '../../../global/helpers'; +import { getMessageReplyInfo } from '../../../global/helpers/replies'; +import { + selectChat, + selectChatMessage, + selectPeer, + selectSender, + selectThreadIdFromMessage, + selectTopic, +} from '../../../global/selectors'; +import { formatDateTimeToString, formatShortDuration } from '../../../util/dates/dateFormat'; +import { ensureProtocol } from '../../../util/ensureProtocol'; +import { formatCurrency } from '../../../util/formatCurrency'; +import { formatStarsAsText } from '../../../util/localization/format'; +import { conjuctionWithNodes } from '../../../util/localization/utils'; +import renderText from '../../common/helpers/renderText'; +import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities'; +import { + getCallMessageKey, + getPinnedMediaValue, + renderMessageLink, + renderPeerLink, + translateWithOutgoing, +} from './helpers/messageActions'; + +import useLang from '../../../hooks/useLang'; +import useLastCallback from '../../../hooks/useLastCallback'; + +import CustomEmoji from '../../common/CustomEmoji'; +import TopicDefaultIcon from '../../common/TopicDefaultIcon'; +import Link from '../../ui/Link'; + +import styles from './ActionMessage.module.scss'; + +type OwnProps = { + message: ApiMessage; + isInsideTopic?: boolean; + asPreview?: boolean; +}; + +type StateProps = { + currentUserId?: string; + sender?: ApiPeer; + replyMessage?: ApiMessage; + chat?: ApiChat; +}; + +const NBSP = '\u00A0'; +const DEFAULT_TOPIC_ICON_ID = '0'; + +const UNSUPPORTED_LANG_KEY = 'ActionUnsupported'; + +const ActionMessageText = ({ + message, + currentUserId, + sender, + chat, + replyMessage, + isInsideTopic, + asPreview, +}: OwnProps & StateProps) => { + const { + openThread, + openTelegramLink, + openUrl, + } = getActions(); + const { chatId, isOutgoing } = message; + const action = message.content.action!; + + const lang = useLang(); + + function renderStrong(text: TeactNode) { + if (asPreview) return text; + return {text}; + } + + const renderActionText = useLastCallback(() => { + const global = getGlobal(); + + const isChannel = chat && isChatChannel(chat); + const isServiceNotificationsChat = chatId === SERVICE_NOTIFICATIONS_USER_ID; + const isSavedMessages = chatId === currentUserId; + + const senderTitle = sender && getPeerTitle(lang, sender); + const chatTitle = chat && getPeerTitle(lang, chat); + + const userFallbackText = lang('ActionFallbackUser'); + const chatFallbackText = lang('ActionFallbackChat'); + const channelFallbackText = lang('ActionFallbackChannel'); + + const senderLink = renderPeerLink(sender?.id, senderTitle || userFallbackText, asPreview); + const chatLink = renderPeerLink(chat?.id, chatTitle || chatFallbackText, asPreview); + + switch (action.type) { + case 'pinMessage': { + if (replyMessage) { + const formattedText = getMessageText(replyMessage); + if (formattedText) { + const textLink = renderMessageLink( + replyMessage, + renderTextWithEntities({ + text: formattedText.text, + entities: formattedText.entities, + asPreview: true, + }), + asPreview, + ); + + return translateWithOutgoing( + lang, 'ActionPinnedText', isOutgoing, { text: textLink, from: senderLink }, + ); + } + + const mediaValue = getPinnedMediaValue(lang, replyMessage); + if (mediaValue) { + const messageLink = renderMessageLink(replyMessage, mediaValue, asPreview); + return translateWithOutgoing( + lang, 'ActionPinnedMedia', isOutgoing, { from: senderLink, media: messageLink }, + ); + } + } + + return translateWithOutgoing( + lang, + 'ActionPinnedNotFound', + isOutgoing, + { from: senderLink }, + ); + } + + case 'gameScore': { + const { score } = action; + + const gameTitle = replyMessage?.content.game?.title; + const gameLink = gameTitle && renderMessageLink(replyMessage, renderText(gameTitle), asPreview); + + if (gameLink) { + return translateWithOutgoing( + lang, + 'ActionGameScore', + isOutgoing, + { from: senderLink, count: score, game: gameLink }, + { pluralValue: score }, + ); + } + + return translateWithOutgoing( + lang, 'ActionGameScoreNoGame', isOutgoing, { from: senderLink, count: score }, { pluralValue: score }, + ); + } + + case 'chatJoinedByLink': + return lang('ActionUserJoinedByLink', { from: senderLink }, { withNodes: true }); + + case 'chatJoinedByRequest': + return translateWithOutgoing(lang, 'ActionJoinedByRequest', isOutgoing, { from: senderLink }); + + case 'channelJoined': { + const { isViaRequest, inviterId } = action; + const inviter = inviterId ? selectPeer(global, inviterId) : undefined; + if (inviter && inviterId !== currentUserId) { + const inviterLink = renderPeerLink(inviterId, getPeerTitle(lang, inviter) || userFallbackText, asPreview); + return lang('ActionAddYou', { from: inviterLink }, { withNodes: true }); + } + + return lang(isViaRequest ? 'ActionChannelJoinedByRequestChannelYou' : 'ActionChannelJoinedYou'); + } + + case 'chatEditTitle': { + const { title } = action; + if (isChannel) return lang('ActionChangedTitleChannel', { title }); + return translateWithOutgoing(lang, 'ActionChangedTitle', isOutgoing, { title, from: senderLink }); + } + + case 'chatDeletePhoto': + return isChannel ? lang('ActionRemovedPhotoChannel') + : translateWithOutgoing(lang, 'ActionRemovedPhoto', isOutgoing, { from: senderLink }); + + case 'chatEditPhoto': + return isChannel ? lang('ActionChangedPhotoChannel') + : translateWithOutgoing(lang, 'ActionChangedPhoto', isOutgoing, { from: senderLink }); + + case 'chatCreate': { + const { title } = action; + return lang('ActionCreatedChat', { title, from: senderLink }, { withNodes: true }); + } + + case 'channelCreate': { + const { title } = action; + return isChannel ? lang('ActionCreatedChannel') + : translateWithOutgoing(lang, 'ActionCreatedChat', isOutgoing, { title, from: senderLink }); + } + + case 'chatMigrateTo': { + const { channelId } = action; + const channel = selectChat(global, channelId)!; + const channelLink = renderPeerLink(channelId, getPeerTitle(lang, channel)!, asPreview); + return lang('ActionMigratedTo', { chat: channelLink }, { withNodes: true }); + } + + case 'channelMigrateFrom': { + const { chatId: originalChatId, title } = action; + const originalChatLink = renderPeerLink(originalChatId, title || chatFallbackText, asPreview); + return lang('ActionMigratedFrom', { chat: originalChatLink }, { withNodes: true }); + } + + case 'topicCreate': { + const { title, iconColor, iconEmojiId } = action; + + const topicId = selectThreadIdFromMessage(global, message); + + const topicLink = ( + openThread({ chatId, threadId: topicId })} + > + {iconEmojiId ? + : } + {NBSP} + {renderText(title)} + + ); + return lang('ActionTopicCreated', { topic: topicLink }, { withNodes: true }); + } + + case 'topicEdit': { + const { + iconEmojiId, isClosed, isHidden, title, + } = action; + + const topicId = selectThreadIdFromMessage(global, message); + const currentTopic = selectTopic(global, chatId, topicId); + const topicLink = ( + openThread({ chatId, threadId: topicId })} + > + {iconEmojiId && iconEmojiId !== DEFAULT_TOPIC_ICON_ID + ? + : ( + + )} + {topicId !== GENERAL_TOPIC_ID && NBSP} + {renderText(title || currentTopic?.title || lang('ActionTopicPlaceholder'))} + + ); + + const topicPlaceholderLink = ( + openThread({ chatId, threadId: topicId })} + > + {lang('ActionTopicPlaceholder')} + + ); + + if (isClosed !== undefined) { + if (isInsideTopic) { + return lang(isClosed ? 'ActionTopicClosedInside' : 'ActionTopicReopenedInside'); + } + + return lang( + isClosed ? 'ActionTopicClosed' : 'ActionTopicReopened', + { from: senderLink, topic: topicLink }, + { withNodes: true }, + ); + } + + if (isHidden !== undefined) { + if (isInsideTopic) { + return lang(isHidden ? 'ActionTopicHiddenInside' : 'ActionTopicUnhiddenInside'); + } + return lang( + isHidden ? 'ActionTopicHidden' : 'ActionTopicUnhidden', + { topic: topicLink }, + { withNodes: true }, + ); + } + + if (title && iconEmojiId) { + return lang( + 'ActionTopicIconAndRenamed', + { + from: senderLink, + link: topicPlaceholderLink, + topic: topicLink, + }, + { withNodes: true }, + ); + } + + if (title === undefined) { + if (!iconEmojiId || iconEmojiId === DEFAULT_TOPIC_ICON_ID) { + return lang( + 'ActionTopicIconRemoved', { from: senderLink, link: topicPlaceholderLink }, { withNodes: true }, + ); + } + return lang( + 'ActionTopicIconChanged', + { + from: senderLink, + link: topicPlaceholderLink, + emoji: , + }, + { withNodes: true }, + ); + } + + return lang('ActionTopicRenamed', { from: senderLink, link: topicPlaceholderLink, title }, { withNodes: true }); + } + + case 'boostApply': + return translateWithOutgoing( + lang, + 'ActionBoostApply', + isOutgoing, + { from: senderLink, count: action.boosts }, + { pluralValue: action.boosts }, + ); + + case 'chatAddUser': { + const { userIds } = action; + + if (sender?.id === userIds[0]) { + return translateWithOutgoing(lang, 'ActionUserJoined', isOutgoing, { from: senderLink }); + } + + if (userIds.length === 1) { + const user = selectPeer(global, userIds[0]); + const userTitle = (user && getPeerTitle(lang, user)) || userFallbackText; + const userLink = renderPeerLink(user?.id, userTitle, asPreview); + return translateWithOutgoing(lang, 'ActionAddUser', isOutgoing, { from: senderLink, user: userLink }); + } + + const users = userIds.map((userId) => selectPeer(global, userId)).filter(Boolean); + + if (!users.length) { + return translateWithOutgoing(lang, 'ActionAddUser', isOutgoing, { from: senderLink, user: userFallbackText }); + } + + const userLinks = users.map((user) => ( + renderPeerLink(user.id, getPeerTitle(lang, user) || userFallbackText, asPreview) + )); + + return translateWithOutgoing( + lang, 'ActionAddUsersMany', isOutgoing, { from: senderLink, users: conjuctionWithNodes(lang, userLinks) }, + ); + } + + case 'chatDeleteUser': { + const { userId } = action; + if (sender?.id === userId) { + return translateWithOutgoing(lang, 'ActionUserLeft', isOutgoing, { from: senderLink }); + } + + const user = selectPeer(global, userId); + const userTitle = (user && getPeerTitle(lang, user)) || userFallbackText; + const userLink = renderPeerLink(user?.id, userTitle, asPreview); + return translateWithOutgoing(lang, 'ActionKickUser', isOutgoing, { from: senderLink, user: userLink }); + } + + case 'botAllowed': { + const { + app, domain, isAttachMenu, isFromRequest, + } = action; + if (isAttachMenu) return lang('ActionAttachMenuBotAllowed'); + if (isFromRequest) return lang('ActionWebappBotAllowed'); + if (app) { + const link = sender?.usernames?.length + && `${TME_LINK_PREFIX + sender.usernames[0].username}/${app.shortName}`; + const appLink = link + // eslint-disable-next-line react/jsx-no-bind + ? openTelegramLink({ url: link })}>{app.title} + : lang('ActionBotAppPlaceholder'); + return lang('ActionBotAllowedFromApp', { app: appLink }, { withNodes: true }); + } + + if (!domain) return lang(UNSUPPORTED_LANG_KEY); + + const url = ensureProtocol(domain)!; + // eslint-disable-next-line react/jsx-no-bind + const link = openUrl({ url })}>{domain}; + return lang('ActionBotAllowedFromDomain', { domain: link }, { withNodes: true }); + } + + case 'giveawayLaunch': { + const { stars } = action; + + if (stars) { + return lang( + isChannel ? 'ActionGiveawayStarsStarted' : 'ActionGiveawayStarsStartedGroup', + { from: senderLink, amount: renderStrong(formatStarsAsText(lang, stars)) }, + { withNodes: true }, + ); + } + + return lang( + isChannel ? 'ActionGiveawayStarted' : 'ActionGiveawayStartedGroup', + { from: senderLink }, + { withNodes: true }, + ); + } + + case 'giveawayResults': { + const { winnersCount, isStars, unclaimedCount } = action; + if (!winnersCount) return lang('ActionGiveawayResultsNone'); + if (unclaimedCount) { + return lang(isStars ? 'ActionGiveawayResultsStarsSome' : 'ActionGiveawayResultsSome'); + } + + return lang( + isStars ? 'ActionGiveawayResultsStars' : 'ActionGiveawayResults', + { count: winnersCount }, + { pluralValue: winnersCount }, + ); + } + + case 'giftStars': + case 'giftPremium': { + const { + amount, currency, cryptoAmount, cryptoCurrency, + } = action; + + const price = formatCurrency(lang, amount, currency, { asFontIcon: true }); + const cryptoPrice = cryptoAmount ? formatCurrency(lang, cryptoAmount, cryptoCurrency!) : undefined; + + const cost = cryptoPrice ? lang('ActionCostCrypto', { price, cryptoPrice }, { withNodes: true }) : price; + + if (isServiceNotificationsChat) { + return lang('ActionGiftTextCostAnonymous', { cost }, { withNodes: true }); + } + return translateWithOutgoing( + lang, 'ActionGiftTextCost', isOutgoing, { from: senderLink, cost: renderStrong(cost) }, + ); + } + + case 'prizeStars': + case 'giftCode': { + return lang('ActionGiftTextUnknown'); + } + + case 'groupCall': { + const { duration } = action; + const durationText = duration ? formatShortDuration(lang, duration) : undefined; + if (durationText) { + if (isChannel) { + return lang('ActionGroupCallFinishedChannel', { duration: durationText }); + } + return lang( + 'ActionGroupCallFinishedGroup', { from: senderLink, duration: durationText }, { withNodes: true }, + ); + } + + if (isChannel) return lang('ActionGroupCallStartedChannel'); + return lang('ActionGroupCallStartedGroup', { from: senderLink }, { withNodes: true }); + } + + case 'groupCallScheduled': { + const { scheduleDate } = action; + const formattedDate = formatDateTimeToString(scheduleDate * 1000, lang.code, true); + + if (isChannel) return lang('ActionGroupCallScheduledChannel', { date: formattedDate }); + return lang('ActionGroupCallScheduledGroup', { from: senderLink, date: formattedDate }, { withNodes: true }); + } + + case 'inviteToGroupCall': { + const { userIds } = action; + + if (userIds.length === 1) { + const user = selectPeer(global, userIds[0]); + const userTitle = (user && getPeerTitle(lang, user)) || userFallbackText; + const userLink = renderPeerLink(user?.id, userTitle, asPreview); + return translateWithOutgoing(lang, 'ActionVideoInvited', isOutgoing, { from: senderLink, user: userLink }); + } + + const users = userIds.map((userId) => selectPeer(global, userId)).filter(Boolean); + + if (!users.length) { + return translateWithOutgoing( + lang, 'ActionVideoInvited', isOutgoing, { from: senderLink, user: userFallbackText }, + ); + } + + const userLinks = users.map((user) => ( + renderPeerLink(user.id, getPeerTitle(lang, user) || userFallbackText, asPreview) + )); + + return translateWithOutgoing( + lang, 'ActionVideoInvitedMany', isOutgoing, { from: senderLink, users: conjuctionWithNodes(lang, userLinks) }, + ); + } + + case 'paymentSent': { + const { + currency, totalAmount, isRecurringInit, isRecurringUsed, + } = action; + + const cost = renderStrong(formatCurrency(lang, totalAmount, currency, { asFontIcon: true })); + const invoice = replyMessage && getMessageInvoice(replyMessage); + const invoiceTitle = invoice?.title; + + if (isRecurringUsed) { + return lang('ActionPaymentUsedRecurring', { amount: cost }, { withNodes: true }); + } + + if (!invoiceTitle) { + if (isRecurringInit) { + return lang('ActionPaymentInitRecurring', { amount: cost, user: chatLink }, { withNodes: true }); + } + + return lang('ActionPaymentDone', { amount: cost, user: chatLink }, { withNodes: true }); + } + + if (isRecurringInit) { + return lang( + 'ActionPaymentInitRecurringFor', + { amount: cost, user: chatLink, invoice: renderMessageLink(replyMessage!, invoiceTitle, asPreview) }, + { withNodes: true }, + ); + } + + return lang( + 'ActionPaymentDoneFor', + { amount: cost, user: chatLink, invoice: renderMessageLink(replyMessage!, invoiceTitle, asPreview) }, + { withNodes: true }, + ); + } + + case 'paymentRefunded': { + const { currency, totalAmount, peerId } = action; + const peer = selectPeer(global, peerId); + const peerTitle = (peer && getPeerTitle(lang, peer)) || userFallbackText; + const peerLink = renderPeerLink(peer?.id, peerTitle, asPreview); + const amount = formatCurrency(lang, totalAmount, currency, { asFontIcon: true }); + + return lang('ActionPaymentRefunded', { peer: peerLink, amount }, { withNodes: true }); + } + + case 'starGift': { + const { + gift, alreadyPaidUpgradeStars, peerId, savedId, fromId, + } = action; + const isToChannel = Boolean(peerId && savedId); + + const fromPeer = fromId ? selectPeer(global, fromId) : sender; + const fromTitle = (fromPeer && getPeerTitle(lang, fromPeer)) || userFallbackText; + const fromLink = renderPeerLink(fromPeer?.id, fromTitle, asPreview); + + const starsAmount = gift.stars + (alreadyPaidUpgradeStars || 0); + const cost = renderStrong(formatStarsAsText(lang, starsAmount)); + + if (isToChannel) { + const channelPeer = selectPeer(global, peerId!); + const isYou = fromPeer?.id === currentUserId; + + const channelTitle = (channelPeer && getPeerTitle(lang, channelPeer)) || channelFallbackText; + const channelLink = renderPeerLink(peerId, channelTitle, asPreview); + return translateWithOutgoing( + lang, 'ActionStarGiftSentChannel', isYou, { user: fromLink, channel: channelLink, cost }, + ); + } + + if (isServiceNotificationsChat) { + return lang('ActionStarGiftReceivedAnonymous', { cost }, { withNodes: true }); + } + + if (isSavedMessages) { + return lang('ActionStarGiftSelfBought', { cost }, { withNodes: true }); + } + + if (isOutgoing) { + return lang('ActionStarGiftSent', { cost }, { withNodes: true }); + } + + return lang('ActionStarGiftReceived', { user: senderLink, cost }, { withNodes: true }); + } + + case 'starGiftUnique': { + const { + isTransferred, isUpgrade, savedId, peerId, fromId, + } = action; + + const isToChannel = Boolean(peerId && savedId); + + const fromPeer = fromId ? selectPeer(global, fromId) : sender; + const fromTitle = (fromPeer && getPeerTitle(lang, fromPeer)) || userFallbackText; + const fromLink = renderPeerLink(fromPeer?.id, fromTitle, asPreview); + + if (isToChannel) { + const channelPeer = selectPeer(global, peerId!); + const isYou = fromPeer?.id === currentUserId; + const isAnonymous = fromPeer?.id === SERVICE_NOTIFICATIONS_USER_ID; + + const channelTitle = (channelPeer && getPeerTitle(lang, channelPeer)) || channelFallbackText; + const channelLink = renderPeerLink(peerId, channelTitle, asPreview); + + if (isUpgrade) { + return translateWithOutgoing( + lang, 'ActionStarGiftUpgradedChannel', isYou, { user: fromLink, channel: channelLink }, + ); + } + + if (isTransferred) { + if (isAnonymous) { + return lang('ActionStarGiftTransferredUnknownChannel', { channel: channelLink }, { withNodes: true }); + } + + return translateWithOutgoing( + lang, 'ActionStarGiftTransferredChannel', isYou, { user: fromLink, channel: channelLink }, + ); + } + } + + if (isSavedMessages) { + if (isUpgrade) return lang('ActionStarGiftUpgradedSelf'); + if (isTransferred) return lang('ActionStarGiftTransferredSelf'); + } + + if (isUpgrade) { + if (isOutgoing) { + return lang('ActionStarGiftUpgradedMine', { user: chatLink }, { withNodes: true }); + } + + if (isSavedMessages) { + return lang('ActionStarGiftUpgradedSelf'); + } + + return lang('ActionStarGiftUpgradedUser', { user: senderLink }, { withNodes: true }); + } + + if (isTransferred) { + if (sender?.id === SERVICE_NOTIFICATIONS_USER_ID) { + return lang('ActionStarGiftTransferredUnknown'); + } + + if (isSavedMessages) { + return lang('ActionStarGiftTransferredSelf'); + } + + if (isOutgoing) { + return lang('ActionStarGiftTransferredMine', { user: chatLink }, { withNodes: true }); + } + + return lang('ActionStarGiftTransferred', { user: senderLink }, { withNodes: true }); + } + + if (isOutgoing) { + return lang('ActionGiftUniqueSent'); + } + + return lang('ActionGiftUniqueReceived', { user: senderLink }, { withNodes: true }); + } + + case 'suggestProfilePhoto': { + const actionPeer = (isOutgoing ? chat : sender)!; + const actionPeerLink = renderPeerLink(actionPeer.id, getPeerTitle(lang, actionPeer) || userFallbackText); + + return translateWithOutgoing(lang, 'ActionSuggestedPhoto', isOutgoing, { user: actionPeerLink }); + } + + case 'webViewDataSent': + return lang('ActionWebviewDataDone', { text: action.text }); + + case 'expired': { + const { isRoundVideo, isVoice } = action; + if (isVoice) return lang('ActionExpiredVoice'); + if (isRoundVideo) return lang('ActionExpiredVideo'); + + return lang(UNSUPPORTED_LANG_KEY); + } + + case 'historyClear': + return lang('ActionHistoryCleared'); + + case 'screenshotTaken': + return translateWithOutgoing(lang, 'ActionScreenshotTaken', isOutgoing, { from: senderLink }); + + case 'contactSignUp': + return lang('ActionUserRegistered', { from: senderLink }, { withNodes: true }); + + case 'customAction': + return action.message; + + case 'phoneCall': // Rendered as a regular message, but considered an action for the summary + return lang(getCallMessageKey(action, isOutgoing)); + default: + return lang(UNSUPPORTED_LANG_KEY); + } + }); + + return renderActionText(); +}; + +export default memo(withGlobal( + (global, { message }): StateProps => { + const chat = selectChat(global, message.chatId); + const sender = selectSender(global, message); + + const { replyToMsgId, replyToPeerId } = getMessageReplyInfo(message) || {}; + const replyMessage = replyToMsgId + ? selectChatMessage(global, replyToPeerId || message.chatId, replyToMsgId) : undefined; + + return { + currentUserId: global.currentUserId, + replyMessage, + chat, + sender, + }; + }, +)(ActionMessageText)); diff --git a/src/components/middle/message/ContextMenuContainer.tsx b/src/components/middle/message/ContextMenuContainer.tsx index d0b86cb52..570f1d1d9 100644 --- a/src/components/middle/message/ContextMenuContainer.tsx +++ b/src/components/middle/message/ContextMenuContainer.tsx @@ -72,6 +72,7 @@ import { selectUser, selectUserStatus, } from '../../../global/selectors'; +import buildClassName from '../../../util/buildClassName'; import { copyTextToClipboard } from '../../../util/clipboard'; import { getSelectionAsFormattedText } from './helpers/getSelectionAsFormattedText'; import { isSelectionRangeInsideMessage } from './helpers/isSelectionRangeInsideMessage'; @@ -96,6 +97,7 @@ export type OwnProps = { noReplies?: boolean; detectedLanguage?: string; repliesThreadInfo?: ApiThreadInfo; + className?: string; onClose: NoneToVoidFunction; onCloseAnimationEnd: NoneToVoidFunction; }; @@ -217,10 +219,11 @@ const ContextMenuContainer: FC = ({ isInSavedMessages, canReplyInChat, isWithPaidReaction, - onClose, - onCloseAnimationEnd, userFullName, canGift, + className, + onClose, + onCloseAnimationEnd, }) => { const { openThread, @@ -621,7 +624,7 @@ const ContextMenuContainer: FC = ({ scheduledMaxDate.setFullYear(scheduledMaxDate.getFullYear() + 1); return ( -
+
= ({ // eslint-disable-next-line no-null/no-null const ref = useRef(null); - const lang = useOldLang(); + const oldLang = useOldLang(); + const lang = useLang(); const invoice = getMessageInvoice(message); const { @@ -120,8 +122,8 @@ const Invoice: FC = ({
)}

- {formatCurrency(amount, currency, lang.code, { iconClassName: 'invoice-currency-icon' })} - {isTest && {lang('PaymentTestInvoice')}} + {formatCurrency(lang, amount, currency, { iconClassName: 'invoice-currency-icon' })} + {isTest && {oldLang('PaymentTestInvoice')}}

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

{lang('PaymentShippingMethod')}

+

{oldLang('PaymentShippingMethod')}

{

{lang('Participants', { count: 42 }, { pluralValue: 42 })}

- {lang('ChatServiceGroupUpdatedPinnedMessage1', { - message: 'Some message', - user: 'Some user', + {lang('ActionPinnedText', { + text: 'Some message', + from: 'Some user', })}

{/*

diff --git a/src/components/ui/CropModal.tsx b/src/components/ui/CropModal.tsx index 20f5162e9..ff83c8cdd 100644 --- a/src/components/ui/CropModal.tsx +++ b/src/components/ui/CropModal.tsx @@ -6,7 +6,7 @@ import React, { import { DEBUG } from '../../config'; import { blobToDataUri, blobToFile } from '../../util/files'; -import useOldLang from '../../hooks/useOldLang'; +import useLang from '../../hooks/useLang'; import Icon from '../common/icons/Icon'; import Button from './Button'; @@ -80,6 +80,8 @@ type OwnProps = { const CropModal: FC = ({ file, onChange, onClose }: OwnProps) => { const [isCroppieReady, setIsCroppieReady] = useState(false); + const lang = useLang(); + useEffect(() => { if (!file) { return; @@ -94,8 +96,6 @@ const CropModal: FC = ({ file, onChange, onClose }: OwnProps) => { initCropper(file); }, [file, isCroppieReady]); - const lang = useOldLang(); - const handleCropClick = useCallback(async () => { if (!cropper) { return; @@ -111,7 +111,7 @@ const CropModal: FC = ({ file, onChange, onClose }: OwnProps) => { @@ -125,7 +125,7 @@ const CropModal: FC = ({ file, onChange, onClose }: OwnProps) => { round color="primary" onClick={handleCropClick} - ariaLabel={lang('CropImage')} + ariaLabel={lang('CropperApply')} > diff --git a/src/components/ui/Link.tsx b/src/components/ui/Link.tsx index a10f8d16f..dbf559ce7 100644 --- a/src/components/ui/Link.tsx +++ b/src/components/ui/Link.tsx @@ -12,11 +12,12 @@ type OwnProps = { className?: string; isRtl?: boolean; isPrimary?: boolean; + withMultilineFix?: boolean; onClick?: (e: React.MouseEvent) => void; }; const Link: FC = ({ - children, isPrimary, className, isRtl, onClick, + children, isPrimary, className, isRtl, withMultilineFix, onClick, }) => { const handleClick = useLastCallback((e: React.MouseEvent) => { e.preventDefault(); @@ -27,7 +28,7 @@ const Link: FC = ({ {children} diff --git a/src/components/ui/ProgressSpinner.tsx b/src/components/ui/ProgressSpinner.tsx index fea2a3b58..8865f3ec5 100644 --- a/src/components/ui/ProgressSpinner.tsx +++ b/src/components/ui/ProgressSpinner.tsx @@ -50,7 +50,7 @@ const ProgressSpinner: FC<{ const dpr = useDevicePixelRatio(); - const color = useDynamicColorListener(canvasRef, !withColor); + const color = useDynamicColorListener(canvasRef, undefined, !withColor); useEffect(() => { let isFirst = true; diff --git a/src/config.ts b/src/config.ts index aaf88c8ea..fafb0e715 100644 --- a/src/config.ts +++ b/src/config.ts @@ -173,6 +173,7 @@ export const TMP_CHAT_ID = '0'; export const ANIMATION_END_DELAY = 100; export const ANIMATION_WAVE_MIN_INTERVAL = 200; +export const MESSAGE_APPEARANCE_DELAY = 10; export const SCROLL_MIN_DURATION = 300; export const SCROLL_MAX_DURATION = 600; diff --git a/src/global/actions/apiUpdaters/initial.ts b/src/global/actions/apiUpdaters/initial.ts index d789c742f..7f458a533 100644 --- a/src/global/actions/apiUpdaters/initial.ts +++ b/src/global/actions/apiUpdaters/initial.ts @@ -180,10 +180,11 @@ function onUpdateAuthorizationState(global: T, update: Ap } function onUpdateAuthorizationError(global: T, update: ApiUpdateAuthorizationError) { + // TODO: Investigate why TS is not happy with spread for lang related types global = { ...global, - authErrorKey: update.errorKey, }; + global.authErrorKey = update.errorKey; setGlobal(global); } diff --git a/src/global/actions/apiUpdaters/messages.ts b/src/global/actions/apiUpdaters/messages.ts index 3bcf49570..f8e6f1663 100644 --- a/src/global/actions/apiUpdaters/messages.ts +++ b/src/global/actions/apiUpdaters/messages.ts @@ -208,7 +208,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { if (!message) return; // Workaround for a weird behavior when interaction is received after watching reaction - if (getMessageText(message) !== update.emoji) return; + if (getMessageText(message)?.text !== update.emoji) return; const tabState = selectTabState(global, tabId); global = updateTabState(global, { @@ -1163,7 +1163,7 @@ export function deleteMessages( return; } - if (message.content.action?.photo) { + if (message.content.action?.type === 'chatEditPhoto' && message.content.action.photo) { global = deletePeerPhoto(global, chatId, message.content.action.photo.id, true); } @@ -1251,7 +1251,7 @@ export function deleteMessages( } } - if (message?.content.action?.photo) { + if (message?.content.action?.type === 'chatEditPhoto' && message.content.action.photo) { global = deletePeerPhoto(global, commonBoxChatId, message.content.action.photo.id, true); } diff --git a/src/global/actions/apiUpdaters/misc.ts b/src/global/actions/apiUpdaters/misc.ts index 96febe59d..f740fc344 100644 --- a/src/global/actions/apiUpdaters/misc.ts +++ b/src/global/actions/apiUpdaters/misc.ts @@ -217,15 +217,16 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { } case 'newMessage': { - const actionStarGift = update.message.content?.action?.starGift; + const action = update.message.content?.action; if (!update.message.isOutgoing && update.message.chatId !== SERVICE_NOTIFICATIONS_USER_ID) return undefined; - if (actionStarGift?.type !== 'starGiftUnique') return undefined; + if (action?.type !== 'starGiftUnique') return undefined; + const actionStarGift = action.gift; Object.values(global.byTabId).forEach(({ id: tabId }) => { const tabState = selectTabState(global, tabId); if (tabState.isWaitingForStarGiftUpgrade) { actions.openUniqueGiftBySlug({ - slug: actionStarGift.gift.slug, + slug: actionStarGift.slug, tabId, }); @@ -259,8 +260,8 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { gift: { key: 'GiftUnique', variables: { - title: actionStarGift.gift.title, - number: actionStarGift.gift.number, + title: actionStarGift.title, + number: actionStarGift.number, }, }, peer: getPeerTitle(getTranslationFn(), receiver), diff --git a/src/global/actions/ui/messages.ts b/src/global/actions/ui/messages.ts index f665284a3..d1f6a4438 100644 --- a/src/global/actions/ui/messages.ts +++ b/src/global/actions/ui/messages.ts @@ -31,7 +31,6 @@ import { getMessageStatefulContent, getPeerTitle, isChatChannel, - isJoinedChannelMessage, } from '../../helpers'; import { getMessageSummaryText } from '../../helpers/messageSummary'; import { renderMessageSummaryHtml } from '../../helpers/renderMessageSummaryHtml'; @@ -345,13 +344,6 @@ addActionHandler('focusLastMessage', (global, actions, payload): ActionReturnTyp lastMessageId = pinnedMessageIds[pinnedMessageIds.length - 1]; } else { lastMessageId = selectChatLastMessageId(global, chatId); - - const chatMessages = selectChatMessages(global, chatId); - // Workaround for scroll to local message 'you joined this channel' - const lastChatMessage = Object.values(chatMessages).reverse()[0]; - if (lastMessageId && isJoinedChannelMessage(lastChatMessage) && lastChatMessage.id > lastMessageId) { - lastMessageId = lastChatMessage.id; - } } } else if (isSavedDialog) { lastMessageId = selectChatLastMessageId(global, String(threadId), 'saved'); diff --git a/src/global/actions/ui/stars.ts b/src/global/actions/ui/stars.ts index ab3ec268e..50741d421 100644 --- a/src/global/actions/ui/stars.ts +++ b/src/global/actions/ui/stars.ts @@ -1,4 +1,4 @@ -import type { ApiMessageActionStarGift, ApiSavedStarGift } from '../../../api/types'; +import type { ApiInputSavedStarGift, ApiSavedStarGift } from '../../../api/types'; import type { ActionReturnType } from '../../types'; import { getCurrentTabId } from '../../../util/establishMultitabRole'; @@ -208,32 +208,35 @@ addActionHandler('openGiftInfoModalFromMessage', (global, actions, payload): Act if (!message || !message.content.action) return; const action = message.content.action; - if (action.type === 'starGiftUnique') { - actions.openGiftInfoModal({ gift: action.starGift?.gift!, tabId }); - return; - } + if (action.type !== 'starGift' && action.type !== 'starGiftUnique') return; - if (action.type !== 'starGift') return; + const starGift = action.type === 'starGift' ? action : undefined; + const uniqueGift = action.type === 'starGiftUnique' ? action : undefined; - const starGift = action.starGift! as ApiMessageActionStarGift; + const giftReceiverId = action.peerId || (message.isOutgoing ? message.chatId : global.currentUserId!); - const giftReceiverId = message.isOutgoing ? message.chatId : global.currentUserId!; + const inputGift: ApiInputSavedStarGift = action.savedId + ? { type: 'chat', chatId, savedId: action.savedId } + : { type: 'user', messageId }; - const gift = { + const gift: ApiSavedStarGift = { date: message.date, - gift: starGift.gift, - message: starGift.message, - starsToConvert: starGift.starsToConvert, - isNameHidden: starGift.isNameHidden, - isUnsaved: !starGift.isSaved, - fromId: message.isOutgoing ? global.currentUserId : message.chatId, - messageId: (!message.isOutgoing || chatId === global.currentUserId) ? message.id : undefined, - isConverted: starGift.isConverted, - upgradeMsgId: starGift.upgradeMsgId, - canUpgrade: starGift.canUpgrade, - alreadyPaidUpgradeStars: starGift.alreadyPaidUpgradeStars, - inputGift: starGift.inputSavedGift, - } satisfies ApiSavedStarGift; + gift: action.gift, + message: starGift?.message, + starsToConvert: starGift?.starsToConvert, + isNameHidden: starGift?.isNameHidden, + isUnsaved: !action.isSaved, + fromId: action.fromId, + messageId: message.id, + isConverted: starGift?.isConverted, + upgradeMsgId: starGift?.upgradeMsgId, + canUpgrade: starGift?.canUpgrade, + alreadyPaidUpgradeStars: starGift?.alreadyPaidUpgradeStars, + inputGift, + canExportAt: uniqueGift?.canExportAt, + savedId: action.savedId, + transferStars: uniqueGift?.transferStars, + }; actions.openGiftInfoModal({ peerId: giftReceiverId, gift, tabId }); }); diff --git a/src/global/cache.ts b/src/global/cache.ts index 5e9a95554..cf154d94e 100644 --- a/src/global/cache.ts +++ b/src/global/cache.ts @@ -277,6 +277,13 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) { if (!cached.peers) { cached.peers = initialState.peers; } + + if (!cached.cacheVersion) { + cached.cacheVersion = initialState.cacheVersion; + // Reset because of the new action message structure + cached.messages = initialState.messages; + cached.chats.listIds = initialState.chats.listIds; + } } function updateCache(force?: boolean) { diff --git a/src/global/helpers/messageSummary.ts b/src/global/helpers/messageSummary.ts index 14aadddfb..2502697af 100644 --- a/src/global/helpers/messageSummary.ts +++ b/src/global/helpers/messageSummary.ts @@ -10,7 +10,7 @@ import { CONTENT_NOT_SUPPORTED } from '../../config'; import trimText from '../../util/trimText'; import { renderTextWithEntities } from '../../components/common/helpers/renderTextWithEntities'; import { - getExpiredMessageContentDescription, getMessageText, getMessageTranscription, isExpiredMessageContent, + getMessageText, getMessageTranscription, } from './messages'; const SPOILER_CHARS = ['⠺', '⠵', '⠞', '⠟']; @@ -35,7 +35,7 @@ export function getMessageSummaryText( export function getMessageTextWithSpoilers(message: ApiMessage, statefulContent: StatefulMediaContent | undefined) { const transcription = getMessageTranscription(message); - const textWithoutTranscription = getMessageText(statefulContent?.story || message); + const textWithoutTranscription = getMessageText(statefulContent?.story || message)?.text; if (!textWithoutTranscription) { return transcription; } @@ -147,7 +147,7 @@ function getSummaryDescription( const { poll } = statefulContent || {}; let hasUsedTruncatedText = false; - let summary: string | TeactNode | undefined; + let summary: TeactNode | undefined; const boughtExtendedMedia = paidMedia?.isBought && paidMedia.extendedMedia; const previewExtendedMedia = paidMedia && !paidMedia.isBought @@ -199,7 +199,7 @@ function getSummaryDescription( summary = renderTextWithEntities({ text: poll.summary.question.text, entities: poll.summary.question.entities, - noLineBreaks: true, + asPreview: true, }); } @@ -239,13 +239,6 @@ function getSummaryDescription( summary = truncatedText || (message ? lang('ForwardedStory') : lang('Chat.ReplyStory')); } - if (isExpiredMessageContent(mediaContent)) { - const expiredMessageText = getExpiredMessageContentDescription(lang, mediaContent); - if (expiredMessageText) { - summary = expiredMessageText; - } - } - return summary || CONTENT_NOT_SUPPORTED; } diff --git a/src/global/helpers/messages.ts b/src/global/helpers/messages.ts index 5081c5d48..d9ea6cb58 100644 --- a/src/global/helpers/messages.ts +++ b/src/global/helpers/messages.ts @@ -7,7 +7,7 @@ import type { ApiTypeStory, } from '../../api/types'; import type { - ApiPoll, MediaContainer, MediaContent, StatefulMediaContent, + ApiPoll, MediaContainer, StatefulMediaContent, } from '../../api/types/messages'; import type { OldLangFn } from '../../hooks/useOldLang'; import type { CustomPeer, ThreadId } from '../../types'; @@ -57,13 +57,13 @@ export function getMessageTranscription(message: ApiMessage) { export function hasMessageText(message: MediaContainer) { const { - text, sticker, photo, video, audio, voice, document, pollId, webPage, contact, invoice, location, - game, action, storyData, giveaway, giveawayResults, isExpiredVoice, paidMedia, + action, text, sticker, photo, video, audio, voice, document, pollId, webPage, contact, invoice, location, + game, storyData, giveaway, giveawayResults, paidMedia, } = message.content; return Boolean(text) || !( sticker || photo || video || audio || voice || document || contact || pollId || webPage || invoice || location - || game || action?.phoneCall || storyData || giveaway || giveawayResults || isExpiredVoice || paidMedia + || game || storyData || giveaway || giveawayResults || paidMedia || action?.type === 'phoneCall' ); } @@ -73,10 +73,10 @@ export function getMessageStatefulContent(global: GlobalState, message: ApiMessa const { peerId: storyPeerId, id: storyId } = message.content.storyData || {}; const story = storyId && storyPeerId ? global.stories.byPeerId[storyPeerId]?.byId[storyId] : undefined; - return groupStatetefulContent({ poll, story }); + return groupStatefulContent({ poll, story }); } -export function groupStatetefulContent({ +export function groupStatefulContent({ poll, story, } : { @@ -90,7 +90,7 @@ export function groupStatetefulContent({ } export function getMessageText(message: MediaContainer) { - return hasMessageText(message) ? message.content.text?.text || CONTENT_NOT_SUPPORTED : undefined; + return hasMessageText(message) ? message.content.text || { text: CONTENT_NOT_SUPPORTED } : undefined; } export function getMessageCustomShape(message: ApiMessage): boolean { @@ -198,7 +198,7 @@ export function isForwardedMessage(message: ApiMessage) { } export function isActionMessage(message: ApiMessage) { - return Boolean(message.content.action) || isExpiredMessage(message); + return Boolean(message.content.action); } export function isServiceNotificationMessage(message: ApiMessage) { @@ -359,36 +359,14 @@ export function extractMessageText(message: ApiMessage | ApiStory, inChatList = return { text, entities }; } -export function getExpiredMessageDescription(langFn: OldLangFn, message: ApiMessage): string | undefined { - return getExpiredMessageContentDescription(langFn, message.content); -} -export function getExpiredMessageContentDescription(langFn: OldLangFn, mediaContent: MediaContent): string | undefined { - const { isExpiredVoice, isExpiredRoundVideo } = mediaContent; - if (isExpiredVoice) { - return langFn('Message.VoiceMessageExpired'); - } else if (isExpiredRoundVideo) { - return langFn('Message.VideoMessageExpired'); - } - return undefined; -} - export function isExpiredMessage(message: ApiMessage) { - return isExpiredMessageContent(message.content); -} - -export function isExpiredMessageContent(content: MediaContent) { - const { isExpiredVoice, isExpiredRoundVideo } = content ?? {}; - return Boolean(isExpiredVoice || isExpiredRoundVideo); + return message.content.action?.type === 'expired'; } export function hasMessageTtl(message: ApiMessage) { return message.content?.ttlSeconds !== undefined; } -export function isJoinedChannelMessage(message: ApiMessage) { - return message.content.action && message.content.action.type === 'joinedChannel'; -} - export function getAttachmentMediaType(attachment: ApiAttachment) { if (SUPPORTED_AUDIO_CONTENT_TYPES.has(attachment.mimeType)) { return 'audio'; diff --git a/src/global/helpers/payments.ts b/src/global/helpers/payments.ts index 83292b82e..3a970b228 100644 --- a/src/global/helpers/payments.ts +++ b/src/global/helpers/payments.ts @@ -317,9 +317,9 @@ export function getStarsTransactionFromGift(message: ApiMessage): ApiStarsTransa const { transactionId, stars } = action; return { - id: transactionId!, + id: transactionId, stars: { - amount: stars!, + amount: stars, nanos: 0, }, peer: { @@ -337,17 +337,17 @@ export function getPrizeStarsTransactionFromGiveaway(message: ApiMessage): ApiSt if (action?.type !== 'prizeStars') return undefined; - const { transactionId, stars, targetChatId } = action; + const { transactionId, stars, boostPeerId } = action; return { - id: transactionId!, + id: transactionId, stars: { - amount: stars!, + amount: stars, nanos: 0, }, peer: { type: 'peer', - id: targetChatId!, + id: boostPeerId, }, date: message.date, giveawayPostId: message.id, diff --git a/src/global/initialState.ts b/src/global/initialState.ts index 5d65c8361..204d4a9dd 100644 --- a/src/global/initialState.ts +++ b/src/global/initialState.ts @@ -70,6 +70,7 @@ export const INITIAL_PERFORMANCE_STATE_MIN: PerformanceType = { }; export const INITIAL_GLOBAL_STATE: GlobalState = { + cacheVersion: 1, isInited: true, attachMenu: { bots: {} }, passcode: {}, diff --git a/src/global/reducers/messages.ts b/src/global/reducers/messages.ts index 068aab739..8991aa5ed 100644 --- a/src/global/reducers/messages.ts +++ b/src/global/reducers/messages.ts @@ -253,15 +253,19 @@ export function updateChatMessage( if (message && messageUpdate.isMediaUnread === false && hasMessageTtl(message)) { if (message.content.voice) { messageUpdate.content = { - ...messageUpdate.content, - voice: undefined, - isExpiredVoice: true, + action: { + mediaType: 'action', + type: 'expired', + isVoice: true, + }, }; } else if (message.content.video?.isRound) { messageUpdate.content = { - ...messageUpdate.content, - video: undefined, - isExpiredRoundVideo: true, + action: { + mediaType: 'action', + type: 'expired', + isRoundVideo: true, + }, }; } } diff --git a/src/global/selectors/messages.ts b/src/global/selectors/messages.ts index 10a135586..f6e3d4dcd 100644 --- a/src/global/selectors/messages.ts +++ b/src/global/selectors/messages.ts @@ -646,7 +646,7 @@ export function selectAllowedMessageActionsSlow( const hasTtl = hasMessageTtl(message); const { content } = message; const isDocumentSticker = isMessageDocumentSticker(message); - const isBoostMessage = message.content.action?.type === 'chatBoost'; + const isBoostMessage = message.content.action?.type === 'boostApply'; const hasChatPinPermission = (chat.isCreator || (!isChannel && !isUserRightBanned(chat, 'pinMessages')) @@ -935,9 +935,7 @@ export function selectFirstUnreadId( return ( (!lastReadId || id > lastReadId) && byId[id] - // For some reason outgoing topic actions are not marked as read, thus we need to mark them as read - // when the edit message hits the viewport - && ((!byId[id].isOutgoing || byId[id].content.action?.isTopicAction) || byId[id].isFromScheduled) + && (!byId[id].isOutgoing || byId[id].isFromScheduled) && id > lastReadServiceNotificationId ); }); diff --git a/src/global/types/globalState.ts b/src/global/types/globalState.ts index 5de41c537..b9c299f9c 100644 --- a/src/global/types/globalState.ts +++ b/src/global/types/globalState.ts @@ -73,6 +73,7 @@ import type { RegularLangFnParameters } from '../../util/localization'; import type { TabState } from './tabState'; export type GlobalState = { + cacheVersion: number; isInited: boolean; config?: ApiConfig; appConfig?: ApiAppConfig; diff --git a/src/hooks/element/useStyleObserver.ts b/src/hooks/element/useStyleObserver.ts new file mode 100644 index 000000000..b22ee9b23 --- /dev/null +++ b/src/hooks/element/useStyleObserver.ts @@ -0,0 +1,71 @@ +import { useEffect, useLayoutEffect, useState } from '../../lib/teact/teact'; + +import useLastCallback from '../useLastCallback'; +import useResizeObserver from '../useResizeObserver'; + +const UPDATE_DEBOUNCE = 50; // ms + +/** + * @param property animateable property + */ +export default function useStyleObserver( + ref: React.RefObject, + property: string, + debounce = UPDATE_DEBOUNCE, + isDisabled?: boolean, +) { + const [value, setValue] = useState(); + + const updateValue = useLastCallback(() => { + if (!ref.current || isDisabled) { + setValue(undefined); + return; + } + + const computedValue = getComputedStyle(ref.current).getPropertyValue(property).trim(); + setValue(computedValue); + }); + + // Element does not receive `transitionend` event if parent has `display: none`. + // We will receive `resize` event when parent is shown again. + useResizeObserver(ref, updateValue, isDisabled); + + useLayoutEffect(() => { + const el = ref.current; + if (!el || isDisabled) { + return undefined; + } + + el.style.setProperty('transition', `${debounce}ms ${property} linear`, 'important'); + + return () => { + el.style.removeProperty('transition'); + }; + }, [debounce, isDisabled, property, ref]); + + useEffect(() => { + const el = ref.current; + if (!el) { + return undefined; + } + + updateValue(); + + if (isDisabled) { + return undefined; + } + + function handleTransitionEnd(e: TransitionEvent) { + if (e.propertyName !== property) return; + updateValue(); + } + + el.addEventListener('transitionend', handleTransitionEnd); + + return () => { + el.removeEventListener('transitionend', handleTransitionEnd); + }; + }, [isDisabled, property, ref, updateValue]); + + return value; +} diff --git a/src/hooks/stickers/useDynamicColorListener.ts b/src/hooks/stickers/useDynamicColorListener.ts index 073ebe665..a8b199cc5 100644 --- a/src/hooks/stickers/useDynamicColorListener.ts +++ b/src/hooks/stickers/useDynamicColorListener.ts @@ -1,69 +1,18 @@ -import { useEffect, useLayoutEffect, useState } from '../../lib/teact/teact'; +import { useMemo } from '../../lib/teact/teact'; -import { getPropertyHexColor } from '../../util/themeStyle'; -import useLastCallback from '../useLastCallback'; -import useResizeObserver from '../useResizeObserver'; +import { prepareHexColor } from '../../util/themeStyle'; +import useStyleObserver from '../element/useStyleObserver'; -// Transition required to detect `color` property change. -// Duration parameter describes a delay between color change and color state update. -// Small values may cause large amount of re-renders. -const TRANSITION_PROPERTY = 'color'; -const TRANSITION_STYLE = `50ms ${TRANSITION_PROPERTY} linear`; +const DEBOUNCE = 50; // ms -export default function useDynamicColorListener(ref: React.RefObject, - isDisabled?: boolean) { - const [hexColor, setHexColor] = useState(); - - const updateColor = useLastCallback(() => { - if (!ref.current || isDisabled) { - setHexColor(undefined); - return; - } - - const currentHexColor = getPropertyHexColor(getComputedStyle(ref.current), TRANSITION_PROPERTY); - setHexColor(currentHexColor); - }); - - // Element does not receive `transitionend` event if parent has `display: none`. - // We will receive `resize` event when parent is shown again. - useResizeObserver(ref, updateColor, isDisabled); - - useLayoutEffect(() => { - const el = ref.current; - if (!el || isDisabled) { - return undefined; - } - - el.style.setProperty('transition', TRANSITION_STYLE, 'important'); - - return () => { - el.style.removeProperty('transition'); - }; - }, [isDisabled, ref]); - - useEffect(() => { - const el = ref.current; - if (!el) { - return undefined; - } - - updateColor(); - - if (isDisabled) { - return undefined; - } - - function handleTransitionEnd(e: TransitionEvent) { - if (e.propertyName !== TRANSITION_PROPERTY) return; - updateColor(); - } - - el.addEventListener('transitionend', handleTransitionEnd); - - return () => { - el.removeEventListener('transitionend', handleTransitionEnd); - }; - }, [isDisabled, ref, updateColor]); +// Style observer that returns hex color value of the property +export default function useDynamicColorListener( + ref: React.RefObject, + property = 'color', + isDisabled?: boolean, +) { + const value = useStyleObserver(ref, property, DEBOUNCE, isDisabled); + const hexColor = useMemo(() => (value ? prepareHexColor(value) : undefined), [value]); return hexColor; } diff --git a/src/hooks/useThumbnail.ts b/src/hooks/useThumbnail.ts index b1d8a3393..4fc20b2b7 100644 --- a/src/hooks/useThumbnail.ts +++ b/src/hooks/useThumbnail.ts @@ -1,14 +1,14 @@ import { useMemo } from '../lib/teact/teact'; import { getGlobal } from '../global'; -import type { ApiSticker, MediaContainer } from '../api/types'; +import type { ApiThumbnail, MediaContainer } from '../api/types'; import { getMessageMediaThumbDataUri } from '../global/helpers'; import { selectTheme } from '../global/selectors'; -export default function useThumbnail(media?: MediaContainer | ApiSticker) { +export default function useThumbnail(media?: MediaContainer | ApiThumbnail) { const isMediaContainer = media && 'content' in media; - const thumbDataUri = isMediaContainer ? getMessageMediaThumbDataUri(media) : media?.thumbnail?.dataUri; + const thumbDataUri = isMediaContainer ? getMessageMediaThumbDataUri(media) : media?.dataUri; // TODO Find a way to update thumbnail on theme change const theme = selectTheme(getGlobal()); diff --git a/src/types/index.ts b/src/types/index.ts index b9ecb696f..bb7b3fd26 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -300,6 +300,7 @@ export enum MediaViewerOrigin { Album, ScheduledAlbum, SearchResult, + ChannelAvatar, SuggestedAvatar, StarsTransaction, PreviewMedia, diff --git a/src/types/language.d.ts b/src/types/language.d.ts index ad46ba74f..b8622b179 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -134,6 +134,7 @@ export interface LangPair { 'SentAppCode': undefined; 'LoginJustSentSms': undefined; 'Code': undefined; + 'Open': undefined; 'LoginHeaderPassword': undefined; 'LoginEnterPasswordDescription': undefined; 'StartText': undefined; @@ -533,6 +534,8 @@ export interface LangPair { 'Block': undefined; 'DeleteThisChat': undefined; 'Caption': undefined; + 'CropperTitle': undefined; + 'CropperApply': undefined; 'AttachmentMenuPhotoOrVideo': undefined; 'AttachDocument': undefined; 'Poll': undefined; @@ -1040,14 +1043,6 @@ export interface LangPair { 'EditProfileNoFirstName': undefined; 'AriaEditProfilePhoto': undefined; 'LaunchConfetti': undefined; - 'SettingsAnimations': undefined; - 'SettingsAnimationsDescription': undefined; - 'SettingsAnimationsLow': undefined; - 'SettingsAnimationsMedium': undefined; - 'SettingsAnimationsHigh': undefined; - 'Settings12HourFormat': undefined; - 'Settings24HourFormat': undefined; - 'SettingsSendCtrlEnterDescription': undefined; 'AriaMoreButton': undefined; 'RecoveryEmailCode': undefined; 'NotificationsWeb': undefined; @@ -1169,6 +1164,7 @@ export interface LangPair { 'GiftSoldOut': undefined; 'GiftMessagePlaceholder': undefined; 'GiftHideMyName': undefined; + 'GiftHideNameDescriptionSelf': undefined; 'GiftHideNameDescriptionChannel': undefined; 'GiftInfoSent': undefined; 'GiftInfoReceived': undefined; @@ -1225,14 +1221,6 @@ export interface LangPair { 'PremiumGiftDescription': undefined; 'StarsReactionLinkText': undefined; 'StarsReactionLink': undefined; - 'ActionStarGiftDisplaying': undefined; - 'ActionStarGiftChannelDisplaying': undefined; - 'ActionStarGiftDescriptionUpgrade': undefined; - 'ActionStarGiftUpgraded': undefined; - 'ActionStarGiftUnpack': undefined; - 'GiftTo': undefined; - 'GiftFrom': undefined; - 'ReceivedGift': undefined; 'SentGift': undefined; 'StarsSubscribeInfoLinkText': undefined; 'StarsSubscribeInfoLink': undefined; @@ -1324,6 +1312,80 @@ export interface LangPair { 'CheckPasswordTitle': undefined; 'CheckPasswordPlaceholder': undefined; 'CheckPasswordDescription': undefined; + 'ActionFallbackUser': undefined; + 'ActionFallbackChat': undefined; + 'ActionFallbackChannel': undefined; + 'ActionFallbackSomeone': undefined; + 'ActionUnsupported': undefined; + 'ActionPinnedNotFoundYou': undefined; + 'ActionPinnedMediaPhoto': undefined; + 'ActionPinnedMediaVideo': undefined; + 'ActionPinnedMediaAudio': undefined; + 'ActionPinnedMediaVoice': undefined; + 'ActionPinnedMediaVideoMessage': undefined; + 'ActionPinnedMediaFile': undefined; + 'ActionPinnedMediaGif': undefined; + 'ActionPinnedMediaContact': undefined; + 'ActionPinnedMediaLocation': undefined; + 'ActionPinnedMediaSticker': undefined; + 'ActionPinnedMediaInvoice': undefined; + 'ActionPinnedMediaStory': undefined; + 'ActionPinnedMediaAlbum': undefined; + 'ActionPinnedMediaPoll': undefined; + 'ActionPinnedMediaGiveaway': undefined; + 'ActionPinnedMediaGiveawayResults': undefined; + 'ActionGroupCallStartedChannel': undefined; + 'ActionExpiredVoice': undefined; + 'ActionExpiredVideo': undefined; + 'ActionChannelJoinedYou': undefined; + 'ActionChannelJoinedByRequestChannelYou': undefined; + 'ActionUserLeftYou': undefined; + 'ActionUserJoinedYou': undefined; + 'ActionJoinedByRequestYou': undefined; + 'ActionRemovedPhotoYou': undefined; + 'ActionRemovedPhotoChannel': undefined; + 'ActionChangedPhotoYou': undefined; + 'ActionChangedPhotoChannel': undefined; + 'ActionCreatedChannel': undefined; + 'ActionScreenshotTakenYou': undefined; + 'ActionBotAppPlaceholder': undefined; + 'ActionGiftTextUnknown': undefined; + 'ActionGiftUniqueSent': undefined; + 'ActionStarGiftUpgradedSelf': undefined; + 'ActionStarGiftTransferredSelf': undefined; + 'ActionStarGiftTransferredUnknown': undefined; + 'ActionStarGiftNoConvertTextYou': undefined; + 'ActionStarGiftDisplaying': undefined; + 'ActionStarGiftChannelDisplaying': undefined; + 'ActionStarGiftUpgradeTextYou': undefined; + 'ActionStarGiftUpgraded': undefined; + 'ActionStarGiftUnpack': undefined; + 'ActionStarGiftUniqueRibbon': undefined; + 'ActionStarGiftUniqueModel': undefined; + 'ActionStarGiftUniqueBackdrop': undefined; + 'ActionStarGiftUniqueSymbol': undefined; + 'ActionStarGiftSelf': undefined; + 'ActionSuggestedPhotoButton': undefined; + 'ActionSuggestedVideoTitle': undefined; + 'ActionSuggestedVideoText': undefined; + 'ActionSuggestedPhotoUpdatedTitle': undefined; + 'ActionSuggestedPhotoUpdatedDescription': undefined; + 'ActionAttachMenuBotAllowed': undefined; + 'ActionWebappBotAllowed': undefined; + 'ActionTopicClosedInside': undefined; + 'ActionTopicReopenedInside': undefined; + 'ActionTopicHiddenInside': undefined; + 'ActionTopicUnhiddenInside': undefined; + 'ActionTopicPlaceholder': undefined; + 'ActionGiveawayResultsSome': undefined; + 'ActionGiveawayResultsStarsSome': undefined; + 'ActionGiveawayResultsNone': undefined; + 'ActionOpenGiftButton': undefined; + 'ActionViewButton': undefined; + 'ActionGiveawayResultTitle': undefined; + 'ActionGiftPremiumText': undefined; + 'ActionGiftStarsText': undefined; + 'ActionHistoryCleared': undefined; 'UniqueStatusBenefitsDescription': undefined; 'UniqueStatusBadgeBenefitTitle': undefined; 'UniqueStatusBadgeDescription': undefined; @@ -1339,13 +1401,6 @@ export interface LangPair { } export interface LangPairWithVariables { - 'ChatServiceGroupUpdatedPinnedMessage1': { - 'user': V; - 'message': V; - }; - 'MessagePinnedGenericMessage': { - 'user': V; - }; 'UserTyping': { 'user': V; }; @@ -1791,16 +1846,6 @@ export interface LangPairWithVariables { 'StarsReactionTerms': { 'link': V; }; - 'ActionStarGiftPeerTitle': { - 'peer': V; - 'count': V; - }; - 'ActionStarGiftOutTitle': { - 'count': V; - }; - 'ActionStarGiftPeerOutDescriptionUpgrade': { - 'peer': V; - }; 'StarsSubscribeInfo': { 'link': V; }; @@ -1850,6 +1895,318 @@ export interface LangPairWithVariables { 'MoreSimilarBotsText': { 'count': V; }; + 'ActionPinnedText': { + 'from': V; + 'text': V; + }; + 'ActionPinnedTextYou': { + 'text': V; + }; + 'ActionPinnedNotFound': { + 'from': V; + }; + 'ActionPinnedMedia': { + 'from': V; + 'media': V; + }; + 'ActionPinnedMediaYou': { + 'media': V; + }; + 'ActionPinnedMediaGame': { + 'game': V; + }; + 'ActionGroupCallStartedGroup': { + 'from': V; + }; + 'ActionGroupCallScheduledGroup': { + 'from': V; + 'date': V; + }; + 'ActionGroupCallScheduledChannel': { + 'date': V; + }; + 'ActionGroupCallFinishedChannel': { + 'duration': V; + }; + 'ActionGroupCallFinishedGroup': { + 'from': V; + 'duration': V; + }; + 'ActionAddUser': { + 'from': V; + 'user': V; + }; + 'ActionAddUserYou': { + 'user': V; + }; + 'ActionAddUsersMany': { + 'from': V; + 'users': V; + }; + 'ActionAddUsersManyYou': { + 'users': V; + }; + 'ActionAddYou': { + 'from': V; + }; + 'ActionAddYouGroup': { + 'from': V; + }; + 'ActionKickUser': { + 'from': V; + 'user': V; + }; + 'ActionKickUserYou': { + 'user': V; + }; + 'ActionUserLeft': { + 'from': V; + }; + 'ActionUserJoined': { + 'from': V; + }; + 'ActionUserJoinedByLink': { + 'from': V; + }; + 'ActionJoinedByRequest': { + 'from': V; + }; + 'ActionVideoInvited': { + 'from': V; + 'user': V; + }; + 'ActionVideoInvitedYou': { + 'user': V; + }; + 'ActionVideoInvitedMany': { + 'from': V; + 'users': V; + }; + 'ActionVideoInvitedManyYou': { + 'users': V; + }; + 'ActionUserRegistered': { + 'from': V; + }; + 'ActionRemovedPhoto': { + 'from': V; + }; + 'ActionChangedPhoto': { + 'from': V; + }; + 'ActionChangedTitle': { + 'from': V; + 'title': V; + }; + 'ActionChangedTitleYou': { + 'title': V; + }; + 'ActionChangedTitleChannel': { + 'title': V; + }; + 'ActionCreatedChat': { + 'from': V; + 'title': V; + }; + 'ActionPaymentDone': { + 'amount': V; + 'user': V; + }; + 'ActionPaymentDoneFor': { + 'amount': V; + 'user': V; + 'invoice': V; + }; + 'ActionPaymentInitRecurringFor': { + 'amount': V; + 'user': V; + 'invoice': V; + }; + 'ActionPaymentInitRecurring': { + 'amount': V; + 'user': V; + }; + 'ActionPaymentUsedRecurring': { + 'amount': V; + }; + 'ActionScreenshotTaken': { + 'from': V; + }; + 'ActionBotAllowedFromDomain': { + 'domain': V; + }; + 'ActionBotAllowedFromApp': { + 'app': V; + }; + 'ActionGiftTextCost': { + 'from': V; + 'cost': V; + }; + 'ActionGiftTextCostYou': { + 'cost': V; + }; + 'ActionGiftTextCostAnonymous': { + 'cost': V; + }; + 'ActionCostCrypto': { + 'price': V; + 'cryptoPrice': V; + }; + 'ActionWebviewDataDone': { + 'text': V; + }; + 'ActionGiftUniqueReceived': { + 'user': V; + }; + 'ActionStarGiftReceived': { + 'user': V; + 'cost': V; + }; + 'ActionStarGiftSent': { + 'cost': V; + }; + 'ActionStarGiftUpgradedUser': { + 'user': V; + }; + 'ActionStarGiftUpgradedChannel': { + 'user': V; + 'channel': V; + }; + 'ActionStarGiftUpgradedChannelYou': { + 'channel': V; + }; + 'ActionStarGiftUpgradedMine': { + 'user': V; + }; + 'ActionStarGiftTransferred': { + 'user': V; + }; + 'ActionStarGiftTransferredChannel': { + 'user': V; + 'channel': V; + }; + 'ActionStarGiftTransferredChannelYou': { + 'channel': V; + }; + 'ActionStarGiftTransferredMine': { + 'user': V; + }; + 'ActionStarGiftTransferredUnknownChannel': { + 'channel': V; + }; + 'ActionStarGiftReceivedAnonymous': { + 'cost': V; + }; + 'ActionStarGiftSentChannel': { + 'user': V; + 'channel': V; + 'cost': V; + }; + 'ActionStarGiftSentChannelYou': { + 'channel': V; + 'cost': V; + }; + 'ActionStarGiftSelfBought': { + 'cost': V; + }; + 'ActionStarGiftTo': { + 'peer': V; + }; + 'ActionStarGiftFrom': { + 'peer': V; + }; + 'ActionStarGiftConvertText': { + 'peer': V; + 'amount': V; + }; + 'ActionStarGiftConvertTextYou': { + 'amount': V; + }; + 'ActionStarGiftNoConvertText': { + 'peer': V; + }; + 'ActionStarGiftConvertedText': { + 'peer': V; + 'amount': V; + }; + 'ActionStarGiftConvertedTextYou': { + 'amount': V; + }; + 'ActionStarGiftChannelText': { + 'amount': V; + }; + 'ActionStarGiftUpgradeText': { + 'peer': V; + }; + 'ActionStarGiftLimitedRibbon': { + 'total': V; + }; + 'ActionSuggestedPhotoYou': { + 'user': V; + }; + 'ActionSuggestedPhoto': { + 'user': V; + }; + 'ActionTopicCreated': { + 'topic': V; + }; + 'ActionTopicClosed': { + 'from': V; + 'topic': V; + }; + 'ActionTopicReopened': { + 'from': V; + 'topic': V; + }; + 'ActionTopicHidden': { + 'topic': V; + }; + 'ActionTopicUnhidden': { + 'topic': V; + }; + 'ActionTopicRenamed': { + 'from': V; + 'link': V; + 'title': V; + }; + 'ActionTopicIconChanged': { + 'from': V; + 'link': V; + 'emoji': V; + }; + 'ActionTopicIconRemoved': { + 'from': V; + 'link': V; + }; + 'ActionTopicIconAndRenamed': { + 'from': V; + 'link': V; + 'topic': V; + }; + 'ActionGiveawayStartedGroup': { + 'from': V; + }; + 'ActionGiveawayStarted': { + 'from': V; + }; + 'ActionGiveawayStarsStartedGroup': { + 'from': V; + 'amount': V; + }; + 'ActionGiveawayStarsStarted': { + 'from': V; + 'amount': V; + }; + 'ActionPaymentRefunded': { + 'peer': V; + 'amount': V; + }; + 'ActionMigratedFrom': { + 'chat': V; + }; + 'ActionMigratedTo': { + 'chat': V; + }; 'UniqueStatusWearTitle': { 'gift': V; }; @@ -2043,13 +2400,6 @@ export interface LangPairPluralWithVariables { 'PrizeCredits2': { 'count': V; }; - 'ActionStarGiftPeerOutDescription': { - 'peer': V; - 'count': V; - }; - 'ActionStarGiftDescription2': { - 'count': V; - }; 'StarsSubscribeText': { 'chat': V; 'amount': V; @@ -2074,6 +2424,53 @@ export interface LangPairPluralWithVariables { 'FolderLinkNotificationUpdatedSubtitle': { 'count': V; }; + 'ActionGameScore': { + 'from': V; + 'count': V; + 'game': V; + }; + 'ActionGameScoreYou': { + 'count': V; + 'game': V; + }; + 'ActionGameScoreNoGame': { + 'from': V; + 'count': V; + }; + 'ActionGameScoreNoGameYou': { + 'count': V; + }; + 'ActionGiveawayResults': { + 'count': V; + }; + 'ActionGiveawayResultsStars': { + 'count': V; + }; + 'ActionGiveawayResultPremiumText': { + 'channel': V; + 'months': V; + }; + 'ActionGiftCodePremiumText': { + 'channel': V; + 'months': V; + }; + 'ActionGiveawayResultStarsText': { + 'channel': V; + 'amount': V; + }; + 'ActionGiftPremiumTitle': { + 'months': V; + }; + 'ActionGiftStarsTitle': { + 'amount': V; + }; + 'ActionBoostApplyYou': { + 'count': V; + }; + 'ActionBoostApply': { + 'from': V; + 'count': V; + }; } export type RegularLangKey = keyof LangPair; export type RegularLangKeyWithVariables = keyof LangPairWithVariables; diff --git a/src/util/browser/intlListFormat.ts b/src/util/browser/intlListFormat.ts index f3b870342..a92262a9c 100644 --- a/src/util/browser/intlListFormat.ts +++ b/src/util/browser/intlListFormat.ts @@ -1,5 +1,22 @@ +type Part = { + type: 'literal' | 'element'; + value: string; +}; + export function getBasicListFormat() { return { format: (items: string[]) => items.join(', '), + formatToParts: (items: string[]): Part[] => { + const result: Part[] = []; + + items.forEach((item, i) => { + if (i > 0) { + result.push({ type: 'literal', value: ', ' }); + } + result.push({ type: 'element', value: item }); + }); + + return result; + }, }; } diff --git a/src/util/colors.ts b/src/util/colors.ts index 83c8ae5e3..194c8da3b 100644 --- a/src/util/colors.ts +++ b/src/util/colors.ts @@ -192,6 +192,12 @@ export function getColorLuma(rgbColor: [number, number, number]) { const luma = 0.2126 * r + 0.7152 * g + 0.0722 * b; return luma; } +// https://stackoverflow.com/a/64090995 +export function hsl2rgb([h, s, l]: [number, number, number]): [number, number, number] { + let a = s * Math.min(l, 1 - l); + let f = (n: number, k = (n + h / 30) % 12) => l - a * Math.max(Math.min(k - 3, 9 - k, 1), -1); + return [f(0), f(8), f(4)]; +} // Function was adapted from https://github.com/telegramdesktop/tdesktop/blob/35ff621b5b52f7e3553fb0f990ea13ade7101b8e/Telegram/SourceFiles/data/data_wall_paper.cpp#L518 export function getPatternColor(rgbColor: [number, number, number]) { @@ -202,7 +208,9 @@ export function getPatternColor(rgbColor: [number, number, number]) { ? Math.max(0, value * 0.65) : Math.max(0, Math.min(1, 1 - value * 0.65)); - return `hsla(${hue * 360}, ${saturation * 100}%, ${value * 100}%, .4)`; + const rgb = hsl2rgb([hue * 360, saturation, value]); + const hex = rgb2hex(rgb.map((c) => Math.floor(c * 255)) as [number, number, number]); + return `#${hex}66`; } /* eslint-disable no-bitwise */ diff --git a/src/util/data/readStrings.ts b/src/util/data/readStrings.ts index 0100dbc41..f26859709 100644 --- a/src/util/data/readStrings.ts +++ b/src/util/data/readStrings.ts @@ -11,6 +11,10 @@ export default function readStrings(data: string): Record { console.warn('Bad formatting in line:', line); continue; } + if (result[key]) { + // eslint-disable-next-line no-console + console.warn('Duplicate key:', key); + } result[key] = value; } return result; diff --git a/src/util/formatCurrency.tsx b/src/util/formatCurrency.tsx index dd191933c..1184133ac 100644 --- a/src/util/formatCurrency.tsx +++ b/src/util/formatCurrency.tsx @@ -1,25 +1,27 @@ -import React, { type TeactNode } from '../lib/teact/teact'; +import { type TeactNode } from '../lib/teact/teact'; + +import type { LangFn } from './localization'; import { STARS_CURRENCY_CODE } from '../config'; - -import StarIcon from '../components/common/icons/StarIcon'; +import { formatStarsAsIcon } from './localization/format'; export function formatCurrency( + lang: LangFn, totalPrice: number, currency: string, - locale: string = 'en', options?: { shouldOmitFractions?: boolean; iconClassName?: string; + asFontIcon?: boolean; }, ): TeactNode { const price = totalPrice / 10 ** getCurrencyExp(currency); if (currency === STARS_CURRENCY_CODE) { - return [, price]; + return formatStarsAsIcon(lang, price, { asFont: options?.asFontIcon, className: options?.iconClassName }); } - return formatCurrencyAsString(totalPrice, currency, locale, options); + return formatCurrencyAsString(totalPrice, currency, lang.code, options); } export function formatCurrencyAsString( diff --git a/src/util/localization/index.ts b/src/util/localization/index.ts index 6d794d2ad..7b8d5a506 100644 --- a/src/util/localization/index.ts +++ b/src/util/localization/index.ts @@ -205,10 +205,11 @@ export async function initLocalization(langCode: string, canLoadFromServer?: boo fetchDifference(); } else if (canLoadFromServer) { await loadAndChangeLanguage(langCode); - } else { - loadFallbackPack(); } + // Always start loading fallback pack in the background. Some languages may not have every string translated. + loadFallbackPack(); + translationFn = createTranslationFn(); scheduleCallbacks(); localizationReady.resolve(); @@ -330,6 +331,7 @@ function createTranslationFn(): LangFn { fn.conjunction = (list: string[]) => formatters?.conjunction.format(list) || list.join(', '); fn.disjunction = (list: string[]) => formatters?.disjunction.format(list) || list.join(', '); fn.number = (value: number) => formatters?.number.format(value) || String(value); + fn.internalFormatters = formatters!; fn.languageInfo = language!; return fn; } diff --git a/src/util/localization/types.ts b/src/util/localization/types.ts index b5b3411ac..0d0e5ec1f 100644 --- a/src/util/localization/types.ts +++ b/src/util/localization/types.ts @@ -155,13 +155,15 @@ export type LangFn = { conjunction: (list: string[]) => string; disjunction: (list: string[]) => string; number: (value: number) => string; + internalFormatters: LangFormatters; isRtl?: boolean; code: string; pluralCode: string; languageInfo: ApiLanguage; }; -type ListFormat = Pick; +// Allow basic polyfill +type ListFormat = Pick; export type LangFormatters = { pluralRules: Intl.PluralRules; diff --git a/src/util/localization/utils.ts b/src/util/localization/utils.ts new file mode 100644 index 000000000..63a75e29f --- /dev/null +++ b/src/util/localization/utils.ts @@ -0,0 +1,39 @@ +import type { TeactNode } from '../../lib/teact/teact'; + +import type { LangFn } from './types'; + +const UNIQUE_PLACEHOLDER_PREFIX = '$PLACEHOLDER-'; + +export function conjuctionWithNodes(langFn: LangFn, nodes: TeactNode[]) { + const placeholders = nodes.map((node, i) => `${UNIQUE_PLACEHOLDER_PREFIX}${i}`); + const replaced = langFn.internalFormatters.conjunction.formatToParts(placeholders); + const result: TeactNode[] = []; + replaced.forEach((part) => { + if (part.type === 'literal') { + result.push(part.value); + return; + } + + const index = Number(part.value.slice(UNIQUE_PLACEHOLDER_PREFIX.length)); + result.push(nodes[index]); + }); + + return result; +} + +export function disjunctionWithNodes(langFn: LangFn, nodes: TeactNode[]) { + const placeholders = nodes.map((node, i) => `${UNIQUE_PLACEHOLDER_PREFIX}${i}`); + const replaced = langFn.internalFormatters.disjunction.formatToParts(placeholders); + const result: TeactNode[] = []; + replaced.forEach((part) => { + if (part.type === 'literal') { + result.push(part.value); + return; + } + + const index = Number(part.value.slice(UNIQUE_PLACEHOLDER_PREFIX.length)); + result.push(nodes[index]); + }); + + return result; +} diff --git a/src/util/notifications.ts b/src/util/notifications.tsx similarity index 89% rename from src/util/notifications.ts rename to src/util/notifications.tsx index 9e0e7208e..a1b10c843 100644 --- a/src/util/notifications.ts +++ b/src/util/notifications.tsx @@ -1,3 +1,4 @@ +import React from '../lib/teact/teact'; import { getActions, getGlobal, setGlobal } from '../global'; import type { @@ -10,38 +11,33 @@ import { APP_NAME, DEBUG, IS_TEST } from '../config'; import { getChatAvatarHash, getChatTitle, - getMessageAction, getMessageRecentReaction, getMessageSenderName, - getMessageStatefulContent, getPrivateChatUserId, getUserFullName, - isActionMessage, isChatChannel, selectIsChatMuted, selectShouldShowMessagePreview, } from '../global/helpers'; -import { getMessageSummaryText } from '../global/helpers/messageSummary'; -import { getMessageReplyInfo } from '../global/helpers/replies'; import { addNotifyExceptions, replaceSettings } from '../global/reducers'; import { selectChat, - selectChatMessage, selectCurrentMessageList, selectIsChatWithSelf, selectNotifyExceptions, selectNotifySettings, - selectTopicFromMessage, selectUser, } from '../global/selectors'; import { callApi } from '../api/gramjs'; -import { renderActionMessageText } from '../components/common/helpers/renderActionMessageText'; +import jsxToHtml from './element/jsxToHtml'; import { buildCollectionByKey } from './iteratees'; import * as mediaLoader from './mediaLoader'; import { oldTranslate } from './oldLangProvider'; import { debounce } from './schedulers'; import { IS_ELECTRON, IS_SERVICE_WORKER_SUPPORTED, IS_TOUCH_ENV } from './windowEnvironment'; +import MessageSummary from '../components/common/MessageSummary'; + function getDeviceToken(subscription: PushSubscription) { const data = subscription.toJSON(); return JSON.stringify({ @@ -300,7 +296,7 @@ function checkIfShouldNotify(chat: ApiChat, message: Partial) { if (!areSettingsLoaded) return false; const global = getGlobal(); const isMuted = selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global)); - const shouldNotifyAboutMessage = !message.content?.action?.phoneCall; + const shouldNotifyAboutMessage = message.content?.action?.type !== 'phoneCall'; if (isMuted || !shouldNotifyAboutMessage || chat.isNotJoined || !chat.isListed || selectIsChatWithSelf(global, chat.id)) { return false; @@ -328,26 +324,9 @@ function getNotificationContent(chat: ApiChat, message: ApiMessage, reaction?: A const { isScreenLocked } = global.passcode; const messageSenderChat = senderId ? selectChat(global, senderId) : undefined; const messageSenderUser = senderId ? selectUser(global, senderId) : undefined; - const messageAction = getMessageAction(message as ApiMessage); - - const replyInfo = getMessageReplyInfo(message); - const actionTargetMessage = messageAction && replyInfo?.replyToMsgId - ? selectChatMessage(global, replyInfo?.replyFrom?.fromChatId || chat.id, replyInfo.replyToMsgId) - : undefined; - const { - targetUserIds: actionTargetUserIds, - targetChatId: actionTargetChatId, - } = messageAction || {}; - - const actionTargetUsers = actionTargetUserIds - ? actionTargetUserIds.map((userId) => selectUser(global, userId)) - .filter(Boolean) - : undefined; const privateChatUserId = getPrivateChatUserId(chat); const isSelf = privateChatUserId === global.currentUserId; - const topic = selectTopicFromMessage(global, message); - let body: string; if ( !isScreenLocked @@ -355,31 +334,16 @@ function getNotificationContent(chat: ApiChat, message: ApiMessage, reaction?: A ) { const isChat = chat && (isChatChannel(chat) || message.senderId === message.chatId); - if (isActionMessage(message)) { - body = renderActionMessageText( - oldTranslate, - message, - messageSenderUser, - chat, - actionTargetUsers, - actionTargetMessage, - actionTargetChatId, - topic, - { asPlainText: true }, - ) as string; - } else { - // TODO[forums] Support ApiChat - const senderName = getMessageSenderName(oldTranslate, chat.id, isChat ? messageSenderChat : messageSenderUser); - const statefulContent = getMessageStatefulContent(global, message); - let summary = getMessageSummaryText(oldTranslate, message, statefulContent, hasReaction, 60); + // TODO[forums] Support ApiChat + const senderName = getMessageSenderName(oldTranslate, chat.id, isChat ? messageSenderChat : messageSenderUser); + let summary = jsxToHtml()[0].textContent || ''; - if (hasReaction) { - const emoji = getReactionEmoji(reaction); - summary = oldTranslate('PushReactText', [emoji, summary]); - } - - body = senderName ? `${senderName}: ${summary}` : summary; + if (hasReaction) { + const emoji = getReactionEmoji(reaction); + summary = oldTranslate('PushReactText', [emoji, summary]); } + + body = senderName ? `${senderName}: ${summary}` : summary; } else { body = 'New message'; } diff --git a/src/util/themeStyle.ts b/src/util/themeStyle.ts index d054b9fa2..66ba0f563 100644 --- a/src/util/themeStyle.ts +++ b/src/util/themeStyle.ts @@ -50,7 +50,7 @@ export function getPropertyHexColor(style: CSSStyleDeclaration, property: string return prepareHexColor(value.trim()); } -function prepareHexColor(color: string) { +export function prepareHexColor(color: string) { if (validateHexColor(color)) return color; return `#${color.match(/^rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*(\d+\.{0,1}\d*))?\)$/)! .slice(1) diff --git a/src/util/windowEnvironment.ts b/src/util/windowEnvironment.ts index 66d8fdb8b..89464c27f 100644 --- a/src/util/windowEnvironment.ts +++ b/src/util/windowEnvironment.ts @@ -84,6 +84,7 @@ export const IS_WAVE_TRANSFORM_SUPPORTED = !IS_MOBILE export const IS_SNAP_EFFECT_SUPPORTED = !IS_MOBILE && !IS_FIREFOX // https://bugzilla.mozilla.org/show_bug.cgi?id=1896504 && !IS_SAFARI; +export const IS_FLUID_BACKGROUND_SUPPORTED = !IS_FIREFOX; const TEST_VIDEO = document.createElement('video');