diff --git a/dev/icons.scss.hbs b/dev/icons.scss.hbs index 61767bffd..1a730fed5 100644 --- a/dev/icons.scss.hbs +++ b/dev/icons.scss.hbs @@ -1,13 +1,4 @@ @use "sass:map"; -${{ name }}-font: "{{ name }}"; - -@font-face { - font-family: ${{ name }}-font; - src: {{{ fontSrc }}}; - font-weight: normal; - font-style: normal; - font-display: block; -} .icon-char::before { font-family: Roboto, "Helvetica Neue", sans-serif; @@ -17,9 +8,7 @@ ${{ name }}-font: "{{ name }}"; display: block; } -{{# if selector }}{{ selector }}::before { -{{ else }}{{ tag }}.{{prefix}} { -{{/ if }} +@mixin icon { /* use !important to prevent issues with browser extensions that change fonts */ /* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */ font-family: "{{ name }}" !important; @@ -35,6 +24,12 @@ ${{ name }}-font: "{{ name }}"; -moz-osx-font-smoothing: grayscale; } +{{# if selector }}{{ selector }}::before { +{{ else }}{{ tag }}.{{prefix}} { +{{/ if }} + @include icon; +} + ${{ name }}-map: ( {{# each codepoints }} "{{ @key }}": "\\{{ codepoint this }}", diff --git a/src/api/gramjs/apiBuilders/appConfig.ts b/src/api/gramjs/apiBuilders/appConfig.ts index ac688885f..6e43d928e 100644 --- a/src/api/gramjs/apiBuilders/appConfig.ts +++ b/src/api/gramjs/apiBuilders/appConfig.ts @@ -52,6 +52,8 @@ export interface GramJsAppConfig extends LimitsConfig { story_expire_period: number; story_viewers_expire_period: number; stories_changelog_user_id?: number; + peer_colors: Record; + dark_peer_colors: Record; } function buildEmojiSounds(appConfig: GramJsAppConfig) { @@ -117,5 +119,7 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp storyExpirePeriod: appConfig.story_expire_period ?? STORY_EXPIRE_PERIOD, storyViewersExpirePeriod: appConfig.story_viewers_expire_period ?? STORY_VIEWERS_EXPIRE_PERIOD, storyChangelogUserId: appConfig.stories_changelog_user_id?.toString() ?? SERVICE_NOTIFICATIONS_USER_ID, + peerColors: appConfig.peer_colors, + darkPeerColors: appConfig.dark_peer_colors, }; } diff --git a/src/api/gramjs/apiBuilders/bots.ts b/src/api/gramjs/apiBuilders/bots.ts index 9a1e645be..7798ad9eb 100644 --- a/src/api/gramjs/apiBuilders/bots.ts +++ b/src/api/gramjs/apiBuilders/bots.ts @@ -72,7 +72,6 @@ export function buildBotSwitchWebview(switchWebview?: GramJs.InlineBotWebView) { export function buildApiAttachBot(bot: GramJs.AttachMenuBot): ApiAttachBot { return { id: bot.botId.toString(), - hasSettings: bot.hasSettings, shouldRequestWriteAccess: bot.requestWriteAccess, shortName: bot.shortName, isForAttachMenu: bot.showInAttachMenu!, diff --git a/src/api/gramjs/apiBuilders/chats.ts b/src/api/gramjs/apiBuilders/chats.ts index 98bb9b400..63aaedb5c 100644 --- a/src/api/gramjs/apiBuilders/chats.ts +++ b/src/api/gramjs/apiBuilders/chats.ts @@ -54,6 +54,8 @@ function buildApiChatFieldsFromPeerEntity( const areStoriesHidden = Boolean('storiesHidden' in peerEntity && peerEntity.storiesHidden); const maxStoryId = 'storiesMaxId' in peerEntity ? peerEntity.storiesMaxId : undefined; const storiesUnavailable = Boolean('storiesUnavailable' in peerEntity && peerEntity.storiesUnavailable); + const color = 'color' in peerEntity ? peerEntity.color : undefined; + const backgroundEmojiId = 'backgroundEmojiId' in peerEntity ? peerEntity.backgroundEmojiId?.toString() : undefined; return { isMin, @@ -66,7 +68,7 @@ function buildApiChatFieldsFromPeerEntity( ...('verified' in peerEntity && { isVerified: peerEntity.verified }), ...('callActive' in peerEntity && { isCallActive: peerEntity.callActive }), ...('callNotEmpty' in peerEntity && { isCallNotEmpty: peerEntity.callNotEmpty }), - ...('date' in peerEntity && { joinDate: peerEntity.date }), + ...('date' in peerEntity && { creationDate: peerEntity.date }), ...('participantsCount' in peerEntity && peerEntity.participantsCount !== undefined && { membersCount: peerEntity.participantsCount, }), @@ -77,6 +79,8 @@ function buildApiChatFieldsFromPeerEntity( ...buildApiChatRestrictions(peerEntity), ...buildApiChatMigrationInfo(peerEntity), fakeType: isScam ? 'scam' : (isFake ? 'fake' : undefined), + color, + backgroundEmojiId, isJoinToSend, isJoinRequest, isForum, diff --git a/src/api/gramjs/apiBuilders/messageContent.ts b/src/api/gramjs/apiBuilders/messageContent.ts index 845d211eb..74c96a4e6 100644 --- a/src/api/gramjs/apiBuilders/messageContent.ts +++ b/src/api/gramjs/apiBuilders/messageContent.ts @@ -8,7 +8,6 @@ import type { ApiGame, ApiInvoice, ApiLocation, - ApiMessage, ApiMessageExtendedMediaPreview, ApiMessageStoryData, ApiPhoto, @@ -19,6 +18,7 @@ import type { ApiWebDocument, ApiWebPage, ApiWebPageStoryData, + MediaContent, } from '../../types'; import type { UniversalMessage } from './messages'; @@ -38,7 +38,7 @@ import { buildStickerFromDocument } from './symbols'; export function buildMessageContent( mtpMessage: UniversalMessage | GramJs.UpdateServiceNotification, ) { - let content: ApiMessage['content'] = {}; + let content: MediaContent = {}; if (mtpMessage.media) { content = { @@ -69,7 +69,7 @@ export function buildMessageTextContent( }; } -export function buildMessageMediaContent(media: GramJs.TypeMessageMedia): ApiMessage['content'] | undefined { +export function buildMessageMediaContent(media: GramJs.TypeMessageMedia): MediaContent | undefined { if ('ttlSeconds' in media && media.ttlSeconds) { return undefined; } diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index d43c62441..74e6e6794 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -1,11 +1,14 @@ import { Api as GramJs } from '../../../lib/gramjs'; +import type { ApiDraft } from '../../../global/types'; import type { ApiAction, ApiAttachment, ApiChat, ApiContact, ApiGroupCall, + ApiInputMessageReplyInfo, + ApiInputReplyInfo, ApiKeyboardButton, ApiMessage, ApiMessageEntity, @@ -13,14 +16,15 @@ import type { ApiNewPoll, ApiPeer, ApiPhoto, + ApiReplyInfo, ApiReplyKeyboard, ApiSponsoredMessage, ApiSticker, ApiStory, ApiStorySkipped, ApiThreadInfo, - ApiTypeReplyTo, ApiVideo, + MediaContent, PhoneCallAction, } from '../../types'; import { @@ -49,7 +53,7 @@ import { buildApiCallDiscardReason } from './calls'; import { buildApiPhoto, } from './common'; -import { buildMessageContent, buildMessageTextContent } from './messageContent'; +import { buildMessageContent, buildMessageMediaContent, buildMessageTextContent } from './messageContent'; import { buildApiPeerId, getApiChatIdFromMtpPeer, isPeerUser } from './peers'; import { buildMessageReactions } from './reactions'; @@ -171,23 +175,6 @@ export function buildApiMessageWithChatId( const isInvoiceMedia = mtpMessage.media instanceof GramJs.MessageMediaInvoice && Boolean(mtpMessage.media.extendedMedia); - let replyToMsgId: number | undefined; - let replyToTopId: number | undefined; - let replyToStoryUserId: string | undefined; - let replyToStoryId: number | undefined; - let forumTopic: boolean | undefined; - let replyToPeerId: GramJs.TypePeer | undefined; - if (mtpMessage.replyTo instanceof GramJs.MessageReplyHeader) { - replyToMsgId = mtpMessage.replyTo.replyToMsgId; - replyToTopId = mtpMessage.replyTo.replyToTopId; - forumTopic = mtpMessage.replyTo.forumTopic; - replyToPeerId = mtpMessage.replyTo.replyToPeerId; - } - if (mtpMessage.replyTo instanceof GramJs.MessageReplyStoryHeader) { - replyToStoryUserId = buildApiPeerId(mtpMessage.replyTo.userId, 'user'); - replyToStoryId = mtpMessage.replyTo.storyId; - } - const isEdited = mtpMessage.editDate && !mtpMessage.editHide; const { inlineButtons, keyboardButtons, keyboardPlaceholder, isKeyboardSingleUse, isKeyboardSelective, @@ -218,12 +205,8 @@ export function buildApiMessageWithChatId( isPinned: mtpMessage.pinned, reactions: mtpMessage.reactions && buildMessageReactions(mtpMessage.reactions), emojiOnlyCount, - ...(replyToMsgId && { replyToMessageId: replyToMsgId }), - ...(forumTopic && { isTopicReply: true }), - ...(replyToPeerId && { replyToChatId: getApiChatIdFromMtpPeer(replyToPeerId) }), - ...(replyToTopId && { replyToTopMessageId: replyToTopId }), + ...(mtpMessage.replyTo && { replyInfo: buildApiReplyInfo(mtpMessage.replyTo) }), ...(forwardInfo && { forwardInfo }), - ...(replyToStoryUserId && { replyToStoryUserId, replyToStoryId }), ...(isEdited && { isEdited }), ...(mtpMessage.editDate && { editDate: mtpMessage.editDate }), ...(isMediaUnread && { isMediaUnread }), @@ -246,18 +229,26 @@ export function buildApiMessageWithChatId( }; } -export function buildMessageDraft(draft: GramJs.TypeDraftMessage) { +export function buildMessageDraft(draft: GramJs.TypeDraftMessage): ApiDraft | undefined { if (draft instanceof GramJs.DraftMessageEmpty) { return undefined; } const { - message, entities, replyToMsgId, date, + message, entities, replyTo, date, } = draft; + const replyInfo = replyTo instanceof GramJs.InputReplyToMessage ? { + type: 'message', + replyToMsgId: replyTo.replyToMsgId, + replyToTopId: replyTo.topMsgId, + replyToPeerId: replyTo.replyToPeerId && getApiChatIdFromMtpPeer(replyTo.replyToPeerId), + quoteText: replyTo.quoteText ? buildMessageTextContent(replyTo.quoteText, replyTo.quoteEntities) : undefined, + } satisfies ApiInputMessageReplyInfo : undefined; + return { - formattedText: message ? buildMessageTextContent(message, entities) : undefined, - replyingToId: replyToMsgId, + text: message ? buildMessageTextContent(message, entities) : undefined, + replyInfo, date, }; } @@ -280,6 +271,44 @@ function buildApiMessageForwardInfo(fwdFrom: GramJs.MessageFwdHeader, isChatWith }; } +function buildApiReplyInfo(replyHeader: GramJs.TypeMessageReplyHeader): ApiReplyInfo | undefined { + if (replyHeader instanceof GramJs.MessageReplyStoryHeader) { + return { + type: 'story', + userId: replyHeader.userId.toString(), + storyId: replyHeader.storyId, + }; + } + + if (replyHeader instanceof GramJs.MessageReplyHeader) { + const { + replyFrom, + replyToMsgId, + replyToTopId, + replyMedia, + replyToPeerId, + forumTopic, + quote, + quoteText, + quoteEntities, + } = replyHeader; + + return { + type: 'message', + replyToMsgId, + replyToTopId, + isForumTopic: forumTopic, + replyFrom: replyFrom && buildApiMessageForwardInfo(replyFrom), + replyToPeerId: replyToPeerId && getApiChatIdFromMtpPeer(replyToPeerId), + replyMedia: replyMedia && buildMessageMediaContent(replyMedia), + isQuote: quote, + quoteText: quoteText ? buildMessageTextContent(quoteText, quoteEntities) : undefined, + }; + } + + return undefined; +} + function buildAction( action: GramJs.TypeMessageAction, senderId: string | undefined, @@ -682,7 +711,7 @@ export function buildLocalMessage( chat: ApiChat, text?: string, entities?: ApiMessageEntity[], - replyingTo?: ApiTypeReplyTo, + replyInfo?: ApiInputReplyInfo, attachment?: ApiAttachment, sticker?: ApiSticker, gif?: ApiVideo, @@ -696,21 +725,8 @@ export function buildLocalMessage( const localId = getNextLocalMessageId(chat.lastMessage?.id); const media = attachment && buildUploadingMedia(attachment); const isChannel = chat.type === 'chatTypeChannel'; - const isForum = chat.isForum; - let replyToMessageId: number | undefined; - let replyingToTopId: number | undefined; - let replyToStoryUserId: string | undefined; - let replyToStoryId: number | undefined; - if (replyingTo) { - if ('replyingTo' in replyingTo) { - replyToMessageId = replyingTo.replyingTo; - replyingToTopId = replyingTo.replyingToTopId; - } else { - replyToStoryUserId = replyingTo.userId; - replyToStoryId = replyingTo.storyId; - } - } + const resultReplyInfo = replyInfo && buildReplyInfo(replyInfo, chat.isForum); const message = { id: localId, @@ -732,10 +748,7 @@ export function buildLocalMessage( date: scheduledAt || Math.round(Date.now() / 1000) + getServerTimeOffset(), isOutgoing: !isChannel, senderId: sendAs?.id || currentUserId, - ...(replyToMessageId && { replyToMessageId }), - ...(replyingToTopId && { replyToTopMessageId: replyingToTopId }), - ...((replyToMessageId || replyingToTopId) && isForum && { isTopicReply: true }), - ...(replyToStoryUserId && { replyToStoryUserId, replyToStoryId }), + replyInfo: resultReplyInfo, ...(groupedId && { groupedId, ...(media && (media.photo || media.video) && { isInAlbum: true }), @@ -796,6 +809,12 @@ export function buildLocalForwardedMessage({ text: !shouldHideText ? strippedText : undefined, }; + const replyInfo: ApiReplyInfo | undefined = toThreadId ? { + type: 'message', + replyToTopId: toThreadId, + isForumTopic: toChat.isForum || undefined, + } : undefined; + return { id: localId, chatId: toChat.id, @@ -807,7 +826,7 @@ export function buildLocalForwardedMessage({ groupedId, isInAlbum, isForwardingAllowed: true, - replyToTopMessageId: toThreadId, + replyInfo, ...(toThreadId && toChat?.isForum && { isTopicReply: true }), ...(emojiOnlyCount && { emojiOnlyCount }), @@ -826,9 +845,28 @@ export function buildLocalForwardedMessage({ }; } +function buildReplyInfo(inputInfo: ApiInputReplyInfo, isForum?: boolean): ApiReplyInfo { + if (inputInfo.type === 'story') { + return { + type: 'story', + userId: inputInfo.userId, + storyId: inputInfo.storyId, + }; + } + + return { + type: 'message', + replyToMsgId: inputInfo.replyToMsgId, + replyToTopId: inputInfo.replyToTopId, + replyToPeerId: inputInfo.replyToPeerId, + quoteText: inputInfo.quoteText, + isForumTopic: isForum && inputInfo.replyToTopId ? true : undefined, + }; +} + function buildUploadingMedia( attachment: ApiAttachment, -): ApiMessage['content'] { +): MediaContent { const { filename: fileName, blobUrl, diff --git a/src/api/gramjs/apiBuilders/stories.ts b/src/api/gramjs/apiBuilders/stories.ts index f5da16fdd..6086b2580 100644 --- a/src/api/gramjs/apiBuilders/stories.ts +++ b/src/api/gramjs/apiBuilders/stories.ts @@ -1,18 +1,17 @@ -import { Api as GramJs, errors } from '../../../lib/gramjs'; +import { Api as GramJs } from '../../../lib/gramjs'; import type { - ApiApplyBoostInfo, ApiBoostsStatus, ApiMediaArea, ApiMediaAreaCoordinates, - ApiMessage, + ApiMyBoost, ApiStealthMode, ApiStoryView, ApiTypeStory, + MediaContent, } from '../../types'; import { buildCollectionByCallback } from '../../../util/iteratees'; -import { getServerTime } from '../../../util/serverTime'; import { buildPrivacyRules } from './common'; import { buildGeoPoint, buildMessageMediaContent, buildMessageTextContent } from './messageContent'; import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers'; @@ -49,7 +48,7 @@ export function buildApiStory(peerId: string, story: GramJs.TypeStoryItem): ApiT mediaAreas, sentReaction, out, } = story; - const content: ApiMessage['content'] = { + const content: MediaContent = { ...buildMessageMediaContent(media), }; @@ -171,45 +170,7 @@ export function buildApiPeerStories(peerStories: GramJs.PeerStories) { return buildCollectionByCallback(peerStories.stories, (story) => [story.id, buildApiStory(peerId, story)]); } -export function buildApiApplyBoostInfo( - applyBoostInfo: GramJs.stories.TypeCanApplyBoostResult, -): ApiApplyBoostInfo | undefined { - if (applyBoostInfo instanceof GramJs.stories.CanApplyBoostOk) { - return { type: 'ok' }; - } - - if (applyBoostInfo instanceof GramJs.stories.CanApplyBoostReplace) { - return { - type: 'replace', - boostedChatId: getApiChatIdFromMtpPeer(applyBoostInfo.currentBoost), - }; - } - - return undefined; -} - -export function buildApiApplyBoostInfoFromError( - error: unknown, -): ApiApplyBoostInfo | undefined { - if (error instanceof errors.FloodWaitError) { - return { - type: 'wait', - waitUntil: getServerTime() + error.seconds, - }; - } - - if (error instanceof Error) { - if (error.message === 'BOOST_NOT_MODIFIED') { - return { - type: 'already', - }; - } - } - - return undefined; -} - -export function buildApiBoostsStatus(boostStatus: GramJs.stories.BoostsStatus): ApiBoostsStatus { +export function buildApiBoostsStatus(boostStatus: GramJs.premium.BoostsStatus): ApiBoostsStatus { const { level, boostUrl, boosts, myBoost, currentLevelBoosts, nextLevelBoosts, premiumAudience, } = boostStatus; @@ -223,3 +184,17 @@ export function buildApiBoostsStatus(boostStatus: GramJs.stories.BoostsStatus): ...(premiumAudience && { premiumSubscribers: buildStatisticsPercentage(premiumAudience) }), }; } + +export function buildApiMyBoost(myBoost: GramJs.MyBoost): ApiMyBoost { + const { + date, expires, slot, cooldownUntilDate, peer, + } = myBoost; + + return { + date, + expires, + slot, + cooldownUntil: cooldownUntilDate, + chatId: peer && getApiChatIdFromMtpPeer(peer), + }; +} diff --git a/src/api/gramjs/apiBuilders/users.ts b/src/api/gramjs/apiBuilders/users.ts index b217d11d0..aef9745f6 100644 --- a/src/api/gramjs/apiBuilders/users.ts +++ b/src/api/gramjs/apiBuilders/users.ts @@ -85,6 +85,8 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined { hasStories: Boolean(storiesMaxId) && !storiesUnavailable, ...(mtpUser.bot && mtpUser.botInlinePlaceholder && { botPlaceholder: mtpUser.botInlinePlaceholder }), ...(mtpUser.bot && mtpUser.botAttachMenu && { isAttachBot: mtpUser.botAttachMenu }), + color: mtpUser.color, + backgroundEmojiId: mtpUser.backgroundEmojiId?.toString(), }; } diff --git a/src/api/gramjs/gramjsBuilders/index.ts b/src/api/gramjs/gramjsBuilders/index.ts index 37f4be045..9a2450e50 100644 --- a/src/api/gramjs/gramjsBuilders/index.ts +++ b/src/api/gramjs/gramjsBuilders/index.ts @@ -11,6 +11,7 @@ import type { ApiChatReactions, ApiFormattedText, ApiGroupCall, + ApiInputReplyInfo, ApiMessageEntity, ApiNewPoll, ApiPhoneCall, @@ -24,7 +25,6 @@ import type { ApiStory, ApiStorySkipped, ApiThemeParameters, - ApiTypeReplyTo, ApiVideo, } from '../../types'; import { @@ -643,24 +643,28 @@ export function buildInputBotApp(app: ApiBotApp) { }); } -export function buildInputReplyToMessage(replyToMsgId: number, topMsgId?: number) { - return new GramJs.InputReplyToMessage({ - replyToMsgId, - topMsgId, - }); -} +export function buildInputReplyTo(replyInfo: ApiInputReplyInfo) { + if (replyInfo.type === 'story') { + return new GramJs.InputReplyToStory({ + userId: buildInputPeerFromLocalDb(replyInfo.userId)!, + storyId: replyInfo.storyId, + }); + } -export function buildInputReplyToStory(userId: string, storyId: number) { - return new GramJs.InputReplyToStory({ - userId: buildInputPeerFromLocalDb(userId)!, - storyId, - }); -} + if (replyInfo.type === 'message') { + const { + replyToMsgId, replyToTopId, replyToPeerId, quoteText, + } = replyInfo; + return new GramJs.InputReplyToMessage({ + replyToMsgId, + topMsgId: replyToTopId, + replyToPeerId: replyToPeerId ? buildInputPeerFromLocalDb(replyToPeerId)! : undefined, + quoteText: quoteText?.text, + quoteEntities: quoteText?.entities?.map(buildMtpMessageEntity), + }); + } -export function buildInputReplyTo(replyingTo: ApiTypeReplyTo) { - return 'replyingTo' in replyingTo - ? buildInputReplyToMessage(replyingTo.replyingTo, replyingTo.replyingToTopId) - : buildInputReplyToStory(replyingTo.userId, replyingTo.storyId); + return undefined; } export function buildInputPrivacyRules( diff --git a/src/api/gramjs/helpers.ts b/src/api/gramjs/helpers.ts index eadce2c12..a1a1b931f 100644 --- a/src/api/gramjs/helpers.ts +++ b/src/api/gramjs/helpers.ts @@ -50,29 +50,10 @@ export function addMessageToLocalDb(message: GramJs.Message | GramJs.MessageServ localDb.messages[messageFullId] = mockMessage; if (mockMessage instanceof GramJs.Message) { - if (mockMessage.media instanceof GramJs.MessageMediaDocument - && mockMessage.media.document instanceof GramJs.Document - ) { - localDb.documents[String(mockMessage.media.document.id)] = mockMessage.media.document; - } + if (mockMessage.media) addMediaToLocalDb(mockMessage.media); - if (mockMessage.media instanceof GramJs.MessageMediaWebPage - && mockMessage.media.webpage instanceof GramJs.WebPage - && mockMessage.media.webpage.document instanceof GramJs.Document - ) { - localDb.documents[String(mockMessage.media.webpage.document.id)] = mockMessage.media.webpage.document; - } - - if (mockMessage.media instanceof GramJs.MessageMediaGame) { - if (mockMessage.media.game.document instanceof GramJs.Document) { - localDb.documents[String(mockMessage.media.game.document.id)] = mockMessage.media.game.document; - } - addPhotoToLocalDb(mockMessage.media.game.photo); - } - - if (mockMessage.media instanceof GramJs.MessageMediaInvoice - && mockMessage.media.photo) { - localDb.webDocuments[String(mockMessage.media.photo.url)] = mockMessage.media.photo; + if (mockMessage.replyTo instanceof GramJs.MessageReplyHeader && mockMessage.replyTo.replyMedia) { + addMediaToLocalDb(mockMessage.replyTo.replyMedia); } } @@ -81,6 +62,33 @@ export function addMessageToLocalDb(message: GramJs.Message | GramJs.MessageServ } } +function addMediaToLocalDb(media: GramJs.TypeMessageMedia) { + if (media instanceof GramJs.MessageMediaDocument + && media.document instanceof GramJs.Document + ) { + localDb.documents[String(media.document.id)] = media.document; + } + + if (media instanceof GramJs.MessageMediaWebPage + && media.webpage instanceof GramJs.WebPage + && media.webpage.document instanceof GramJs.Document + ) { + localDb.documents[String(media.webpage.document.id)] = media.webpage.document; + } + + if (media instanceof GramJs.MessageMediaGame) { + if (media.game.document instanceof GramJs.Document) { + localDb.documents[String(media.game.document.id)] = media.game.document; + } + addPhotoToLocalDb(media.game.photo); + } + + if (media instanceof GramJs.MessageMediaInvoice + && media.photo) { + localDb.webDocuments[String(media.photo.url)] = media.photo; + } +} + export function addStoryToLocalDb(story: GramJs.TypeStoryItem, peerId: string) { if (!(story instanceof GramJs.StoryItem)) { return; diff --git a/src/api/gramjs/methods/bots.ts b/src/api/gramjs/methods/bots.ts index 1f32baaa8..e51b54e8c 100644 --- a/src/api/gramjs/methods/bots.ts +++ b/src/api/gramjs/methods/bots.ts @@ -3,7 +3,7 @@ import { Api as GramJs } from '../../../lib/gramjs'; import type { ApiBotApp, - ApiChat, ApiPeer, ApiThemeParameters, ApiUser, OnApiUpdate, + ApiChat, ApiInputMessageReplyInfo, ApiPeer, ApiThemeParameters, ApiUser, OnApiUpdate, } from '../../types'; import { WEB_APP_PLATFORM } from '../../../config'; @@ -24,7 +24,7 @@ import { buildInputBotApp, buildInputEntity, buildInputPeer, - buildInputReplyToMessage, + buildInputReplyTo, buildInputThemeParams, generateRandomBigInt, } from '../gramjsBuilders'; @@ -124,13 +124,12 @@ export async function fetchInlineBotResults({ } export async function sendInlineBotResult({ - chat, replyingToTopId, resultId, queryId, replyingTo, sendAs, isSilent, scheduleDate, + chat, replyInfo, resultId, queryId, sendAs, isSilent, scheduleDate, }: { chat: ApiChat; - replyingToTopId?: number; + replyInfo?: ApiInputMessageReplyInfo; resultId: string; queryId: string; - replyingTo?: number; sendAs?: ApiPeer; isSilent?: boolean; scheduleDate?: number; @@ -144,9 +143,8 @@ export async function sendInlineBotResult({ peer: buildInputPeer(chat.id, chat.accessHash), id: resultId, scheduleDate, - ...(replyingToTopId && { topMsgId: replyingToTopId }), + replyTo: replyInfo && buildInputReplyTo(replyInfo), ...(isSilent && { silent: true }), - ...(replyingTo && { replyToMsgId: replyingTo }), ...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }), })); } @@ -173,8 +171,7 @@ export async function requestWebView({ bot, url, startParam, - replyToMessageId, - threadId, + replyInfo, theme, sendAs, isFromBotMenu, @@ -184,8 +181,7 @@ export async function requestWebView({ bot: ApiUser; url?: string; startParam?: string; - replyToMessageId?: number; - threadId?: number; + replyInfo?: ApiInputMessageReplyInfo; theme?: ApiThemeParameters; sendAs?: ApiPeer; isFromBotMenu?: boolean; @@ -199,7 +195,7 @@ export async function requestWebView({ themeParams: theme ? buildInputThemeParams(theme) : undefined, fromBotMenu: isFromBotMenu || undefined, platform: WEB_APP_PLATFORM, - ...(replyToMessageId && { replyTo: buildInputReplyToMessage(replyToMessageId, threadId) }), + replyTo: replyInfo && buildInputReplyTo(replyInfo), ...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }), })); @@ -292,16 +288,14 @@ export function prolongWebView({ peer, bot, queryId, - replyToMessageId, - threadId, + replyInfo, sendAs, }: { isSilent?: boolean; peer: ApiPeer; bot: ApiUser; queryId: string; - replyToMessageId?: number; - threadId?: number; + replyInfo?: ApiInputMessageReplyInfo; sendAs?: ApiPeer; }) { return invokeRequest(new GramJs.messages.ProlongWebView({ @@ -309,7 +303,7 @@ export function prolongWebView({ peer: buildInputPeer(peer.id, peer.accessHash), bot: buildInputPeer(bot.id, bot.accessHash), queryId: BigInt(queryId), - ...(replyToMessageId && { replyTo: buildInputReplyToMessage(replyToMessageId, threadId) }), + replyTo: replyInfo && buildInputReplyTo(replyInfo), ...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }), })); } diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index 028fb5d2e..7e9d0f323 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -1,6 +1,7 @@ import BigInt from 'big-integer'; import { Api as GramJs } from '../../../lib/gramjs'; +import type { ApiDraft } from '../../../global/types'; import type { ApiChat, ApiChatAdminRights, @@ -8,10 +9,8 @@ import type { ApiChatFolder, ApiChatFullInfo, ApiChatReactions, - ApiFormattedText, ApiGroupCall, ApiMessage, - ApiMessageEntity, ApiPeer, ApiPhoto, ApiTopic, @@ -58,6 +57,7 @@ import { buildInputEntity, buildInputPeer, buildInputPhoto, + buildInputReplyTo, buildMtpMessageEntity, generateRandomBigInt, isMessageWithMedia, @@ -135,7 +135,7 @@ export async function fetchChats({ } const chats: ApiChat[] = []; - const draftsById: Record = {}; + const draftsById: Record = {}; const replyingToById: Record = {}; const dialogs = (resultPinned ? resultPinned.dialogs : []).concat(result.dialogs); @@ -180,12 +180,9 @@ export async function fetchChats({ } if (dialog.draft) { - const { formattedText, replyingToId } = buildMessageDraft(dialog.draft) || {}; - if (formattedText) { - draftsById[chat.id] = formattedText; - } - if (replyingToId) { - replyingToById[chat.id] = replyingToId; + const draft = buildMessageDraft(dialog.draft); + if (draft) { + draftsById[chat.id] = draft; } } }); @@ -364,33 +361,16 @@ export async function requestChatUpdate({ export function saveDraft({ chat, - text, - entities, - threadId, - replyToMsgId, + draft, }: { chat: ApiChat; - text: string; - entities?: ApiMessageEntity[]; - threadId?: number; - replyToMsgId?: number; + draft?: ApiDraft; }) { return invokeRequest(new GramJs.messages.SaveDraft({ peer: buildInputPeer(chat.id, chat.accessHash), - message: text, - ...(entities && { - entities: entities.map(buildMtpMessageEntity), - }), - replyToMsgId, - topMsgId: threadId, - })); -} - -export function clearDraft(chat: ApiChat, threadId?: number) { - return invokeRequest(new GramJs.messages.SaveDraft({ - peer: buildInputPeer(chat.id, chat.accessHash), - message: '', - ...(threadId && { topMsgId: threadId }), + message: draft?.text?.text || '', + entities: draft?.text?.entities?.map(buildMtpMessageEntity), + replyTo: draft?.replyInfo && buildInputReplyTo(draft.replyInfo), })); } diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index 987855696..6ef5e32f8 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -13,7 +13,7 @@ export { export { fetchChats, fetchFullChat, searchChats, requestChatUpdate, fetchChatSettings, - saveDraft, clearDraft, fetchChat, updateChatMutedState, updateTopicMutedState, + saveDraft, fetchChat, updateChatMutedState, updateTopicMutedState, createChannel, joinChannel, deleteChatUser, deleteChat, leaveChannel, deleteChannel, createGroupChat, editChatPhoto, toggleChatPinned, toggleChatArchived, toggleDialogUnread, setChatEnabledReactions, fetchChatFolders, editChatFolder, deleteChatFolder, sortChatFolders, fetchRecommendedChatFolders, diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index ca3f92692..cbb9312f2 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -6,6 +6,7 @@ import type { ApiContact, ApiFormattedText, ApiGlobalMessageSearchType, + ApiInputReplyInfo, ApiMessage, ApiMessageEntity, ApiMessageSearchType, @@ -18,8 +19,8 @@ import type { ApiSticker, ApiStory, ApiStorySkipped, - ApiTypeReplyTo, ApiVideo, + MediaContent, OnApiUpdate, } from '../../types'; import { @@ -238,7 +239,7 @@ export function sendMessage( chat, text, entities, - replyingTo, + replyInfo, attachment, sticker, story, @@ -256,7 +257,7 @@ export function sendMessage( lastMessageId?: number; text?: string; entities?: ApiMessageEntity[]; - replyingTo?: ApiTypeReplyTo; + replyInfo?: ApiInputReplyInfo; attachment?: ApiAttachment; sticker?: ApiSticker; story?: ApiStory | ApiStorySkipped; @@ -276,7 +277,7 @@ export function sendMessage( chat, text, entities, - replyingTo, + replyInfo, attachment, sticker, gif, @@ -315,7 +316,7 @@ export function sendMessage( chat, text, entities, - replyingTo, + replyInfo, attachment: attachment!, groupedId, isSilent, @@ -356,7 +357,6 @@ export function sendMessage( } const RequestClass = media ? GramJs.messages.SendMedia : GramJs.messages.SendMessage; - const replyTo = replyingTo ? buildInputReplyTo(replyingTo) : undefined; try { const update = await invokeRequest(new RequestClass({ @@ -365,9 +365,9 @@ export function sendMessage( entities: entities ? entities.map(buildMtpMessageEntity) : undefined, peer: buildInputPeer(chat.id, chat.accessHash), randomId, + replyTo: replyInfo && buildInputReplyTo(replyInfo), ...(isSilent && { silent: isSilent }), ...(scheduledAt && { scheduleDate: scheduledAt }), - ...(replyTo && { replyTo }), ...(media && { media }), ...(noWebPage && { noWebpage: noWebPage }), ...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }), @@ -402,7 +402,7 @@ function sendGroupedMedia( chat, text, entities, - replyingTo, + replyInfo, attachment, groupedId, isSilent, @@ -412,7 +412,7 @@ function sendGroupedMedia( chat: ApiChat; text?: string; entities?: ApiMessageEntity[]; - replyingTo?: ApiTypeReplyTo; + replyInfo?: ApiInputReplyInfo; attachment: ApiAttachment; groupedId: string; isSilent?: boolean; @@ -484,13 +484,12 @@ function sendGroupedMedia( const { singleMediaByIndex, localMessages } = groupedUploads[groupedId]; delete groupedUploads[groupedId]; - const replyTo = replyingTo ? buildInputReplyTo(replyingTo) : undefined; const update = await invokeRequest(new GramJs.messages.SendMultiMedia({ clearDraft: true, peer: buildInputPeer(chat.id, chat.accessHash), multiMedia: Object.values(singleMediaByIndex), // Object keys are usually ordered - ...(replyingTo && { replyTo }), + replyTo: replyInfo && buildInputReplyTo(replyInfo), ...(isSilent && { silent: isSilent }), ...(scheduledAt && { scheduleDate: scheduledAt }), ...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }), @@ -1738,7 +1737,7 @@ function handleLocalMessageUpdate(localMessage: ApiMessage, update: GramJs.TypeU return; } - let newContent: ApiMessage['content'] | undefined; + let newContent: MediaContent | undefined; if (messageUpdate instanceof GramJs.UpdateShortSentMessage) { if (localMessage.content.text && messageUpdate.entities) { newContent = { diff --git a/src/api/gramjs/methods/stories.ts b/src/api/gramjs/methods/stories.ts index d6824985b..5a24a229d 100644 --- a/src/api/gramjs/methods/stories.ts +++ b/src/api/gramjs/methods/stories.ts @@ -17,9 +17,8 @@ import { buildCollectionByCallback } from '../../../util/iteratees'; import { buildApiChatFromPreview } from '../apiBuilders/chats'; import { getApiChatIdFromMtpPeer } from '../apiBuilders/peers'; import { - buildApiApplyBoostInfo, - buildApiApplyBoostInfoFromError, buildApiBoostsStatus, + buildApiMyBoost, buildApiPeerStories, buildApiStealthMode, buildApiStory, @@ -430,50 +429,35 @@ export function activateStealthMode({ }); } -export async function fetchCanApplyBoost({ - chat, -} : { - chat: ApiChat; -}) { - let result: GramJs.stories.TypeCanApplyBoostResult | undefined; - try { - result = await invokeRequest(new GramJs.stories.CanApplyBoost({ - peer: buildInputPeer(chat.id, chat.accessHash), - }), { - shouldThrow: true, - }); - } catch (error) { - const info = buildApiApplyBoostInfoFromError(error); - if (!info) return undefined; - return { - info, - chats: [], - }; - } +export async function fetchMyBoosts() { + const result = await invokeRequest(new GramJs.premium.GetMyBoosts()); - if (!result) { - return undefined; - } + if (!result) return undefined; - const mtpChats = 'chats' in result ? result.chats : []; - addEntitiesToLocalDb(mtpChats); + addEntitiesToLocalDb(result.users); + addEntitiesToLocalDb(result.chats); - const chats = mtpChats.map((c) => buildApiChatFromPreview(c)).filter(Boolean); - const info = buildApiApplyBoostInfo(result); + const users = result.users.map(buildApiUser).filter(Boolean); + const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean); + const boosts = result.myBoosts.map(buildApiMyBoost); return { - info, + users, chats, + boosts, }; } export function applyBoost({ chat, + slots, } : { chat: ApiChat; + slots: number[]; }) { - return invokeRequest(new GramJs.stories.ApplyBoost({ + return invokeRequest(new GramJs.premium.ApplyBoost({ peer: buildInputPeer(chat.id, chat.accessHash), + slots, }), { shouldReturnTrue: true, }); @@ -484,7 +468,7 @@ export async function fetchBoostsStatus({ }: { chat: ApiChat; }) { - const result = await invokeRequest(new GramJs.stories.GetBoostsStatus({ + const result = await invokeRequest(new GramJs.premium.GetBoostsStatus({ peer: buildInputPeer(chat.id, chat.accessHash), })); @@ -504,7 +488,7 @@ export async function fetchBoostersList({ offset?: string; limit?: number; }) { - const result = await invokeRequest(new GramJs.stories.GetBoostersList({ + const result = await invokeRequest(new GramJs.premium.GetBoostsList({ peer: buildInputPeer(chat.id, chat.accessHash), offset, limit, @@ -518,9 +502,10 @@ export async function fetchBoostersList({ const users = result.users.map(buildApiUser).filter(Boolean); - const boosterIds = result.boosters.map((booster) => booster.userId.toString()); - const boosters = buildCollectionByCallback(result.boosters, (booster) => ( - [booster.userId.toString(), booster.expires] + const userBoosts = result.boosts.filter((boost) => boost.userId); + const boosterIds = userBoosts.map((boost) => boost.userId!.toString()); + const boosters = buildCollectionByCallback(userBoosts, (boost) => ( + [boost.userId!.toString(), boost.expires] )); return { diff --git a/src/api/gramjs/updater.ts b/src/api/gramjs/updater.ts index 33b3a61b1..89a207aec 100644 --- a/src/api/gramjs/updater.ts +++ b/src/api/gramjs/updater.ts @@ -3,7 +3,7 @@ import { Api as GramJs, connection } from '../../lib/gramjs'; import type { GroupCallConnectionData } from '../../lib/secret-sauce'; import type { ApiMessage, ApiMessageExtendedMediaPreview, ApiStory, ApiStorySkipped, - ApiUpdate, ApiUpdateConnectionStateType, OnApiUpdate, + ApiUpdate, ApiUpdateConnectionStateType, MediaContent, OnApiUpdate, } from '../types'; import { DEBUG, GENERAL_TOPIC_ID } from '../../config'; @@ -363,7 +363,7 @@ export function updater(update: Update) { reactions: buildMessageReactions(update.reactions), }); } else if (update instanceof GramJs.UpdateMessageExtendedMedia) { - let media: ApiMessage['content'] | undefined; + let media: MediaContent | undefined; if (update.extendedMedia instanceof GramJs.MessageExtendedMedia) { media = buildMessageMediaContent(update.extendedMedia.media); } @@ -902,7 +902,7 @@ export function updater(update: Update) { '@type': 'draftMessage', chatId: getApiChatIdFromMtpPeer(update.peer), threadId: update.topMsgId, - ...buildMessageDraft(update.draft), + draft: buildMessageDraft(update.draft), }); } else if (update instanceof GramJs.UpdateContactsReset) { onUpdate({ '@type': 'updateResetContactList' }); diff --git a/src/api/types/chats.ts b/src/api/types/chats.ts index 1f9e437f1..9c3dca610 100644 --- a/src/api/types/chats.ts +++ b/src/api/types/chats.ts @@ -36,12 +36,14 @@ export interface ApiChat { avatarHash?: string; usernames?: ApiUsername[]; membersCount?: number; - joinDate?: number; + creationDate?: number; isSupport?: true; photos?: ApiPhoto[]; draftDate?: number; isProtected?: boolean; fakeType?: ApiFakeType; + color?: number; + backgroundEmojiId?: string; isForum?: boolean; topics?: Record; listedTopicIds?: number[]; diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index 90cfb8771..972b436ce 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -298,18 +298,42 @@ export interface ApiWebPage { story?: ApiWebPageStoryData; } -export type ApiTypeReplyTo = ApiMessageReplyTo | ApiStoryReplyTo; +export type ApiReplyInfo = ApiMessageReplyInfo | ApiStoryReplyInfo; -export interface ApiMessageReplyTo { - replyingTo: number; - replyingToTopId?: number; +export interface ApiMessageReplyInfo { + type: 'message'; + replyToMsgId?: number; + replyToPeerId?: string; + replyFrom?: ApiMessageForwardInfo; + replyMedia?: MediaContent; + replyToTopId?: number; + isForumTopic?: true; + isQuote?: true; + quoteText?: ApiFormattedText; } -export interface ApiStoryReplyTo { +export interface ApiStoryReplyInfo { + type: 'story'; userId: string; storyId: number; } +export interface ApiInputMessageReplyInfo { + type: 'message'; + replyToMsgId: number; + replyToTopId?: number; + replyToPeerId?: string; + quoteText?: ApiFormattedText; +} + +export interface ApiInputStoryReplyInfo { + type: 'story'; + userId: string; + storyId: number; +} + +export type ApiInputReplyInfo = ApiInputMessageReplyInfo | ApiInputStoryReplyInfo; + export interface ApiMessageForwardInfo { date: number; isImported?: boolean; @@ -391,36 +415,33 @@ export interface ApiFormattedText { entities?: ApiMessageEntity[]; } +export type MediaContent = { + text?: ApiFormattedText; + photo?: ApiPhoto; + video?: ApiVideo; + altVideo?: ApiVideo; + document?: ApiDocument; + sticker?: ApiSticker; + contact?: ApiContact; + poll?: ApiPoll; + action?: ApiAction; + webPage?: ApiWebPage; + audio?: ApiAudio; + voice?: ApiVoice; + invoice?: ApiInvoice; + location?: ApiLocation; + game?: ApiGame; + storyData?: ApiMessageStoryData; +}; + export interface ApiMessage { id: number; chatId: string; - content: { - text?: ApiFormattedText; - photo?: ApiPhoto; - video?: ApiVideo; - altVideo?: ApiVideo; - document?: ApiDocument; - sticker?: ApiSticker; - contact?: ApiContact; - poll?: ApiPoll; - action?: ApiAction; - webPage?: ApiWebPage; - audio?: ApiAudio; - voice?: ApiVoice; - invoice?: ApiInvoice; - location?: ApiLocation; - game?: ApiGame; - storyData?: ApiMessageStoryData; - }; + content: MediaContent; date: number; isOutgoing: boolean; senderId?: string; - replyToChatId?: string; - replyToMessageId?: number; - replyToTopMessageId?: number; - isTopicReply?: true; - replyToStoryUserId?: string; - replyToStoryId?: number; + replyInfo?: ApiReplyInfo; sendingState?: 'messageSendingStatePending' | 'messageSendingStateFailed'; forwardInfo?: ApiMessageForwardInfo; isDeleting?: boolean; @@ -657,6 +678,12 @@ export type ApiThemeParameters = { button_color: string; button_text_color: string; secondary_bg_color: string; + header_bg_color: string; + accent_text_color: string; + section_bg_color: string; + section_header_text_color: string; + subtitle_text_color: string; + destructive_text_color: string; }; export type ApiBotApp = { diff --git a/src/api/types/misc.ts b/src/api/types/misc.ts index 6b0301cfb..f28f97001 100644 --- a/src/api/types/misc.ts +++ b/src/api/types/misc.ts @@ -195,6 +195,8 @@ export interface ApiAppConfig { storyExpirePeriod: number; storyViewersExpirePeriod: number; storyChangelogUserId: string; + peerColors: Record; + darkPeerColors: Record; } export interface ApiConfig { diff --git a/src/api/types/stories.ts b/src/api/types/stories.ts index c492b1ca2..df76110fd 100644 --- a/src/api/types/stories.ts +++ b/src/api/types/stories.ts @@ -1,6 +1,6 @@ import type { ApiPrivacySettings } from '../../types'; import type { - ApiGeoPoint, ApiMessage, ApiReaction, ApiReactionCount, + ApiGeoPoint, ApiReaction, ApiReactionCount, MediaContent, } from './messages'; import type { StatisticsOverviewPercentage } from './statistics'; @@ -10,7 +10,7 @@ export interface ApiStory { peerId: string; date: number; expireDate: number; - content: ApiMessage['content']; + content: MediaContent; isPinned?: boolean; isEdited?: boolean; isForCloseFriends?: boolean; @@ -110,26 +110,6 @@ export type ApiMediaAreaSuggestedReaction = { export type ApiMediaArea = ApiMediaAreaVenue | ApiMediaAreaGeoPoint | ApiMediaAreaSuggestedReaction; -export type ApiApplyBoostOk = { - type: 'ok'; -}; - -export type ApiApplyBoostReplace = { - type: 'replace'; - boostedChatId: string; -}; - -export type ApiApplyBoostWait = { - type: 'wait'; - waitUntil: number; -}; - -export type ApiApplyBoostAlready = { - type: 'already'; -}; - -export type ApiApplyBoostInfo = ApiApplyBoostOk | ApiApplyBoostReplace | ApiApplyBoostWait | ApiApplyBoostAlready; - export type ApiBoostsStatus = { level: number; currentLevelBoosts: number; @@ -139,3 +119,11 @@ export type ApiBoostsStatus = { boostUrl: string; premiumSubscribers?: StatisticsOverviewPercentage; }; + +export type ApiMyBoost = { + slot: number; + chatId?: string; + date: number; + expires: number; + cooldownUntil?: number; +}; diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index 21637ec6f..4d2e23962 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -1,3 +1,4 @@ +import type { ApiDraft } from '../../global/types'; import type { GroupCallConnectionData, GroupCallConnectionState, @@ -27,6 +28,7 @@ import type { ApiReactions, ApiStickerSet, ApiThreadInfo, + MediaContent, } from './messages'; import type { ApiEmojiInteraction, ApiError, ApiInviteInfo, ApiNotifyException, ApiSessionData, @@ -312,9 +314,7 @@ export type ApiUpdateDraftMessage = { '@type': 'draftMessage'; chatId: string; threadId?: number; - formattedText?: ApiFormattedText; - date?: number; - replyingToId?: number; + draft?: ApiDraft; }; export type ApiUpdateMessageReactions = { @@ -328,7 +328,7 @@ export type ApiUpdateMessageExtendedMedia = { '@type': 'updateMessageExtendedMedia'; id: number; chatId: string; - media?: ApiMessage['content']; + media?: MediaContent; preview?: ApiMessageExtendedMediaPreview; }; diff --git a/src/api/types/users.ts b/src/api/types/users.ts index 3ba7ee718..8c91e7284 100644 --- a/src/api/types/users.ts +++ b/src/api/types/users.ts @@ -35,6 +35,8 @@ export interface ApiUser { hasStories?: boolean; hasUnreadStories?: boolean; maxStoryId?: number; + color?: number; + backgroundEmojiId?: string; } export interface ApiUserFullInfo { @@ -81,7 +83,6 @@ type ApiAttachBotForMenu = { type ApiAttachBotBase = { id: string; - hasSettings?: boolean; shouldRequestWriteAccess?: boolean; shortName: string; isForSideMenu?: true; diff --git a/src/assets/font-icons/quote.svg b/src/assets/font-icons/quote.svg new file mode 100644 index 000000000..23fc5c89e --- /dev/null +++ b/src/assets/font-icons/quote.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/components/calls/group/GroupCall.module.scss b/src/components/calls/group/GroupCall.module.scss index c7659d90b..3a704964c 100644 --- a/src/components/calls/group/GroupCall.module.scss +++ b/src/components/calls/group/GroupCall.module.scss @@ -1,4 +1,4 @@ -@import "../../../styles/mixins"; +@use "../../../styles/mixins"; .root { --group-call-panel-color: #212121; @@ -74,7 +74,7 @@ border-bottom: 0.0625rem solid transparent; padding: 0.375rem 0.875rem; - @include adapt-padding-to-scrollbar(0.875rem); + @include mixins.adapt-padding-to-scrollbar(0.875rem); user-select: none; z-index: 1; @@ -139,7 +139,7 @@ .participants { position: relative; margin: 0.125rem 0.5rem 0; - @include adapt-margin-to-scrollbar(0.5rem); + @include mixins.adapt-margin-to-scrollbar(0.5rem); } .participantVideos { diff --git a/src/components/calls/group/GroupCallParticipantMenu.scss b/src/components/calls/group/GroupCallParticipantMenu.scss index ac5bc5713..a479e4c81 100644 --- a/src/components/calls/group/GroupCallParticipantMenu.scss +++ b/src/components/calls/group/GroupCallParticipantMenu.scss @@ -1,4 +1,4 @@ -@import '../../../styles/mixins'; +@use '../../../styles/mixins'; .participant-menu { --color-text: white; @@ -84,7 +84,7 @@ transition: 0.25s ease-in-out background-color, 0.25s ease-in-out box-shadow; } - @include reset-range(); + @include mixins.reset-range(); // Apply custom styles input[type="range"] { diff --git a/src/components/common/Avatar.tsx b/src/components/common/Avatar.tsx index a7321191c..3e5a8ec7f 100644 --- a/src/components/common/Avatar.tsx +++ b/src/components/common/Avatar.tsx @@ -14,7 +14,6 @@ import { IS_TEST } from '../../config'; import { getChatAvatarHash, getChatTitle, - getPeerColorKey, getPeerStoryHtmlId, getUserFullName, isChatWithRepliesBot, @@ -23,6 +22,7 @@ import { } from '../../global/helpers'; import buildClassName, { createClassNameBuilder } from '../../util/buildClassName'; import { getFirstLetters } from '../../util/textFormat'; +import { getPeerColorClass } from './helpers/peerColor'; import renderText from './helpers/renderText'; import { useFastClick } from '../../hooks/useFastClick'; @@ -210,7 +210,7 @@ const Avatar: FC = ({ const fullClassName = buildClassName( `Avatar size-${size}`, className, - `color-bg-${getPeerColorKey(peer)}`, + getPeerColorClass(peer), isSavedMessages && 'saved-messages', isDeleted && 'deleted-account', isReplies && 'replies-bot-account', diff --git a/src/components/common/Composer.tsx b/src/components/common/Composer.tsx index a7baeeef4..b85cad9ba 100644 --- a/src/components/common/Composer.tsx +++ b/src/components/common/Composer.tsx @@ -70,7 +70,6 @@ import { selectIsRightColumnShown, selectNewestMessageWithBotKeyboardButtons, selectPeerStory, - selectReplyingToId, selectRequestedDraftFiles, selectRequestedDraftText, selectScheduledIds, @@ -188,7 +187,6 @@ type StateProps = isChatWithBot?: boolean; isChatWithSelf?: boolean; isChannel?: boolean; - replyingToId?: number; isForCurrentMessageList: boolean; isRightColumnShown?: boolean; isSelectModeActive?: boolean; @@ -318,7 +316,6 @@ const Composer: FC = ({ sendAsChat, sendAsId, editingDraft, - replyingToId, requestedDraftText, requestedDraftFiles, botMenuButton, @@ -695,7 +692,6 @@ const Composer: FC = ({ messageListType, draft, editingDraft, - replyingToId, ); // Handle chat change (should be placed after `useDraft` and `useEditing`) @@ -890,7 +886,7 @@ const Composer: FC = ({ lastMessageSendTimeSeconds.current = getServerTime(); - clearDraft({ chatId, localOnly: true }); + clearDraft({ chatId, isLocalOnly: true }); // Wait until message animation starts requestMeasure(() => { @@ -971,7 +967,7 @@ const Composer: FC = ({ lastMessageSendTimeSeconds.current = getServerTime(); - clearDraft({ chatId, localOnly: true }); + clearDraft({ chatId, isLocalOnly: true }); if (IS_IOS && messageInput && messageInput === document.activeElement) { applyIosAutoCapitalizationFix(messageInput); @@ -1158,14 +1154,14 @@ const Composer: FC = ({ applyIosAutoCapitalizationFix(messageInput); } - clearDraft({ chatId, localOnly: true }); + clearDraft({ chatId, isLocalOnly: true }); requestMeasure(() => { resetComposer(); }); }); const handleBotCommandSelect = useLastCallback(() => { - clearDraft({ chatId, localOnly: true }); + clearDraft({ chatId, isLocalOnly: true }); requestMeasure(() => { resetComposer(); }); @@ -1930,8 +1926,6 @@ export default memo(withGlobal( ? selectEditingScheduledDraft(global, chatId) : selectEditingDraft(global, chatId, threadId); - const replyingToId = selectReplyingToId(global, chatId, threadId); - const story = storyId && selectPeerStory(global, chatId, storyId); const sentStoryReaction = story && 'sentReaction' in story ? story.sentReaction : undefined; @@ -1940,7 +1934,6 @@ export default memo(withGlobal( topReactions: type === 'story' ? global.topReactions : undefined, isOnActiveTab: !tabState.isBlurred, editingMessage: selectEditingMessage(global, chatId, threadId, messageListType), - replyingToId, draft: selectDraft(global, chatId, threadId), chat, isChatWithBot, diff --git a/src/components/common/EmbeddedMessage.tsx b/src/components/common/EmbeddedMessage.tsx deleted file mode 100644 index b56bf5f06..000000000 --- a/src/components/common/EmbeddedMessage.tsx +++ /dev/null @@ -1,173 +0,0 @@ -import type { FC } from '../../lib/teact/teact'; -import React, { useRef } from '../../lib/teact/teact'; - -import type { - ApiMessage, ApiPeer, -} from '../../api/types'; -import type { ChatTranslatedMessages } from '../../global/types'; -import type { ObserveFn } from '../../hooks/useIntersectionObserver'; - -import { - getMessageIsSpoiler, - getMessageMediaHash, - getMessageRoundVideo, - getPeerColorKey, - getSenderTitle, - isActionMessage, - isMessageTranslatable, -} from '../../global/helpers'; -import buildClassName from '../../util/buildClassName'; -import { getPictogramDimensions } from './helpers/mediaDimensions'; -import renderText from './helpers/renderText'; - -import { useFastClick } from '../../hooks/useFastClick'; -import { useIsIntersecting } from '../../hooks/useIntersectionObserver'; -import useLang from '../../hooks/useLang'; -import useMedia from '../../hooks/useMedia'; -import useThumbnail from '../../hooks/useThumbnail'; -import useMessageTranslation from '../middle/message/hooks/useMessageTranslation'; - -import ActionMessage from '../middle/ActionMessage'; -import Icon from './Icon'; -import MediaSpoiler from './MediaSpoiler'; -import MessageSummary from './MessageSummary'; - -import './EmbeddedMessage.scss'; - -type OwnProps = { - className?: string; - message?: ApiMessage; - sender?: ApiPeer; - forwardSender?: ApiPeer; - title?: string; - customText?: string; - noUserColors?: boolean; - isProtected?: boolean; - hasContextMenu?: boolean; - chatTranslations?: ChatTranslatedMessages; - requestedChatTranslationLanguage?: string; - observeIntersectionForLoading?: ObserveFn; - observeIntersectionForPlaying?: ObserveFn; - onClick: NoneToVoidFunction; -}; - -const NBSP = '\u00A0'; - -const EmbeddedMessage: FC = ({ - className, - message, - sender, - forwardSender, - title, - customText, - isProtected, - noUserColors, - hasContextMenu, - chatTranslations, - requestedChatTranslationLanguage, - observeIntersectionForLoading, - observeIntersectionForPlaying, - onClick, -}) => { - // eslint-disable-next-line no-null/no-null - const ref = useRef(null); - const isIntersecting = useIsIntersecting(ref, observeIntersectionForLoading); - - const mediaBlobUrl = useMedia(message && getMessageMediaHash(message, 'pictogram'), !isIntersecting); - const mediaThumbnail = useThumbnail(message); - const isRoundVideo = Boolean(message && getMessageRoundVideo(message)); - const isSpoiler = Boolean(message && getMessageIsSpoiler(message)); - - const shouldTranslate = message && isMessageTranslatable(message); - const { translatedText } = useMessageTranslation( - chatTranslations, message?.chatId, shouldTranslate ? message?.id : undefined, requestedChatTranslationLanguage, - ); - - const lang = useLang(); - - const senderTitle = sender ? getSenderTitle(lang, sender) : message?.forwardInfo?.hiddenUserName; - const forwardSenderTitle = forwardSender ? getSenderTitle(lang, forwardSender) - : message?.forwardInfo?.hiddenUserName; - const areSendersSame = sender?.id === forwardSender?.id; - - const { handleClick, handleMouseDown } = useFastClick(onClick); - - return ( -
- {mediaThumbnail && renderPictogram(mediaThumbnail, mediaBlobUrl, isRoundVideo, isProtected, isSpoiler)} -
-

- {!message ? ( - customText || NBSP - ) : isActionMessage(message) ? ( - - ) : ( - - )} -

-
- {renderText(senderTitle || title || NBSP)} - {forwardSenderTitle && !areSendersSame && ( - <> - - {renderText(forwardSenderTitle)} - - )} -
-
- {hasContextMenu && } -
- ); -}; - -function renderPictogram( - thumbDataUri: string, - blobUrl?: string, - isRoundVideo?: boolean, - isProtected?: boolean, - isSpoiler?: boolean, -) { - const { width, height } = getPictogramDimensions(); - - const srcUrl = blobUrl || thumbDataUri; - - return ( -
- {!isSpoiler && ( - - )} - - {isProtected && } -
- ); -} - -export default EmbeddedMessage; diff --git a/src/components/common/ManageUsernames.module.scss b/src/components/common/ManageUsernames.module.scss index ac2aa229d..de4cf855f 100644 --- a/src/components/common/ManageUsernames.module.scss +++ b/src/components/common/ManageUsernames.module.scss @@ -1,10 +1,10 @@ -@import "../../styles/mixins"; +@use "../../styles/mixins"; .container { padding: 1.5rem 1.5rem 0; margin-bottom: 0.625rem; - @include side-panel-section; + @include mixins.side-panel-section; } .header { diff --git a/src/components/common/ProfilePhoto.tsx b/src/components/common/ProfilePhoto.tsx index ec1544421..f2f236a80 100644 --- a/src/components/common/ProfilePhoto.tsx +++ b/src/components/common/ProfilePhoto.tsx @@ -6,7 +6,6 @@ import type { ApiChat, ApiPhoto, ApiUser } from '../../api/types'; import { getChatAvatarHash, getChatTitle, - getPeerColorKey, getUserFullName, getVideoAvatarMediaHash, isChatWithRepliesBot, @@ -16,6 +15,7 @@ import { import buildClassName from '../../util/buildClassName'; import { getFirstLetters } from '../../util/textFormat'; import { IS_CANVAS_FILTER_SUPPORTED } from '../../util/windowEnvironment'; +import { getPeerColorClass } from './helpers/peerColor'; import renderText from './helpers/renderText'; import useAppLayout from '../../hooks/useAppLayout'; @@ -55,11 +55,11 @@ const ProfilePhoto: FC = ({ const isDeleted = user && isDeletedUser(user); const isRepliesChat = chat && isChatWithRepliesBot(chat.id); - const userOrChat = user || chat; - const canHaveMedia = userOrChat && !isSavedMessages && !isDeleted && !isRepliesChat; + const peer = user || chat; + const canHaveMedia = peer && !isSavedMessages && !isDeleted && !isRepliesChat; const { isVideo } = photo || {}; - const avatarHash = canHaveMedia && getChatAvatarHash(userOrChat, 'normal'); + const avatarHash = canHaveMedia && getChatAvatarHash(peer, 'normal'); const avatarBlobUrl = useMedia(avatarHash); const photoHash = canHaveMedia && photo && !isVideo && `photo${photo.id}?size=c`; @@ -139,7 +139,7 @@ const ProfilePhoto: FC = ({ const fullClassName = buildClassName( 'ProfilePhoto', - `color-bg-${getPeerColorKey(user || chat)}`, + getPeerColorClass(peer), isSavedMessages && 'saved-messages', isDeleted && 'deleted-account', isRepliesChat && 'replies-bot-account', diff --git a/src/components/common/StickerView.module.scss b/src/components/common/StickerView.module.scss index 0dbe436b7..565271d71 100644 --- a/src/components/common/StickerView.module.scss +++ b/src/components/common/StickerView.module.scss @@ -1,7 +1,3 @@ -:root { - --thumbs-background: var(--color-background); -} - .thumb { width: 100%; height: 100%; @@ -13,7 +9,6 @@ } .thumb-opaque { - background: var(--thumbs-background); transition-delay: 0s; } diff --git a/src/components/common/EmbeddedMessage.scss b/src/components/common/embedded/EmbeddedMessage.scss similarity index 64% rename from src/components/common/EmbeddedMessage.scss rename to src/components/common/embedded/EmbeddedMessage.scss index a8e20827b..a25ff7fdf 100644 --- a/src/components/common/EmbeddedMessage.scss +++ b/src/components/common/embedded/EmbeddedMessage.scss @@ -1,21 +1,22 @@ +@use "sass:map"; +@use "../../../styles/mixins"; +@use "../../../styles/icons"; + .EmbeddedMessage { display: flex; align-items: center; font-size: calc(var(--message-text-size, 1rem) - 0.125rem); line-height: 1.125rem; - margin: 0 -0.25rem 0.0625rem; - padding: 0.1875rem 0.25rem 0.1875rem 0.4375rem; + margin-bottom: 0.0625rem; + padding: 0.1875rem 0.375rem 0.1875rem 0.1875rem; border-radius: var(--border-radius-messages-small); position: relative; overflow: hidden; cursor: var(--custom-cursor, pointer); - direction: ltr; - @for $i from 1 through 8 { - &.color-#{$i} { - --accent-color: var(--color-user-#{$i}); - } - } + background-color: var(--accent-background-color); + + transition: background-color 0.2s ease-in; body.no-page-transitions & { .ripple-container { @@ -25,17 +26,10 @@ .custom-shape & { max-width: 15rem; - padding: 0.5rem; margin: 0; - background-color: var(--background-color); + background-color: var(--color-reply-active); box-shadow: 0 1px 2px var(--color-default-shadow); - &::before { - left: 0.625rem; - top: 0.625rem; - bottom: 0.625rem; - } - .embedded-thumb { margin-inline-start: 0.5rem; } @@ -49,38 +43,62 @@ content: ""; display: block; position: absolute; - top: 0.3125rem; - bottom: 0.3125rem; - left: 0.375rem; - width: 2px; - background: var(--accent-color); - border-radius: 2px; - } - - &:hover { - background-color: var(--hover-color); + top: 0; + bottom: 0; + inset-inline-start: 0; + width: 3px; + background: var(--bar-gradient, var(--accent-color)); } &:active { - background-color: var(--active-color); + background-color: var(--background-active-color); + } + + &.is-quote { + .message-title { + padding-inline-end: 0.75rem; + } + + .message-text .embedded-text-wrapper { + white-space: normal; + } + + &::after { + @include icons.icon; + content: map.get(icons.$icons-map, "quote"); + + color: var(--accent-color); + position: absolute; + top: 0.25rem; + inset-inline-end: 0.25rem; + + font-size: 0.625rem; + } + } + + &.with-thumb { + .message-title { + padding-inline-start: 2.25rem; + } + + .embedded-text-wrapper { + text-indent: 2.25rem; + } } .message-title { font-size: calc(var(--message-text-size, 1rem) - 0.125rem); } - .embedded-more { - font-size: 1.125rem; - margin-inline-end: 0.125rem; - line-height: 0.9375rem; - vertical-align: -0.1875rem; + .embedded-origin-icon { + margin-inline: 0.125rem; + vertical-align: middle; + line-height: 1.25; } - .embedded-origin-icon { - display: inline-block; + .embedded-chat-icon { font-size: 0.75rem; - margin-inline: 0.125rem; - transform: translateY(1px); + vertical-align: middle; } .message-text { @@ -90,15 +108,20 @@ flex-direction: column-reverse; .message-title { + display: flex; + align-items: center; + flex-wrap: wrap; + flex: 1; + column-gap: 0.25rem; + } + + .message-title, .embedded-sender { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; - margin-bottom: 0.125rem; - flex: 1; - display: block; } - p { + .embedded-text-wrapper { white-space: nowrap; overflow: hidden; text-overflow: ellipsis; @@ -139,7 +162,9 @@ } .embedded-thumb { - position: relative; + position: absolute; + top: 0.375rem; + inset-inline-start: 0.375rem; width: 2rem; height: 2rem; border-radius: 0.25rem; @@ -159,16 +184,13 @@ object-fit: cover; } + &--background-icons { + color: var(--accent-color); + } + &.inside-input { padding-inline-start: 0.5625rem; width: 100%; - --accent-color: var(--color-primary); - --hover-color: var(--color-interactive-element-hover); - --active-color: var(--color-reply-active); - - &::before { - bottom: 0.3125rem; - } .embedded-thumb { margin-left: 0.125rem; @@ -183,11 +205,5 @@ font-weight: 500; color: var(--accent-color); } - - .embedded-more { - font-size: 1.5rem; - opacity: 0.8; - color: var(--color-text-secondary); - } } } diff --git a/src/components/common/embedded/EmbeddedMessage.tsx b/src/components/common/embedded/EmbeddedMessage.tsx new file mode 100644 index 000000000..77ae482d9 --- /dev/null +++ b/src/components/common/embedded/EmbeddedMessage.tsx @@ -0,0 +1,249 @@ +import type { FC } from '../../../lib/teact/teact'; +import React, { useMemo, useRef } from '../../../lib/teact/teact'; + +import type { + ApiChat, + ApiMessage, ApiPeer, ApiReplyInfo, +} from '../../../api/types'; +import type { ChatTranslatedMessages } from '../../../global/types'; +import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; +import type { IconName } from '../../../types/icons'; + +import { + getMessageIsSpoiler, + getMessageMediaHash, + getMessageRoundVideo, + getSenderTitle, + isActionMessage, + isChatChannel, + isChatGroup, + isMessageTranslatable, +} from '../../../global/helpers'; +import buildClassName from '../../../util/buildClassName'; +import { getPictogramDimensions } from '../helpers/mediaDimensions'; +import { getPeerColorClass } from '../helpers/peerColor'; +import renderText from '../helpers/renderText'; +import { renderTextWithEntities } from '../helpers/renderTextWithEntities'; + +import { useFastClick } from '../../../hooks/useFastClick'; +import { useIsIntersecting } from '../../../hooks/useIntersectionObserver'; +import useLang from '../../../hooks/useLang'; +import useMedia from '../../../hooks/useMedia'; +import useThumbnail from '../../../hooks/useThumbnail'; +import useMessageTranslation from '../../middle/message/hooks/useMessageTranslation'; + +import ActionMessage from '../../middle/ActionMessage'; +import Icon from '../Icon'; +import MediaSpoiler from '../MediaSpoiler'; +import MessageSummary from '../MessageSummary'; +import EmojiIconBackground from './EmojiIconBackground'; + +import './EmbeddedMessage.scss'; + +type OwnProps = { + className?: string; + replyInfo?: ApiReplyInfo; + message?: ApiMessage; + sender?: ApiPeer; + senderChat?: ApiChat; + forwardSender?: ApiPeer; + title?: string; + customText?: string; + noUserColors?: boolean; + isProtected?: boolean; + chatTranslations?: ChatTranslatedMessages; + requestedChatTranslationLanguage?: string; + observeIntersectionForLoading?: ObserveFn; + observeIntersectionForPlaying?: ObserveFn; + onClick: NoneToVoidFunction; +}; + +const NBSP = '\u00A0'; + +const EmbeddedMessage: FC = ({ + className, + message, + replyInfo, + sender, + senderChat, + forwardSender, + title, + customText, + isProtected, + noUserColors, + chatTranslations, + requestedChatTranslationLanguage, + observeIntersectionForLoading, + observeIntersectionForPlaying, + onClick, +}) => { + // eslint-disable-next-line no-null/no-null + const ref = useRef(null); + const isIntersecting = useIsIntersecting(ref, observeIntersectionForLoading); + + const wrappedMedia = useMemo(() => { + const replyMedia = replyInfo?.type === 'message' && replyInfo.replyMedia; + if (!replyMedia) return undefined; + return { + content: replyMedia, + }; + }, [replyInfo]); + + const mediaBlobUrl = useMedia(message && getMessageMediaHash(message, 'pictogram'), !isIntersecting); + const mediaThumbnail = useThumbnail(message || wrappedMedia); + const isRoundVideo = Boolean(message && getMessageRoundVideo(message)); + const isSpoiler = Boolean(message && getMessageIsSpoiler(message)); + const isQuote = Boolean(replyInfo?.type === 'message' && replyInfo.isQuote); + const replyForwardInfo = replyInfo?.type === 'message' ? replyInfo.replyFrom : undefined; + + const shouldTranslate = message && isMessageTranslatable(message); + const { translatedText } = useMessageTranslation( + chatTranslations, message?.chatId, shouldTranslate ? message?.id : undefined, requestedChatTranslationLanguage, + ); + + const lang = useLang(); + + const senderTitle = sender ? getSenderTitle(lang, sender) + : (replyForwardInfo?.hiddenUserName || message?.forwardInfo?.hiddenUserName); + const senderChatTitle = senderChat ? getSenderTitle(lang, senderChat) : message?.forwardInfo?.hiddenUserName; + const forwardSenderTitle = forwardSender ? getSenderTitle(lang, forwardSender) + : message?.forwardInfo?.hiddenUserName; + const areSendersSame = sender?.id === forwardSender?.id; + + const { handleClick, handleMouseDown } = useFastClick(onClick); + + function renderTextContent() { + if (replyInfo?.type === 'message' && replyInfo.quoteText) { + return renderTextWithEntities({ + text: replyInfo.quoteText.text, + entities: replyInfo.quoteText.entities, + }); + } + + if (!message) { + return customText || NBSP; + } + + if (isActionMessage(message)) { + return ( + + ); + } + + return ( + + ); + } + + function renderSender() { + if (forwardSenderTitle && !areSendersSame) { + return ( + <> + + {renderText(forwardSenderTitle)} + + ); + } + + if (title) { + return renderText(title); + } + + if (!senderTitle) { + return NBSP; + } + + let shouldIgnoreSender = false; + let icon: IconName | undefined; + if (senderChat) { + if (isChatChannel(senderChat)) { + shouldIgnoreSender = true; + icon = 'channel-filled'; + } + + if (isChatGroup(senderChat)) { + icon = 'group-filled'; + } + } + + return ( + <> + {!shouldIgnoreSender && {renderText(senderTitle)}} + {icon && } + {senderChatTitle && renderText(senderChatTitle)} + + ); + } + + return ( +
+ {mediaThumbnail && renderPictogram(mediaThumbnail, mediaBlobUrl, isRoundVideo, isProtected, isSpoiler)} + {sender?.backgroundEmojiId && ( + + )} +
+

+ {renderTextContent()} +

+
+ {renderSender()} +
+
+
+ ); +}; + +function renderPictogram( + thumbDataUri: string, + blobUrl?: string, + isRoundVideo?: boolean, + isProtected?: boolean, + isSpoiler?: boolean, +) { + const { width, height } = getPictogramDimensions(); + + const srcUrl = blobUrl || thumbDataUri; + + return ( +
+ {!isSpoiler && ( + + )} + + {isProtected && } +
+ ); +} + +export default EmbeddedMessage; diff --git a/src/components/common/EmbeddedStory.tsx b/src/components/common/embedded/EmbeddedStory.tsx similarity index 66% rename from src/components/common/EmbeddedStory.tsx rename to src/components/common/embedded/EmbeddedStory.tsx index e24f0b5cd..e346e9f3f 100644 --- a/src/components/common/EmbeddedStory.tsx +++ b/src/components/common/embedded/EmbeddedStory.tsx @@ -1,24 +1,26 @@ -import type { FC } from '../../lib/teact/teact'; -import React, { useRef } from '../../lib/teact/teact'; -import { getActions } from '../../global'; +import type { FC } from '../../../lib/teact/teact'; +import React, { useRef } from '../../../lib/teact/teact'; +import { getActions } from '../../../global'; -import type { ApiPeer, ApiTypeStory } from '../../api/types'; -import type { ObserveFn } from '../../hooks/useIntersectionObserver'; +import type { ApiPeer, ApiTypeStory } from '../../../api/types'; +import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import { - getPeerColorKey, getSenderTitle, getStoryMediaHash, -} from '../../global/helpers'; -import buildClassName from '../../util/buildClassName'; -import { getPictogramDimensions } from './helpers/mediaDimensions'; -import renderText from './helpers/renderText'; +} from '../../../global/helpers'; +import buildClassName from '../../../util/buildClassName'; +import { getPictogramDimensions } from '../helpers/mediaDimensions'; +import { getPeerColorClass } from '../helpers/peerColor'; +import renderText from '../helpers/renderText'; -import { useFastClick } from '../../hooks/useFastClick'; -import { useIsIntersecting } from '../../hooks/useIntersectionObserver'; -import useLang from '../../hooks/useLang'; -import useLastCallback from '../../hooks/useLastCallback'; -import useMedia from '../../hooks/useMedia'; +import { useFastClick } from '../../../hooks/useFastClick'; +import { useIsIntersecting } from '../../../hooks/useIntersectionObserver'; +import useLang from '../../../hooks/useLang'; +import useLastCallback from '../../../hooks/useLastCallback'; +import useMedia from '../../../hooks/useMedia'; + +import Icon from '../Icon'; import './EmbeddedMessage.scss'; @@ -75,23 +77,24 @@ const EmbeddedStory: FC = ({ ref={ref} className={buildClassName( 'EmbeddedMessage', - sender && !noUserColors && `color-${getPeerColorKey(sender)}`, + getPeerColorClass(sender, noUserColors, true), + pictogramUrl && 'with-thumb', )} onClick={handleClick} onMouseDown={handleMouseDown} > {pictogramUrl && renderPictogram(pictogramUrl, isProtected)}
-

+

{isExpiredStory && ( - + )} {isFullStory && ( - + )} {lang(title)}

-
{renderText(senderTitle || NBSP)}
+
{renderText(senderTitle || NBSP)}
); diff --git a/src/components/common/embedded/EmojiIconBackground.module.scss b/src/components/common/embedded/EmojiIconBackground.module.scss new file mode 100644 index 000000000..795b588bf --- /dev/null +++ b/src/components/common/embedded/EmojiIconBackground.module.scss @@ -0,0 +1,15 @@ +.root { + --custom-emoji-border-radius: 0.25rem; + --custom-emoji-size: 1.25rem; + + position: absolute; + top: 0; + right: 0; + bottom: 0; + left: 0; + pointer-events: none; +} + +.emoji { + position: absolute; +} diff --git a/src/components/common/embedded/EmojiIconBackground.tsx b/src/components/common/embedded/EmojiIconBackground.tsx new file mode 100644 index 000000000..27be167fe --- /dev/null +++ b/src/components/common/embedded/EmojiIconBackground.tsx @@ -0,0 +1,86 @@ +import React, { memo } from '../../../lib/teact/teact'; + +import buildClassName from '../../../util/buildClassName'; +import buildStyle from '../../../util/buildStyle'; + +import CustomEmoji from '../CustomEmoji'; + +import styles from './EmojiIconBackground.module.scss'; + +type IconPosition = { + inline: number; + block: number; + opacity: number; + scale: number; +}; + +const ICON_POSITIONS: IconPosition[] = [ + { + inline: 5, block: 15, opacity: 0.35, scale: 1, + }, + { + inline: 10, block: 45, opacity: 0.3, scale: 0.9, + }, + { + inline: 20, block: 75, opacity: 0.3, scale: 0.75, + }, + { + inline: 40, block: 20, opacity: 0.25, scale: 0.8, + }, + { + inline: 60, block: 50, opacity: 0.25, scale: 0.85, + }, + { + inline: 55, block: -5, opacity: 0.20, scale: 0.75, + }, + { + inline: 80, block: 15, opacity: 0.15, scale: 0.95, + }, + { + inline: 100, block: 70, opacity: 0.15, scale: 0.9, + }, + { + inline: 120, block: 25, opacity: 0.10, scale: 0.65, + }, + { + inline: 140, block: 0, opacity: 0.10, scale: 0.75, + }, +]; + +type OwnProps = { + emojiDocumentId: string; + className?: string; +}; + +const EmojiIconBackground = ({ + emojiDocumentId, + className, +}: OwnProps) => { + return ( +
+ {ICON_POSITIONS.map((position) => { + const { + inline, block, opacity, scale, + } = position; + + const style = buildStyle( + `inset-inline-end: ${inline}px`, + `inset-block-start: ${block}px`, + `opacity: ${opacity}`, + `transform: scale(${scale})`, + ); + + return ( + + ); + })} +
+ ); +}; + +export default memo(EmojiIconBackground); diff --git a/src/components/common/helpers/peerColor.ts b/src/components/common/helpers/peerColor.ts new file mode 100644 index 000000000..8e9c5eefb --- /dev/null +++ b/src/components/common/helpers/peerColor.ts @@ -0,0 +1,11 @@ +import type { ApiPeer } from '../../../api/types'; + +import { getPeerColorCount, getPeerColorKey } from '../../../global/helpers'; + +export function getPeerColorClass(peer?: ApiPeer, noUserColors?: boolean, shouldReset?: boolean) { + if (!peer) { + if (!shouldReset) return undefined; + return noUserColors ? 'peer-color-count-0' : 'peer-color-0'; + } + return noUserColors ? `peer-color-count-${getPeerColorCount(peer)}` : `peer-color-${getPeerColorKey(peer)}`; +} diff --git a/src/components/common/helpers/renderTextWithEntities.tsx b/src/components/common/helpers/renderTextWithEntities.tsx index fb79c6f21..a3ff0b43d 100644 --- a/src/components/common/helpers/renderTextWithEntities.tsx +++ b/src/components/common/helpers/renderTextWithEntities.tsx @@ -391,7 +391,13 @@ function processEntity({ case ApiMessageEntityTypes.Bold: return {renderNestedMessagePart()}; case ApiMessageEntityTypes.Blockquote: - return
{renderNestedMessagePart()}
; + return ( +
+
+ {renderNestedMessagePart()} +
+
+ ); case ApiMessageEntityTypes.BotCommand: return ( ( return {}; } - const { senderId, replyToMessageId, isOutgoing } = chat.lastMessage || {}; + const { lastMessage } = chat; + const { senderId, isOutgoing } = lastMessage || {}; + const replyToMessageId = lastMessage && getMessageReplyInfo(lastMessage)?.replyToMsgId; const lastMessageSender = senderId ? (selectUser(global, senderId) || selectChat(global, senderId)) : undefined; - const lastMessageAction = chat.lastMessage ? getMessageAction(chat.lastMessage) : undefined; + const lastMessageAction = lastMessage ? getMessageAction(lastMessage) : undefined; const actionTargetMessage = lastMessageAction && replyToMessageId ? selectChatMessage(global, chat.id, replyToMessageId) : undefined; @@ -364,7 +367,7 @@ export default memo(withGlobal( const user = privateChatUserId ? selectUser(global, privateChatUserId) : undefined; const userStatus = privateChatUserId ? selectUserStatus(global, privateChatUserId) : undefined; - const lastMessageTopic = chat.lastMessage && selectTopicFromMessage(global, chat.lastMessage); + const lastMessageTopic = lastMessage && selectTopicFromMessage(global, lastMessage); const typingStatus = selectThreadParam(global, chatId, MAIN_THREAD_ID, 'typingStatus'); @@ -381,8 +384,8 @@ export default memo(withGlobal( isForumPanelOpen: selectIsForumPanelOpen(global), canScrollDown: isSelected && messageListType === 'thread', canChangeFolder: (global.chatFolders.orderedIds?.length || 0) > 1, - ...(isOutgoing && chat.lastMessage && { - lastMessageOutgoingStatus: selectOutgoingStatus(global, chat.lastMessage), + ...(isOutgoing && lastMessage && { + lastMessageOutgoingStatus: selectOutgoingStatus(global, lastMessage), }), user, userStatus, diff --git a/src/components/left/main/LeftMainHeader.scss b/src/components/left/main/LeftMainHeader.scss index 6dc776302..041f644d4 100644 --- a/src/components/left/main/LeftMainHeader.scss +++ b/src/components/left/main/LeftMainHeader.scss @@ -1,4 +1,4 @@ -@import "../../../styles/mixins"; +@use "../../../styles/mixins"; #LeftMainHeader { position: relative; @@ -123,7 +123,7 @@ } // @optimization - @include while-transition() { + @include mixins.while-transition() { .Menu .bubble { transition: none !important; } diff --git a/src/components/left/main/LeftSideMenuItems.tsx b/src/components/left/main/LeftSideMenuItems.tsx index 5c1ca65d6..a7bc34c00 100644 --- a/src/components/left/main/LeftSideMenuItems.tsx +++ b/src/components/left/main/LeftSideMenuItems.tsx @@ -158,6 +158,7 @@ const LeftSideMenuItems = ({ bot={bot} theme={theme} isInSideMenu + canShowNew onMenuOpened={onBotMenuOpened} onMenuClosed={onBotMenuClosed} /> diff --git a/src/components/left/main/Topic.tsx b/src/components/left/main/Topic.tsx index 32c740f7a..359b76c61 100644 --- a/src/components/left/main/Topic.tsx +++ b/src/components/left/main/Topic.tsx @@ -3,13 +3,15 @@ import React, { memo } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; import type { - ApiChat, ApiFormattedText, ApiMessage, ApiMessageOutgoingStatus, + ApiChat, ApiMessage, ApiMessageOutgoingStatus, ApiPeer, ApiTopic, ApiTypingStatus, } from '../../../api/types'; +import type { ApiDraft } from '../../../global/types'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import type { ChatAnimationTypes } from './hooks'; import { getMessageAction } from '../../../global/helpers'; +import { getMessageReplyInfo } from '../../../global/helpers/replies'; import { selectCanAnimateInterface, selectCanDeleteTopic, @@ -48,7 +50,6 @@ type OwnProps = { isSelected: boolean; style: string; observeIntersection?: ObserveFn; - orderDiff: number; animationType: ChatAnimationTypes; }; @@ -63,7 +64,7 @@ type StateProps = { lastMessageSender?: ApiPeer; actionTargetChatId?: string; typingStatus?: ApiTypingStatus; - draft?: ApiFormattedText; + draft?: ApiDraft; canScrollDown?: boolean; wasTopicOpened?: boolean; withInterfaceAnimations?: boolean; @@ -232,8 +233,9 @@ export default memo(withGlobal( (global, { chatId, topic, isSelected }) => { const chat = selectChat(global, chatId); - const lastMessage = selectChatMessage(global, chatId, topic.lastMessageId)!; - const { senderId, replyToMessageId, isOutgoing } = lastMessage || {}; + const lastMessage = selectChatMessage(global, chatId, topic.lastMessageId); + const { senderId, isOutgoing } = lastMessage || {}; + const replyToMessageId = lastMessage && getMessageReplyInfo(lastMessage)?.replyToMsgId; const lastMessageSender = senderId ? (selectUser(global, senderId) || selectChat(global, senderId)) : undefined; const lastMessageAction = lastMessage ? getMessageAction(lastMessage) : undefined; diff --git a/src/components/left/main/hooks/useChatListEntry.tsx b/src/components/left/main/hooks/useChatListEntry.tsx index 1603c9584..c97fdb8ff 100644 --- a/src/components/left/main/hooks/useChatListEntry.tsx +++ b/src/components/left/main/hooks/useChatListEntry.tsx @@ -6,7 +6,7 @@ import { getGlobal } from '../../../../global'; import type { ApiChat, ApiMessage, ApiPeer, ApiTopic, ApiTypingStatus, ApiUser, } from '../../../../api/types'; -import type { Thread } from '../../../../global/types'; +import type { ApiDraft } from '../../../../global/types'; import type { ObserveFn } from '../../../../hooks/useIntersectionObserver'; import type { LangFn } from '../../../../hooks/useLang'; @@ -23,6 +23,7 @@ import { isActionMessage, isChatChannel, } from '../../../../global/helpers'; +import { getMessageReplyInfo } from '../../../../global/helpers/replies'; import buildClassName from '../../../../util/buildClassName'; import { renderActionMessageText } from '../../../common/helpers/renderActionMessageText'; import renderText from '../../../common/helpers/renderText'; @@ -60,7 +61,7 @@ export default function useChatListEntry({ lastMessage?: ApiMessage; chatId: string; typingStatus?: ApiTypingStatus; - draft?: Thread['draft']; + draft?: ApiDraft; actionTargetMessage?: ApiMessage; actionTargetUserIds?: string[]; lastMessageTopic?: ApiTopic; @@ -79,7 +80,8 @@ export default function useChatListEntry({ const isAction = lastMessage && isActionMessage(lastMessage); - useEnsureMessage(chatId, isAction ? lastMessage.replyToMessageId : undefined, actionTargetMessage); + const replyToMessageId = lastMessage && getMessageReplyInfo(lastMessage)?.replyToMsgId; + useEnsureMessage(chatId, isAction ? replyToMessageId : undefined, actionTargetMessage); const mediaThumbnail = lastMessage && !getMessageSticker(lastMessage) ? getMessageMediaThumbDataUri(lastMessage) @@ -102,13 +104,15 @@ export default function useChatListEntry({ return ; } - if (draft?.text.length && (!chat?.isForum || isTopic)) { + const isDraftReplyToTopic = draft && draft.replyInfo?.replyToMsgId === lastMessageTopic?.id; + + if (draft && (!chat?.isForum || (isTopic && !isDraftReplyToTopic))) { return (

{lang('Draft')} {renderTextWithEntities({ - text: draft.text, - entities: draft.entities, + text: draft.text?.text || '', + entities: draft.text?.entities, isSimple: true, withTranslucentThumbs: true, })} @@ -153,7 +157,7 @@ export default function useChatListEntry({ )} {lastMessage.forwardInfo && ()} - {Boolean(lastMessage.replyToStoryId) && ()} + {lastMessage.replyInfo?.type === 'story' && ()} {renderSummary(lang, lastMessage, observeIntersection, mediaBlobUrl || mediaThumbnail, isRoundVideo)}

); diff --git a/src/components/left/settings/Settings.scss b/src/components/left/settings/Settings.scss index eb92bbe20..992f38df4 100644 --- a/src/components/left/settings/Settings.scss +++ b/src/components/left/settings/Settings.scss @@ -1,4 +1,4 @@ -@import "../../../styles/mixins"; +@use "../../../styles/mixins"; #Settings { height: 100%; @@ -82,7 +82,7 @@ text-align: center; margin-bottom: 0.625rem; - @include side-panel-section; + @include mixins.side-panel-section; &.no-border { margin-bottom: 0; @@ -109,12 +109,12 @@ .settings-main-menu { padding: 0.5rem; - @include side-panel-section; + @include mixins.side-panel-section; > .ChatExtra { padding: 0 0.5rem 0.3125rem; margin: 0 -0.5rem 0.625rem; - @include side-panel-section; + @include mixins.side-panel-section; .ListItem.narrow { margin-bottom: 0.25rem; @@ -125,7 +125,7 @@ .settings-item-simple, .settings-item { padding: 1.5rem 1.5rem 1rem; - @include side-panel-section; + @include mixins.side-panel-section; } .settings-item { diff --git a/src/components/left/settings/SettingsGeneralBackground.scss b/src/components/left/settings/SettingsGeneralBackground.scss index 47c345aad..065df7bd0 100644 --- a/src/components/left/settings/SettingsGeneralBackground.scss +++ b/src/components/left/settings/SettingsGeneralBackground.scss @@ -1,4 +1,4 @@ -@import "../../../styles/mixins"; +@use "../../../styles/mixins"; .SettingsGeneralBackground { .settings-wallpapers { @@ -6,7 +6,7 @@ grid-template-columns: repeat(3, 1fr); grid-auto-rows: 1fr; grid-gap: 0.0625rem; - @include side-panel-section; + @include mixins.side-panel-section; } .Loading { diff --git a/src/components/left/settings/SettingsGeneralBackgroundColor.scss b/src/components/left/settings/SettingsGeneralBackgroundColor.scss index 3d738bb8b..001dfa872 100644 --- a/src/components/left/settings/SettingsGeneralBackgroundColor.scss +++ b/src/components/left/settings/SettingsGeneralBackgroundColor.scss @@ -1,4 +1,4 @@ -@import "../../../styles/mixins"; +@use "../../../styles/mixins"; .SettingsGeneralBackgroundColor { &:not(.is-dragging) .handle { @@ -71,7 +71,7 @@ grid-template-columns: repeat(3, 1fr); grid-auto-rows: 1fr; grid-gap: 0.0625rem; - @include side-panel-section; + @include mixins.side-panel-section; } .predefined-color { diff --git a/src/components/main/premium/PremiumMainModal.module.scss b/src/components/main/premium/PremiumMainModal.module.scss index 1791b6b7d..201dc3297 100644 --- a/src/components/main/premium/PremiumMainModal.module.scss +++ b/src/components/main/premium/PremiumMainModal.module.scss @@ -1,4 +1,4 @@ -@import '../../../styles/mixins'; +@use '../../../styles/mixins'; .root { --premium-gradient: linear-gradient(88.39deg, #6C93FF -2.56%, #976FFF 51.27%, #DF69D1 107.39%); @@ -39,7 +39,7 @@ flex-direction: column; align-items: center; - @include adapt-padding-to-scrollbar(0.5rem); + @include mixins.adapt-padding-to-scrollbar(0.5rem); } .logo { diff --git a/src/components/middle/ActionMessage.tsx b/src/components/middle/ActionMessage.tsx index 99a28e918..58450c6bd 100644 --- a/src/components/middle/ActionMessage.tsx +++ b/src/components/middle/ActionMessage.tsx @@ -13,6 +13,7 @@ import type { FocusDirection } from '../../types'; import type { PinnedIntersectionChangedCallback } from './hooks/usePinnedMessage'; import { getMessageHtmlId, isChatChannel } from '../../global/helpers'; +import { getMessageReplyInfo } from '../../global/helpers/replies'; import { selectCanPlayAnimatedEmojis, selectChat, @@ -102,7 +103,11 @@ const ActionMessage: FC = ({ const ref = useRef(null); useOnIntersect(ref, observeIntersectionForReading); - useEnsureMessage(message.chatId, message.replyToMessageId, targetMessage); + useEnsureMessage( + message.chatId, + message.replyInfo?.type === 'message' ? message.replyInfo.replyToMsgId : undefined, + targetMessage, + ); useFocusMessage(ref, message.chatId, isFocused, focusDirection, noFocusHighlight, isJustAdded); useEffect(() => { @@ -263,12 +268,12 @@ const ActionMessage: FC = ({ export default memo(withGlobal( (global, { message, threadId }): StateProps => { const { - chatId, senderId, replyToMessageId, content, + chatId, senderId, content, } = message; const userId = senderId; const { targetUserIds, targetChatId } = content.action || {}; - const targetMessageId = replyToMessageId; + const targetMessageId = getMessageReplyInfo(message)?.replyToMsgId; const targetMessage = targetMessageId ? selectChatMessage(global, chatId, targetMessageId) : undefined; diff --git a/src/components/middle/HeaderPinnedMessage.module.scss b/src/components/middle/HeaderPinnedMessage.module.scss index 897c38f9e..1eb6b0b6f 100644 --- a/src/components/middle/HeaderPinnedMessage.module.scss +++ b/src/components/middle/HeaderPinnedMessage.module.scss @@ -1,4 +1,4 @@ -@import "../../styles/mixins"; +@use "../../styles/mixins"; .root { display: flex; @@ -247,7 +247,7 @@ } .root { - @include header-mobile(); + @include mixins.header-mobile(); } } diff --git a/src/components/middle/MiddleColumn.tsx b/src/components/middle/MiddleColumn.tsx index b9f9e491f..e0fa9b067 100644 --- a/src/components/middle/MiddleColumn.tsx +++ b/src/components/middle/MiddleColumn.tsx @@ -4,7 +4,7 @@ import React, { } from '../../lib/teact/teact'; import { getActions, withGlobal } from '../../global'; -import type { ApiChat, ApiChatBannedRights } from '../../api/types'; +import type { ApiChat, ApiChatBannedRights, ApiInputMessageReplyInfo } from '../../api/types'; import type { ActiveEmojiInteraction, MessageListType, @@ -44,12 +44,12 @@ import { selectChatMessage, selectCurrentMessageList, selectCurrentTextSearch, + selectDraft, selectIsChatBotNotStarted, selectIsInSelectMode, selectIsRightColumnShown, selectIsUserBlocked, selectPinnedIds, - selectReplyingToId, selectTabState, selectTheme, selectThreadInfo, @@ -105,7 +105,7 @@ type StateProps = { threadId?: number; messageListType?: MessageListType; chat?: ApiChat; - replyingToId?: number; + draftReplyInfo?: ApiInputMessageReplyInfo; isPrivate?: boolean; isPinnedMessageList?: boolean; canPost?: boolean; @@ -160,7 +160,7 @@ function MiddleColumn({ messageListType, isMobile, chat, - replyingToId, + draftReplyInfo, isPrivate, isPinnedMessageList, canPost, @@ -433,7 +433,7 @@ function MiddleColumn({ const messageSendingRestrictionReason = getMessageSendingRestrictionReason( lang, currentUserBannedRights, defaultBannedRights, ); - const forumComposerPlaceholder = getForumComposerPlaceholder(lang, chat, threadId, Boolean(replyingToId)); + const forumComposerPlaceholder = getForumComposerPlaceholder(lang, chat, threadId, Boolean(draftReplyInfo)); const composerRestrictionMessage = messageSendingRestrictionReason || forumComposerPlaceholder; @@ -752,9 +752,9 @@ export default memo(withGlobal( const shouldLoadFullChat = Boolean( chat && isChatGroup(chat) && !selectChatFullInfo(global, chat.id), ); - const replyingToId = selectReplyingToId(global, chatId, threadId); + const draftReplyInfo = selectDraft(global, chatId, threadId)?.replyInfo; const shouldBlockSendInForum = chat?.isForum - ? threadId === MAIN_THREAD_ID && !replyingToId && (chat.topics?.[GENERAL_TOPIC_ID]?.isClosed) + ? threadId === MAIN_THREAD_ID && !draftReplyInfo && (chat.topics?.[GENERAL_TOPIC_ID]?.isClosed) : false; const audioMessage = audioChatId && audioMessageId ? selectChatMessage(global, audioChatId, audioMessageId) @@ -776,7 +776,7 @@ export default memo(withGlobal( threadId, messageListType, chat, - replyingToId, + draftReplyInfo, isPrivate, areChatSettingsLoaded: Boolean(chat?.settings), canPost: !isPinnedMessageList diff --git a/src/components/middle/MiddleHeader.scss b/src/components/middle/MiddleHeader.scss index bb1b28680..3d47864a2 100644 --- a/src/components/middle/MiddleHeader.scss +++ b/src/components/middle/MiddleHeader.scss @@ -1,8 +1,8 @@ -@import "../../styles/mixins"; +@use "../../styles/mixins"; @mixin mobile-header-styles() { .AudioPlayer { - @include header-mobile; + @include mixins.header-mobile; flex-direction: row; margin-top: 0; diff --git a/src/components/middle/composer/AttachBotItem.tsx b/src/components/middle/composer/AttachBotItem.tsx index 67f48fdc5..139ea9d1b 100644 --- a/src/components/middle/composer/AttachBotItem.tsx +++ b/src/components/middle/composer/AttachBotItem.tsx @@ -21,6 +21,7 @@ type OwnProps = { isInSideMenu?: true; chatId?: string; threadId?: number; + canShowNew?: boolean; onMenuOpened: VoidFunction; onMenuClosed: VoidFunction; }; @@ -31,6 +32,7 @@ const AttachBotItem: FC = ({ chatId, threadId, isInSideMenu, + canShowNew, onMenuOpened, onMenuClosed, }) => { @@ -93,6 +95,7 @@ const AttachBotItem: FC = ({ onContextMenu={handleContextMenu} > {bot.shortName} + {canShowNew && bot.isDisclaimerNeeded && {lang('New')}} {menuPosition && ( = ({ - replyingToId, + replyInfo, editingId, message, sender, @@ -77,7 +78,7 @@ const ComposerEmbeddedMessage: FC = ({ onClear, }) => { const { - setReplyingToId, + resetDraftReplyInfo, setEditingId, focusMessage, changeForwardRecipient, @@ -89,23 +90,31 @@ const ComposerEmbeddedMessage: FC = ({ const ref = useRef(null); const lang = useLang(); + const isReplyToTopicStart = message?.content.action?.type === 'topicCreate'; + const isForwarding = Boolean(forwardedMessagesCount); const isShown = Boolean( - ((replyingToId || editingId) && message) + ((replyInfo || editingId) && message) || (sender && forwardedMessagesCount), ); const canAnimate = useAsyncRendering( - [forwardedMessagesCount], - forwardedMessagesCount ? FORWARD_RENDERING_DELAY : undefined, + [isShown], + isShown ? FORWARD_RENDERING_DELAY : undefined, ); const { shouldRender, transitionClassNames, - } = useShowTransition(canAnimate && isShown, undefined, !shouldAnimate, undefined, !shouldAnimate); + } = useShowTransition( + canAnimate && isShown && !isReplyToTopicStart, + undefined, + !shouldAnimate, + undefined, + !shouldAnimate, + ); const clearEmbedded = useLastCallback(() => { - if (replyingToId && !shouldForceShowEditing) { - setReplyingToId({ messageId: undefined }); + if (replyInfo && !shouldForceShowEditing) { + resetDraftReplyInfo(); } else if (editingId) { setEditingId({ messageId: undefined }); } else if (forwardedMessagesCount) { @@ -153,9 +162,13 @@ const ComposerEmbeddedMessage: FC = ({ }, [handleContextMenuClose, shouldRender]); const className = buildClassName('ComposerEmbeddedMessage', transitionClassNames); + const innerClassName = buildClassName( + 'ComposerEmbeddedMessage_inner', + getPeerColorClass(sender), + ); const leftIcon = useMemo(() => { - if (replyingToId && !shouldForceShowEditing) { + if (replyInfo && !shouldForceShowEditing) { return 'icon-reply'; } if (editingId) { @@ -166,7 +179,7 @@ const ComposerEmbeddedMessage: FC = ({ } return undefined; - }, [editingId, isForwarding, replyingToId, shouldForceShowEditing]); + }, [editingId, isForwarding, replyInfo, shouldForceShowEditing]); const customText = forwardedMessagesCount && forwardedMessagesCount > 1 ? lang('ForwardedMessageCount', forwardedMessagesCount) @@ -191,18 +204,18 @@ const ComposerEmbeddedMessage: FC = ({ return (
-
+