Message: Reply re-design and blockquotes (#3926)

This commit is contained in:
Alexander Zinchuk 2023-11-06 01:43:21 +04:00
parent f894010a70
commit 330bc42c98
127 changed files with 2430 additions and 1377 deletions

View File

@ -1,13 +1,4 @@
@use "sass:map"; @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 { .icon-char::before {
font-family: Roboto, "Helvetica Neue", sans-serif; font-family: Roboto, "Helvetica Neue", sans-serif;
@ -17,9 +8,7 @@ ${{ name }}-font: "{{ name }}";
display: block; display: block;
} }
{{# if selector }}{{ selector }}::before { @mixin icon {
{{ else }}{{ tag }}.{{prefix}} {
{{/ if }}
/* use !important to prevent issues with browser extensions that change fonts */ /* use !important to prevent issues with browser extensions that change fonts */
/* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */ /* stylelint-disable-next-line font-family-no-missing-generic-family-keyword */
font-family: "{{ name }}" !important; font-family: "{{ name }}" !important;
@ -35,6 +24,12 @@ ${{ name }}-font: "{{ name }}";
-moz-osx-font-smoothing: grayscale; -moz-osx-font-smoothing: grayscale;
} }
{{# if selector }}{{ selector }}::before {
{{ else }}{{ tag }}.{{prefix}} {
{{/ if }}
@include icon;
}
${{ name }}-map: ( ${{ name }}-map: (
{{# each codepoints }} {{# each codepoints }}
"{{ @key }}": "\\{{ codepoint this }}", "{{ @key }}": "\\{{ codepoint this }}",

View File

@ -52,6 +52,8 @@ export interface GramJsAppConfig extends LimitsConfig {
story_expire_period: number; story_expire_period: number;
story_viewers_expire_period: number; story_viewers_expire_period: number;
stories_changelog_user_id?: number; stories_changelog_user_id?: number;
peer_colors: Record<string, string[]>;
dark_peer_colors: Record<string, string[]>;
} }
function buildEmojiSounds(appConfig: GramJsAppConfig) { 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, storyExpirePeriod: appConfig.story_expire_period ?? STORY_EXPIRE_PERIOD,
storyViewersExpirePeriod: appConfig.story_viewers_expire_period ?? STORY_VIEWERS_EXPIRE_PERIOD, storyViewersExpirePeriod: appConfig.story_viewers_expire_period ?? STORY_VIEWERS_EXPIRE_PERIOD,
storyChangelogUserId: appConfig.stories_changelog_user_id?.toString() ?? SERVICE_NOTIFICATIONS_USER_ID, storyChangelogUserId: appConfig.stories_changelog_user_id?.toString() ?? SERVICE_NOTIFICATIONS_USER_ID,
peerColors: appConfig.peer_colors,
darkPeerColors: appConfig.dark_peer_colors,
}; };
} }

View File

@ -72,7 +72,6 @@ export function buildBotSwitchWebview(switchWebview?: GramJs.InlineBotWebView) {
export function buildApiAttachBot(bot: GramJs.AttachMenuBot): ApiAttachBot { export function buildApiAttachBot(bot: GramJs.AttachMenuBot): ApiAttachBot {
return { return {
id: bot.botId.toString(), id: bot.botId.toString(),
hasSettings: bot.hasSettings,
shouldRequestWriteAccess: bot.requestWriteAccess, shouldRequestWriteAccess: bot.requestWriteAccess,
shortName: bot.shortName, shortName: bot.shortName,
isForAttachMenu: bot.showInAttachMenu!, isForAttachMenu: bot.showInAttachMenu!,

View File

@ -54,6 +54,8 @@ function buildApiChatFieldsFromPeerEntity(
const areStoriesHidden = Boolean('storiesHidden' in peerEntity && peerEntity.storiesHidden); const areStoriesHidden = Boolean('storiesHidden' in peerEntity && peerEntity.storiesHidden);
const maxStoryId = 'storiesMaxId' in peerEntity ? peerEntity.storiesMaxId : undefined; const maxStoryId = 'storiesMaxId' in peerEntity ? peerEntity.storiesMaxId : undefined;
const storiesUnavailable = Boolean('storiesUnavailable' in peerEntity && peerEntity.storiesUnavailable); 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 { return {
isMin, isMin,
@ -66,7 +68,7 @@ function buildApiChatFieldsFromPeerEntity(
...('verified' in peerEntity && { isVerified: peerEntity.verified }), ...('verified' in peerEntity && { isVerified: peerEntity.verified }),
...('callActive' in peerEntity && { isCallActive: peerEntity.callActive }), ...('callActive' in peerEntity && { isCallActive: peerEntity.callActive }),
...('callNotEmpty' in peerEntity && { isCallNotEmpty: peerEntity.callNotEmpty }), ...('callNotEmpty' in peerEntity && { isCallNotEmpty: peerEntity.callNotEmpty }),
...('date' in peerEntity && { joinDate: peerEntity.date }), ...('date' in peerEntity && { creationDate: peerEntity.date }),
...('participantsCount' in peerEntity && peerEntity.participantsCount !== undefined && { ...('participantsCount' in peerEntity && peerEntity.participantsCount !== undefined && {
membersCount: peerEntity.participantsCount, membersCount: peerEntity.participantsCount,
}), }),
@ -77,6 +79,8 @@ function buildApiChatFieldsFromPeerEntity(
...buildApiChatRestrictions(peerEntity), ...buildApiChatRestrictions(peerEntity),
...buildApiChatMigrationInfo(peerEntity), ...buildApiChatMigrationInfo(peerEntity),
fakeType: isScam ? 'scam' : (isFake ? 'fake' : undefined), fakeType: isScam ? 'scam' : (isFake ? 'fake' : undefined),
color,
backgroundEmojiId,
isJoinToSend, isJoinToSend,
isJoinRequest, isJoinRequest,
isForum, isForum,

View File

@ -8,7 +8,6 @@ import type {
ApiGame, ApiGame,
ApiInvoice, ApiInvoice,
ApiLocation, ApiLocation,
ApiMessage,
ApiMessageExtendedMediaPreview, ApiMessageExtendedMediaPreview,
ApiMessageStoryData, ApiMessageStoryData,
ApiPhoto, ApiPhoto,
@ -19,6 +18,7 @@ import type {
ApiWebDocument, ApiWebDocument,
ApiWebPage, ApiWebPage,
ApiWebPageStoryData, ApiWebPageStoryData,
MediaContent,
} from '../../types'; } from '../../types';
import type { UniversalMessage } from './messages'; import type { UniversalMessage } from './messages';
@ -38,7 +38,7 @@ import { buildStickerFromDocument } from './symbols';
export function buildMessageContent( export function buildMessageContent(
mtpMessage: UniversalMessage | GramJs.UpdateServiceNotification, mtpMessage: UniversalMessage | GramJs.UpdateServiceNotification,
) { ) {
let content: ApiMessage['content'] = {}; let content: MediaContent = {};
if (mtpMessage.media) { if (mtpMessage.media) {
content = { 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) { if ('ttlSeconds' in media && media.ttlSeconds) {
return undefined; return undefined;
} }

View File

@ -1,11 +1,14 @@
import { Api as GramJs } from '../../../lib/gramjs'; import { Api as GramJs } from '../../../lib/gramjs';
import type { ApiDraft } from '../../../global/types';
import type { import type {
ApiAction, ApiAction,
ApiAttachment, ApiAttachment,
ApiChat, ApiChat,
ApiContact, ApiContact,
ApiGroupCall, ApiGroupCall,
ApiInputMessageReplyInfo,
ApiInputReplyInfo,
ApiKeyboardButton, ApiKeyboardButton,
ApiMessage, ApiMessage,
ApiMessageEntity, ApiMessageEntity,
@ -13,14 +16,15 @@ import type {
ApiNewPoll, ApiNewPoll,
ApiPeer, ApiPeer,
ApiPhoto, ApiPhoto,
ApiReplyInfo,
ApiReplyKeyboard, ApiReplyKeyboard,
ApiSponsoredMessage, ApiSponsoredMessage,
ApiSticker, ApiSticker,
ApiStory, ApiStory,
ApiStorySkipped, ApiStorySkipped,
ApiThreadInfo, ApiThreadInfo,
ApiTypeReplyTo,
ApiVideo, ApiVideo,
MediaContent,
PhoneCallAction, PhoneCallAction,
} from '../../types'; } from '../../types';
import { import {
@ -49,7 +53,7 @@ import { buildApiCallDiscardReason } from './calls';
import { import {
buildApiPhoto, buildApiPhoto,
} from './common'; } from './common';
import { buildMessageContent, buildMessageTextContent } from './messageContent'; import { buildMessageContent, buildMessageMediaContent, buildMessageTextContent } from './messageContent';
import { buildApiPeerId, getApiChatIdFromMtpPeer, isPeerUser } from './peers'; import { buildApiPeerId, getApiChatIdFromMtpPeer, isPeerUser } from './peers';
import { buildMessageReactions } from './reactions'; import { buildMessageReactions } from './reactions';
@ -171,23 +175,6 @@ export function buildApiMessageWithChatId(
const isInvoiceMedia = mtpMessage.media instanceof GramJs.MessageMediaInvoice const isInvoiceMedia = mtpMessage.media instanceof GramJs.MessageMediaInvoice
&& Boolean(mtpMessage.media.extendedMedia); && 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 isEdited = mtpMessage.editDate && !mtpMessage.editHide;
const { const {
inlineButtons, keyboardButtons, keyboardPlaceholder, isKeyboardSingleUse, isKeyboardSelective, inlineButtons, keyboardButtons, keyboardPlaceholder, isKeyboardSingleUse, isKeyboardSelective,
@ -218,12 +205,8 @@ export function buildApiMessageWithChatId(
isPinned: mtpMessage.pinned, isPinned: mtpMessage.pinned,
reactions: mtpMessage.reactions && buildMessageReactions(mtpMessage.reactions), reactions: mtpMessage.reactions && buildMessageReactions(mtpMessage.reactions),
emojiOnlyCount, emojiOnlyCount,
...(replyToMsgId && { replyToMessageId: replyToMsgId }), ...(mtpMessage.replyTo && { replyInfo: buildApiReplyInfo(mtpMessage.replyTo) }),
...(forumTopic && { isTopicReply: true }),
...(replyToPeerId && { replyToChatId: getApiChatIdFromMtpPeer(replyToPeerId) }),
...(replyToTopId && { replyToTopMessageId: replyToTopId }),
...(forwardInfo && { forwardInfo }), ...(forwardInfo && { forwardInfo }),
...(replyToStoryUserId && { replyToStoryUserId, replyToStoryId }),
...(isEdited && { isEdited }), ...(isEdited && { isEdited }),
...(mtpMessage.editDate && { editDate: mtpMessage.editDate }), ...(mtpMessage.editDate && { editDate: mtpMessage.editDate }),
...(isMediaUnread && { isMediaUnread }), ...(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) { if (draft instanceof GramJs.DraftMessageEmpty) {
return undefined; return undefined;
} }
const { const {
message, entities, replyToMsgId, date, message, entities, replyTo, date,
} = draft; } = 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 { return {
formattedText: message ? buildMessageTextContent(message, entities) : undefined, text: message ? buildMessageTextContent(message, entities) : undefined,
replyingToId: replyToMsgId, replyInfo,
date, 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( function buildAction(
action: GramJs.TypeMessageAction, action: GramJs.TypeMessageAction,
senderId: string | undefined, senderId: string | undefined,
@ -682,7 +711,7 @@ export function buildLocalMessage(
chat: ApiChat, chat: ApiChat,
text?: string, text?: string,
entities?: ApiMessageEntity[], entities?: ApiMessageEntity[],
replyingTo?: ApiTypeReplyTo, replyInfo?: ApiInputReplyInfo,
attachment?: ApiAttachment, attachment?: ApiAttachment,
sticker?: ApiSticker, sticker?: ApiSticker,
gif?: ApiVideo, gif?: ApiVideo,
@ -696,21 +725,8 @@ export function buildLocalMessage(
const localId = getNextLocalMessageId(chat.lastMessage?.id); const localId = getNextLocalMessageId(chat.lastMessage?.id);
const media = attachment && buildUploadingMedia(attachment); const media = attachment && buildUploadingMedia(attachment);
const isChannel = chat.type === 'chatTypeChannel'; const isChannel = chat.type === 'chatTypeChannel';
const isForum = chat.isForum;
let replyToMessageId: number | undefined; const resultReplyInfo = replyInfo && buildReplyInfo(replyInfo, chat.isForum);
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 message = { const message = {
id: localId, id: localId,
@ -732,10 +748,7 @@ export function buildLocalMessage(
date: scheduledAt || Math.round(Date.now() / 1000) + getServerTimeOffset(), date: scheduledAt || Math.round(Date.now() / 1000) + getServerTimeOffset(),
isOutgoing: !isChannel, isOutgoing: !isChannel,
senderId: sendAs?.id || currentUserId, senderId: sendAs?.id || currentUserId,
...(replyToMessageId && { replyToMessageId }), replyInfo: resultReplyInfo,
...(replyingToTopId && { replyToTopMessageId: replyingToTopId }),
...((replyToMessageId || replyingToTopId) && isForum && { isTopicReply: true }),
...(replyToStoryUserId && { replyToStoryUserId, replyToStoryId }),
...(groupedId && { ...(groupedId && {
groupedId, groupedId,
...(media && (media.photo || media.video) && { isInAlbum: true }), ...(media && (media.photo || media.video) && { isInAlbum: true }),
@ -796,6 +809,12 @@ export function buildLocalForwardedMessage({
text: !shouldHideText ? strippedText : undefined, text: !shouldHideText ? strippedText : undefined,
}; };
const replyInfo: ApiReplyInfo | undefined = toThreadId ? {
type: 'message',
replyToTopId: toThreadId,
isForumTopic: toChat.isForum || undefined,
} : undefined;
return { return {
id: localId, id: localId,
chatId: toChat.id, chatId: toChat.id,
@ -807,7 +826,7 @@ export function buildLocalForwardedMessage({
groupedId, groupedId,
isInAlbum, isInAlbum,
isForwardingAllowed: true, isForwardingAllowed: true,
replyToTopMessageId: toThreadId, replyInfo,
...(toThreadId && toChat?.isForum && { isTopicReply: true }), ...(toThreadId && toChat?.isForum && { isTopicReply: true }),
...(emojiOnlyCount && { emojiOnlyCount }), ...(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( function buildUploadingMedia(
attachment: ApiAttachment, attachment: ApiAttachment,
): ApiMessage['content'] { ): MediaContent {
const { const {
filename: fileName, filename: fileName,
blobUrl, blobUrl,

View File

@ -1,18 +1,17 @@
import { Api as GramJs, errors } from '../../../lib/gramjs'; import { Api as GramJs } from '../../../lib/gramjs';
import type { import type {
ApiApplyBoostInfo,
ApiBoostsStatus, ApiBoostsStatus,
ApiMediaArea, ApiMediaArea,
ApiMediaAreaCoordinates, ApiMediaAreaCoordinates,
ApiMessage, ApiMyBoost,
ApiStealthMode, ApiStealthMode,
ApiStoryView, ApiStoryView,
ApiTypeStory, ApiTypeStory,
MediaContent,
} from '../../types'; } from '../../types';
import { buildCollectionByCallback } from '../../../util/iteratees'; import { buildCollectionByCallback } from '../../../util/iteratees';
import { getServerTime } from '../../../util/serverTime';
import { buildPrivacyRules } from './common'; import { buildPrivacyRules } from './common';
import { buildGeoPoint, buildMessageMediaContent, buildMessageTextContent } from './messageContent'; import { buildGeoPoint, buildMessageMediaContent, buildMessageTextContent } from './messageContent';
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers'; import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
@ -49,7 +48,7 @@ export function buildApiStory(peerId: string, story: GramJs.TypeStoryItem): ApiT
mediaAreas, sentReaction, out, mediaAreas, sentReaction, out,
} = story; } = story;
const content: ApiMessage['content'] = { const content: MediaContent = {
...buildMessageMediaContent(media), ...buildMessageMediaContent(media),
}; };
@ -171,45 +170,7 @@ export function buildApiPeerStories(peerStories: GramJs.PeerStories) {
return buildCollectionByCallback(peerStories.stories, (story) => [story.id, buildApiStory(peerId, story)]); return buildCollectionByCallback(peerStories.stories, (story) => [story.id, buildApiStory(peerId, story)]);
} }
export function buildApiApplyBoostInfo( export function buildApiBoostsStatus(boostStatus: GramJs.premium.BoostsStatus): ApiBoostsStatus {
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 {
const { const {
level, boostUrl, boosts, myBoost, currentLevelBoosts, nextLevelBoosts, premiumAudience, level, boostUrl, boosts, myBoost, currentLevelBoosts, nextLevelBoosts, premiumAudience,
} = boostStatus; } = boostStatus;
@ -223,3 +184,17 @@ export function buildApiBoostsStatus(boostStatus: GramJs.stories.BoostsStatus):
...(premiumAudience && { premiumSubscribers: buildStatisticsPercentage(premiumAudience) }), ...(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),
};
}

View File

@ -85,6 +85,8 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined {
hasStories: Boolean(storiesMaxId) && !storiesUnavailable, hasStories: Boolean(storiesMaxId) && !storiesUnavailable,
...(mtpUser.bot && mtpUser.botInlinePlaceholder && { botPlaceholder: mtpUser.botInlinePlaceholder }), ...(mtpUser.bot && mtpUser.botInlinePlaceholder && { botPlaceholder: mtpUser.botInlinePlaceholder }),
...(mtpUser.bot && mtpUser.botAttachMenu && { isAttachBot: mtpUser.botAttachMenu }), ...(mtpUser.bot && mtpUser.botAttachMenu && { isAttachBot: mtpUser.botAttachMenu }),
color: mtpUser.color,
backgroundEmojiId: mtpUser.backgroundEmojiId?.toString(),
}; };
} }

View File

@ -11,6 +11,7 @@ import type {
ApiChatReactions, ApiChatReactions,
ApiFormattedText, ApiFormattedText,
ApiGroupCall, ApiGroupCall,
ApiInputReplyInfo,
ApiMessageEntity, ApiMessageEntity,
ApiNewPoll, ApiNewPoll,
ApiPhoneCall, ApiPhoneCall,
@ -24,7 +25,6 @@ import type {
ApiStory, ApiStory,
ApiStorySkipped, ApiStorySkipped,
ApiThemeParameters, ApiThemeParameters,
ApiTypeReplyTo,
ApiVideo, ApiVideo,
} from '../../types'; } from '../../types';
import { import {
@ -643,24 +643,28 @@ export function buildInputBotApp(app: ApiBotApp) {
}); });
} }
export function buildInputReplyToMessage(replyToMsgId: number, topMsgId?: number) { export function buildInputReplyTo(replyInfo: ApiInputReplyInfo) {
return new GramJs.InputReplyToMessage({ if (replyInfo.type === 'story') {
replyToMsgId, return new GramJs.InputReplyToStory({
topMsgId, userId: buildInputPeerFromLocalDb(replyInfo.userId)!,
}); storyId: replyInfo.storyId,
} });
}
export function buildInputReplyToStory(userId: string, storyId: number) { if (replyInfo.type === 'message') {
return new GramJs.InputReplyToStory({ const {
userId: buildInputPeerFromLocalDb(userId)!, replyToMsgId, replyToTopId, replyToPeerId, quoteText,
storyId, } = 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 undefined;
return 'replyingTo' in replyingTo
? buildInputReplyToMessage(replyingTo.replyingTo, replyingTo.replyingToTopId)
: buildInputReplyToStory(replyingTo.userId, replyingTo.storyId);
} }
export function buildInputPrivacyRules( export function buildInputPrivacyRules(

View File

@ -50,29 +50,10 @@ export function addMessageToLocalDb(message: GramJs.Message | GramJs.MessageServ
localDb.messages[messageFullId] = mockMessage; localDb.messages[messageFullId] = mockMessage;
if (mockMessage instanceof GramJs.Message) { if (mockMessage instanceof GramJs.Message) {
if (mockMessage.media instanceof GramJs.MessageMediaDocument if (mockMessage.media) addMediaToLocalDb(mockMessage.media);
&& mockMessage.media.document instanceof GramJs.Document
) {
localDb.documents[String(mockMessage.media.document.id)] = mockMessage.media.document;
}
if (mockMessage.media instanceof GramJs.MessageMediaWebPage if (mockMessage.replyTo instanceof GramJs.MessageReplyHeader && mockMessage.replyTo.replyMedia) {
&& mockMessage.media.webpage instanceof GramJs.WebPage addMediaToLocalDb(mockMessage.replyTo.replyMedia);
&& 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;
} }
} }
@ -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) { export function addStoryToLocalDb(story: GramJs.TypeStoryItem, peerId: string) {
if (!(story instanceof GramJs.StoryItem)) { if (!(story instanceof GramJs.StoryItem)) {
return; return;

View File

@ -3,7 +3,7 @@ import { Api as GramJs } from '../../../lib/gramjs';
import type { import type {
ApiBotApp, ApiBotApp,
ApiChat, ApiPeer, ApiThemeParameters, ApiUser, OnApiUpdate, ApiChat, ApiInputMessageReplyInfo, ApiPeer, ApiThemeParameters, ApiUser, OnApiUpdate,
} from '../../types'; } from '../../types';
import { WEB_APP_PLATFORM } from '../../../config'; import { WEB_APP_PLATFORM } from '../../../config';
@ -24,7 +24,7 @@ import {
buildInputBotApp, buildInputBotApp,
buildInputEntity, buildInputEntity,
buildInputPeer, buildInputPeer,
buildInputReplyToMessage, buildInputReplyTo,
buildInputThemeParams, buildInputThemeParams,
generateRandomBigInt, generateRandomBigInt,
} from '../gramjsBuilders'; } from '../gramjsBuilders';
@ -124,13 +124,12 @@ export async function fetchInlineBotResults({
} }
export async function sendInlineBotResult({ export async function sendInlineBotResult({
chat, replyingToTopId, resultId, queryId, replyingTo, sendAs, isSilent, scheduleDate, chat, replyInfo, resultId, queryId, sendAs, isSilent, scheduleDate,
}: { }: {
chat: ApiChat; chat: ApiChat;
replyingToTopId?: number; replyInfo?: ApiInputMessageReplyInfo;
resultId: string; resultId: string;
queryId: string; queryId: string;
replyingTo?: number;
sendAs?: ApiPeer; sendAs?: ApiPeer;
isSilent?: boolean; isSilent?: boolean;
scheduleDate?: number; scheduleDate?: number;
@ -144,9 +143,8 @@ export async function sendInlineBotResult({
peer: buildInputPeer(chat.id, chat.accessHash), peer: buildInputPeer(chat.id, chat.accessHash),
id: resultId, id: resultId,
scheduleDate, scheduleDate,
...(replyingToTopId && { topMsgId: replyingToTopId }), replyTo: replyInfo && buildInputReplyTo(replyInfo),
...(isSilent && { silent: true }), ...(isSilent && { silent: true }),
...(replyingTo && { replyToMsgId: replyingTo }),
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }), ...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
})); }));
} }
@ -173,8 +171,7 @@ export async function requestWebView({
bot, bot,
url, url,
startParam, startParam,
replyToMessageId, replyInfo,
threadId,
theme, theme,
sendAs, sendAs,
isFromBotMenu, isFromBotMenu,
@ -184,8 +181,7 @@ export async function requestWebView({
bot: ApiUser; bot: ApiUser;
url?: string; url?: string;
startParam?: string; startParam?: string;
replyToMessageId?: number; replyInfo?: ApiInputMessageReplyInfo;
threadId?: number;
theme?: ApiThemeParameters; theme?: ApiThemeParameters;
sendAs?: ApiPeer; sendAs?: ApiPeer;
isFromBotMenu?: boolean; isFromBotMenu?: boolean;
@ -199,7 +195,7 @@ export async function requestWebView({
themeParams: theme ? buildInputThemeParams(theme) : undefined, themeParams: theme ? buildInputThemeParams(theme) : undefined,
fromBotMenu: isFromBotMenu || undefined, fromBotMenu: isFromBotMenu || undefined,
platform: WEB_APP_PLATFORM, platform: WEB_APP_PLATFORM,
...(replyToMessageId && { replyTo: buildInputReplyToMessage(replyToMessageId, threadId) }), replyTo: replyInfo && buildInputReplyTo(replyInfo),
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }), ...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
})); }));
@ -292,16 +288,14 @@ export function prolongWebView({
peer, peer,
bot, bot,
queryId, queryId,
replyToMessageId, replyInfo,
threadId,
sendAs, sendAs,
}: { }: {
isSilent?: boolean; isSilent?: boolean;
peer: ApiPeer; peer: ApiPeer;
bot: ApiUser; bot: ApiUser;
queryId: string; queryId: string;
replyToMessageId?: number; replyInfo?: ApiInputMessageReplyInfo;
threadId?: number;
sendAs?: ApiPeer; sendAs?: ApiPeer;
}) { }) {
return invokeRequest(new GramJs.messages.ProlongWebView({ return invokeRequest(new GramJs.messages.ProlongWebView({
@ -309,7 +303,7 @@ export function prolongWebView({
peer: buildInputPeer(peer.id, peer.accessHash), peer: buildInputPeer(peer.id, peer.accessHash),
bot: buildInputPeer(bot.id, bot.accessHash), bot: buildInputPeer(bot.id, bot.accessHash),
queryId: BigInt(queryId), queryId: BigInt(queryId),
...(replyToMessageId && { replyTo: buildInputReplyToMessage(replyToMessageId, threadId) }), replyTo: replyInfo && buildInputReplyTo(replyInfo),
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }), ...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
})); }));
} }

View File

@ -1,6 +1,7 @@
import BigInt from 'big-integer'; import BigInt from 'big-integer';
import { Api as GramJs } from '../../../lib/gramjs'; import { Api as GramJs } from '../../../lib/gramjs';
import type { ApiDraft } from '../../../global/types';
import type { import type {
ApiChat, ApiChat,
ApiChatAdminRights, ApiChatAdminRights,
@ -8,10 +9,8 @@ import type {
ApiChatFolder, ApiChatFolder,
ApiChatFullInfo, ApiChatFullInfo,
ApiChatReactions, ApiChatReactions,
ApiFormattedText,
ApiGroupCall, ApiGroupCall,
ApiMessage, ApiMessage,
ApiMessageEntity,
ApiPeer, ApiPeer,
ApiPhoto, ApiPhoto,
ApiTopic, ApiTopic,
@ -58,6 +57,7 @@ import {
buildInputEntity, buildInputEntity,
buildInputPeer, buildInputPeer,
buildInputPhoto, buildInputPhoto,
buildInputReplyTo,
buildMtpMessageEntity, buildMtpMessageEntity,
generateRandomBigInt, generateRandomBigInt,
isMessageWithMedia, isMessageWithMedia,
@ -135,7 +135,7 @@ export async function fetchChats({
} }
const chats: ApiChat[] = []; const chats: ApiChat[] = [];
const draftsById: Record<string, ApiFormattedText> = {}; const draftsById: Record<string, ApiDraft> = {};
const replyingToById: Record<string, number> = {}; const replyingToById: Record<string, number> = {};
const dialogs = (resultPinned ? resultPinned.dialogs : []).concat(result.dialogs); const dialogs = (resultPinned ? resultPinned.dialogs : []).concat(result.dialogs);
@ -180,12 +180,9 @@ export async function fetchChats({
} }
if (dialog.draft) { if (dialog.draft) {
const { formattedText, replyingToId } = buildMessageDraft(dialog.draft) || {}; const draft = buildMessageDraft(dialog.draft);
if (formattedText) { if (draft) {
draftsById[chat.id] = formattedText; draftsById[chat.id] = draft;
}
if (replyingToId) {
replyingToById[chat.id] = replyingToId;
} }
} }
}); });
@ -364,33 +361,16 @@ export async function requestChatUpdate({
export function saveDraft({ export function saveDraft({
chat, chat,
text, draft,
entities,
threadId,
replyToMsgId,
}: { }: {
chat: ApiChat; chat: ApiChat;
text: string; draft?: ApiDraft;
entities?: ApiMessageEntity[];
threadId?: number;
replyToMsgId?: number;
}) { }) {
return invokeRequest(new GramJs.messages.SaveDraft({ return invokeRequest(new GramJs.messages.SaveDraft({
peer: buildInputPeer(chat.id, chat.accessHash), peer: buildInputPeer(chat.id, chat.accessHash),
message: text, message: draft?.text?.text || '',
...(entities && { entities: draft?.text?.entities?.map(buildMtpMessageEntity),
entities: entities.map(buildMtpMessageEntity), replyTo: draft?.replyInfo && buildInputReplyTo(draft.replyInfo),
}),
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 }),
})); }));
} }

View File

@ -13,7 +13,7 @@ export {
export { export {
fetchChats, fetchFullChat, searchChats, requestChatUpdate, fetchChatSettings, fetchChats, fetchFullChat, searchChats, requestChatUpdate, fetchChatSettings,
saveDraft, clearDraft, fetchChat, updateChatMutedState, updateTopicMutedState, saveDraft, fetchChat, updateChatMutedState, updateTopicMutedState,
createChannel, joinChannel, deleteChatUser, deleteChat, leaveChannel, deleteChannel, createGroupChat, editChatPhoto, createChannel, joinChannel, deleteChatUser, deleteChat, leaveChannel, deleteChannel, createGroupChat, editChatPhoto,
toggleChatPinned, toggleChatArchived, toggleDialogUnread, setChatEnabledReactions, toggleChatPinned, toggleChatArchived, toggleDialogUnread, setChatEnabledReactions,
fetchChatFolders, editChatFolder, deleteChatFolder, sortChatFolders, fetchRecommendedChatFolders, fetchChatFolders, editChatFolder, deleteChatFolder, sortChatFolders, fetchRecommendedChatFolders,

View File

@ -6,6 +6,7 @@ import type {
ApiContact, ApiContact,
ApiFormattedText, ApiFormattedText,
ApiGlobalMessageSearchType, ApiGlobalMessageSearchType,
ApiInputReplyInfo,
ApiMessage, ApiMessage,
ApiMessageEntity, ApiMessageEntity,
ApiMessageSearchType, ApiMessageSearchType,
@ -18,8 +19,8 @@ import type {
ApiSticker, ApiSticker,
ApiStory, ApiStory,
ApiStorySkipped, ApiStorySkipped,
ApiTypeReplyTo,
ApiVideo, ApiVideo,
MediaContent,
OnApiUpdate, OnApiUpdate,
} from '../../types'; } from '../../types';
import { import {
@ -238,7 +239,7 @@ export function sendMessage(
chat, chat,
text, text,
entities, entities,
replyingTo, replyInfo,
attachment, attachment,
sticker, sticker,
story, story,
@ -256,7 +257,7 @@ export function sendMessage(
lastMessageId?: number; lastMessageId?: number;
text?: string; text?: string;
entities?: ApiMessageEntity[]; entities?: ApiMessageEntity[];
replyingTo?: ApiTypeReplyTo; replyInfo?: ApiInputReplyInfo;
attachment?: ApiAttachment; attachment?: ApiAttachment;
sticker?: ApiSticker; sticker?: ApiSticker;
story?: ApiStory | ApiStorySkipped; story?: ApiStory | ApiStorySkipped;
@ -276,7 +277,7 @@ export function sendMessage(
chat, chat,
text, text,
entities, entities,
replyingTo, replyInfo,
attachment, attachment,
sticker, sticker,
gif, gif,
@ -315,7 +316,7 @@ export function sendMessage(
chat, chat,
text, text,
entities, entities,
replyingTo, replyInfo,
attachment: attachment!, attachment: attachment!,
groupedId, groupedId,
isSilent, isSilent,
@ -356,7 +357,6 @@ export function sendMessage(
} }
const RequestClass = media ? GramJs.messages.SendMedia : GramJs.messages.SendMessage; const RequestClass = media ? GramJs.messages.SendMedia : GramJs.messages.SendMessage;
const replyTo = replyingTo ? buildInputReplyTo(replyingTo) : undefined;
try { try {
const update = await invokeRequest(new RequestClass({ const update = await invokeRequest(new RequestClass({
@ -365,9 +365,9 @@ export function sendMessage(
entities: entities ? entities.map(buildMtpMessageEntity) : undefined, entities: entities ? entities.map(buildMtpMessageEntity) : undefined,
peer: buildInputPeer(chat.id, chat.accessHash), peer: buildInputPeer(chat.id, chat.accessHash),
randomId, randomId,
replyTo: replyInfo && buildInputReplyTo(replyInfo),
...(isSilent && { silent: isSilent }), ...(isSilent && { silent: isSilent }),
...(scheduledAt && { scheduleDate: scheduledAt }), ...(scheduledAt && { scheduleDate: scheduledAt }),
...(replyTo && { replyTo }),
...(media && { media }), ...(media && { media }),
...(noWebPage && { noWebpage: noWebPage }), ...(noWebPage && { noWebpage: noWebPage }),
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }), ...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
@ -402,7 +402,7 @@ function sendGroupedMedia(
chat, chat,
text, text,
entities, entities,
replyingTo, replyInfo,
attachment, attachment,
groupedId, groupedId,
isSilent, isSilent,
@ -412,7 +412,7 @@ function sendGroupedMedia(
chat: ApiChat; chat: ApiChat;
text?: string; text?: string;
entities?: ApiMessageEntity[]; entities?: ApiMessageEntity[];
replyingTo?: ApiTypeReplyTo; replyInfo?: ApiInputReplyInfo;
attachment: ApiAttachment; attachment: ApiAttachment;
groupedId: string; groupedId: string;
isSilent?: boolean; isSilent?: boolean;
@ -484,13 +484,12 @@ function sendGroupedMedia(
const { singleMediaByIndex, localMessages } = groupedUploads[groupedId]; const { singleMediaByIndex, localMessages } = groupedUploads[groupedId];
delete groupedUploads[groupedId]; delete groupedUploads[groupedId];
const replyTo = replyingTo ? buildInputReplyTo(replyingTo) : undefined;
const update = await invokeRequest(new GramJs.messages.SendMultiMedia({ const update = await invokeRequest(new GramJs.messages.SendMultiMedia({
clearDraft: true, clearDraft: true,
peer: buildInputPeer(chat.id, chat.accessHash), peer: buildInputPeer(chat.id, chat.accessHash),
multiMedia: Object.values(singleMediaByIndex), // Object keys are usually ordered multiMedia: Object.values(singleMediaByIndex), // Object keys are usually ordered
...(replyingTo && { replyTo }), replyTo: replyInfo && buildInputReplyTo(replyInfo),
...(isSilent && { silent: isSilent }), ...(isSilent && { silent: isSilent }),
...(scheduledAt && { scheduleDate: scheduledAt }), ...(scheduledAt && { scheduleDate: scheduledAt }),
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }), ...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
@ -1738,7 +1737,7 @@ function handleLocalMessageUpdate(localMessage: ApiMessage, update: GramJs.TypeU
return; return;
} }
let newContent: ApiMessage['content'] | undefined; let newContent: MediaContent | undefined;
if (messageUpdate instanceof GramJs.UpdateShortSentMessage) { if (messageUpdate instanceof GramJs.UpdateShortSentMessage) {
if (localMessage.content.text && messageUpdate.entities) { if (localMessage.content.text && messageUpdate.entities) {
newContent = { newContent = {

View File

@ -17,9 +17,8 @@ import { buildCollectionByCallback } from '../../../util/iteratees';
import { buildApiChatFromPreview } from '../apiBuilders/chats'; import { buildApiChatFromPreview } from '../apiBuilders/chats';
import { getApiChatIdFromMtpPeer } from '../apiBuilders/peers'; import { getApiChatIdFromMtpPeer } from '../apiBuilders/peers';
import { import {
buildApiApplyBoostInfo,
buildApiApplyBoostInfoFromError,
buildApiBoostsStatus, buildApiBoostsStatus,
buildApiMyBoost,
buildApiPeerStories, buildApiPeerStories,
buildApiStealthMode, buildApiStealthMode,
buildApiStory, buildApiStory,
@ -430,50 +429,35 @@ export function activateStealthMode({
}); });
} }
export async function fetchCanApplyBoost({ export async function fetchMyBoosts() {
chat, const result = await invokeRequest(new GramJs.premium.GetMyBoosts());
} : {
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: [],
};
}
if (!result) { if (!result) return undefined;
return undefined;
}
const mtpChats = 'chats' in result ? result.chats : []; addEntitiesToLocalDb(result.users);
addEntitiesToLocalDb(mtpChats); addEntitiesToLocalDb(result.chats);
const chats = mtpChats.map((c) => buildApiChatFromPreview(c)).filter(Boolean); const users = result.users.map(buildApiUser).filter(Boolean);
const info = buildApiApplyBoostInfo(result); const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean);
const boosts = result.myBoosts.map(buildApiMyBoost);
return { return {
info, users,
chats, chats,
boosts,
}; };
} }
export function applyBoost({ export function applyBoost({
chat, chat,
slots,
} : { } : {
chat: ApiChat; chat: ApiChat;
slots: number[];
}) { }) {
return invokeRequest(new GramJs.stories.ApplyBoost({ return invokeRequest(new GramJs.premium.ApplyBoost({
peer: buildInputPeer(chat.id, chat.accessHash), peer: buildInputPeer(chat.id, chat.accessHash),
slots,
}), { }), {
shouldReturnTrue: true, shouldReturnTrue: true,
}); });
@ -484,7 +468,7 @@ export async function fetchBoostsStatus({
}: { }: {
chat: ApiChat; chat: ApiChat;
}) { }) {
const result = await invokeRequest(new GramJs.stories.GetBoostsStatus({ const result = await invokeRequest(new GramJs.premium.GetBoostsStatus({
peer: buildInputPeer(chat.id, chat.accessHash), peer: buildInputPeer(chat.id, chat.accessHash),
})); }));
@ -504,7 +488,7 @@ export async function fetchBoostersList({
offset?: string; offset?: string;
limit?: number; limit?: number;
}) { }) {
const result = await invokeRequest(new GramJs.stories.GetBoostersList({ const result = await invokeRequest(new GramJs.premium.GetBoostsList({
peer: buildInputPeer(chat.id, chat.accessHash), peer: buildInputPeer(chat.id, chat.accessHash),
offset, offset,
limit, limit,
@ -518,9 +502,10 @@ export async function fetchBoostersList({
const users = result.users.map(buildApiUser).filter(Boolean); const users = result.users.map(buildApiUser).filter(Boolean);
const boosterIds = result.boosters.map((booster) => booster.userId.toString()); const userBoosts = result.boosts.filter((boost) => boost.userId);
const boosters = buildCollectionByCallback(result.boosters, (booster) => ( const boosterIds = userBoosts.map((boost) => boost.userId!.toString());
[booster.userId.toString(), booster.expires] const boosters = buildCollectionByCallback(userBoosts, (boost) => (
[boost.userId!.toString(), boost.expires]
)); ));
return { return {

View File

@ -3,7 +3,7 @@ import { Api as GramJs, connection } from '../../lib/gramjs';
import type { GroupCallConnectionData } from '../../lib/secret-sauce'; import type { GroupCallConnectionData } from '../../lib/secret-sauce';
import type { import type {
ApiMessage, ApiMessageExtendedMediaPreview, ApiStory, ApiStorySkipped, ApiMessage, ApiMessageExtendedMediaPreview, ApiStory, ApiStorySkipped,
ApiUpdate, ApiUpdateConnectionStateType, OnApiUpdate, ApiUpdate, ApiUpdateConnectionStateType, MediaContent, OnApiUpdate,
} from '../types'; } from '../types';
import { DEBUG, GENERAL_TOPIC_ID } from '../../config'; import { DEBUG, GENERAL_TOPIC_ID } from '../../config';
@ -363,7 +363,7 @@ export function updater(update: Update) {
reactions: buildMessageReactions(update.reactions), reactions: buildMessageReactions(update.reactions),
}); });
} else if (update instanceof GramJs.UpdateMessageExtendedMedia) { } else if (update instanceof GramJs.UpdateMessageExtendedMedia) {
let media: ApiMessage['content'] | undefined; let media: MediaContent | undefined;
if (update.extendedMedia instanceof GramJs.MessageExtendedMedia) { if (update.extendedMedia instanceof GramJs.MessageExtendedMedia) {
media = buildMessageMediaContent(update.extendedMedia.media); media = buildMessageMediaContent(update.extendedMedia.media);
} }
@ -902,7 +902,7 @@ export function updater(update: Update) {
'@type': 'draftMessage', '@type': 'draftMessage',
chatId: getApiChatIdFromMtpPeer(update.peer), chatId: getApiChatIdFromMtpPeer(update.peer),
threadId: update.topMsgId, threadId: update.topMsgId,
...buildMessageDraft(update.draft), draft: buildMessageDraft(update.draft),
}); });
} else if (update instanceof GramJs.UpdateContactsReset) { } else if (update instanceof GramJs.UpdateContactsReset) {
onUpdate({ '@type': 'updateResetContactList' }); onUpdate({ '@type': 'updateResetContactList' });

View File

@ -36,12 +36,14 @@ export interface ApiChat {
avatarHash?: string; avatarHash?: string;
usernames?: ApiUsername[]; usernames?: ApiUsername[];
membersCount?: number; membersCount?: number;
joinDate?: number; creationDate?: number;
isSupport?: true; isSupport?: true;
photos?: ApiPhoto[]; photos?: ApiPhoto[];
draftDate?: number; draftDate?: number;
isProtected?: boolean; isProtected?: boolean;
fakeType?: ApiFakeType; fakeType?: ApiFakeType;
color?: number;
backgroundEmojiId?: string;
isForum?: boolean; isForum?: boolean;
topics?: Record<number, ApiTopic>; topics?: Record<number, ApiTopic>;
listedTopicIds?: number[]; listedTopicIds?: number[];

View File

@ -298,18 +298,42 @@ export interface ApiWebPage {
story?: ApiWebPageStoryData; story?: ApiWebPageStoryData;
} }
export type ApiTypeReplyTo = ApiMessageReplyTo | ApiStoryReplyTo; export type ApiReplyInfo = ApiMessageReplyInfo | ApiStoryReplyInfo;
export interface ApiMessageReplyTo { export interface ApiMessageReplyInfo {
replyingTo: number; type: 'message';
replyingToTopId?: number; 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; userId: string;
storyId: number; 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 { export interface ApiMessageForwardInfo {
date: number; date: number;
isImported?: boolean; isImported?: boolean;
@ -391,36 +415,33 @@ export interface ApiFormattedText {
entities?: ApiMessageEntity[]; 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 { export interface ApiMessage {
id: number; id: number;
chatId: string; chatId: string;
content: { content: 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;
};
date: number; date: number;
isOutgoing: boolean; isOutgoing: boolean;
senderId?: string; senderId?: string;
replyToChatId?: string; replyInfo?: ApiReplyInfo;
replyToMessageId?: number;
replyToTopMessageId?: number;
isTopicReply?: true;
replyToStoryUserId?: string;
replyToStoryId?: number;
sendingState?: 'messageSendingStatePending' | 'messageSendingStateFailed'; sendingState?: 'messageSendingStatePending' | 'messageSendingStateFailed';
forwardInfo?: ApiMessageForwardInfo; forwardInfo?: ApiMessageForwardInfo;
isDeleting?: boolean; isDeleting?: boolean;
@ -657,6 +678,12 @@ export type ApiThemeParameters = {
button_color: string; button_color: string;
button_text_color: string; button_text_color: string;
secondary_bg_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 = { export type ApiBotApp = {

View File

@ -195,6 +195,8 @@ export interface ApiAppConfig {
storyExpirePeriod: number; storyExpirePeriod: number;
storyViewersExpirePeriod: number; storyViewersExpirePeriod: number;
storyChangelogUserId: string; storyChangelogUserId: string;
peerColors: Record<string, string[]>;
darkPeerColors: Record<string, string[]>;
} }
export interface ApiConfig { export interface ApiConfig {

View File

@ -1,6 +1,6 @@
import type { ApiPrivacySettings } from '../../types'; import type { ApiPrivacySettings } from '../../types';
import type { import type {
ApiGeoPoint, ApiMessage, ApiReaction, ApiReactionCount, ApiGeoPoint, ApiReaction, ApiReactionCount, MediaContent,
} from './messages'; } from './messages';
import type { StatisticsOverviewPercentage } from './statistics'; import type { StatisticsOverviewPercentage } from './statistics';
@ -10,7 +10,7 @@ export interface ApiStory {
peerId: string; peerId: string;
date: number; date: number;
expireDate: number; expireDate: number;
content: ApiMessage['content']; content: MediaContent;
isPinned?: boolean; isPinned?: boolean;
isEdited?: boolean; isEdited?: boolean;
isForCloseFriends?: boolean; isForCloseFriends?: boolean;
@ -110,26 +110,6 @@ export type ApiMediaAreaSuggestedReaction = {
export type ApiMediaArea = ApiMediaAreaVenue | ApiMediaAreaGeoPoint | 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 = { export type ApiBoostsStatus = {
level: number; level: number;
currentLevelBoosts: number; currentLevelBoosts: number;
@ -139,3 +119,11 @@ export type ApiBoostsStatus = {
boostUrl: string; boostUrl: string;
premiumSubscribers?: StatisticsOverviewPercentage; premiumSubscribers?: StatisticsOverviewPercentage;
}; };
export type ApiMyBoost = {
slot: number;
chatId?: string;
date: number;
expires: number;
cooldownUntil?: number;
};

View File

@ -1,3 +1,4 @@
import type { ApiDraft } from '../../global/types';
import type { import type {
GroupCallConnectionData, GroupCallConnectionData,
GroupCallConnectionState, GroupCallConnectionState,
@ -27,6 +28,7 @@ import type {
ApiReactions, ApiReactions,
ApiStickerSet, ApiStickerSet,
ApiThreadInfo, ApiThreadInfo,
MediaContent,
} from './messages'; } from './messages';
import type { import type {
ApiEmojiInteraction, ApiError, ApiInviteInfo, ApiNotifyException, ApiSessionData, ApiEmojiInteraction, ApiError, ApiInviteInfo, ApiNotifyException, ApiSessionData,
@ -312,9 +314,7 @@ export type ApiUpdateDraftMessage = {
'@type': 'draftMessage'; '@type': 'draftMessage';
chatId: string; chatId: string;
threadId?: number; threadId?: number;
formattedText?: ApiFormattedText; draft?: ApiDraft;
date?: number;
replyingToId?: number;
}; };
export type ApiUpdateMessageReactions = { export type ApiUpdateMessageReactions = {
@ -328,7 +328,7 @@ export type ApiUpdateMessageExtendedMedia = {
'@type': 'updateMessageExtendedMedia'; '@type': 'updateMessageExtendedMedia';
id: number; id: number;
chatId: string; chatId: string;
media?: ApiMessage['content']; media?: MediaContent;
preview?: ApiMessageExtendedMediaPreview; preview?: ApiMessageExtendedMediaPreview;
}; };

View File

@ -35,6 +35,8 @@ export interface ApiUser {
hasStories?: boolean; hasStories?: boolean;
hasUnreadStories?: boolean; hasUnreadStories?: boolean;
maxStoryId?: number; maxStoryId?: number;
color?: number;
backgroundEmojiId?: string;
} }
export interface ApiUserFullInfo { export interface ApiUserFullInfo {
@ -81,7 +83,6 @@ type ApiAttachBotForMenu = {
type ApiAttachBotBase = { type ApiAttachBotBase = {
id: string; id: string;
hasSettings?: boolean;
shouldRequestWriteAccess?: boolean; shouldRequestWriteAccess?: boolean;
shortName: string; shortName: string;
isForSideMenu?: true; isForSideMenu?: true;

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32"><path d="M.36 10.786a6.951 6.951 0 0 1 13.902 0v2.029c0 3.777-.88 7.502-2.568 10.88l-1.274 2.548a3.476 3.476 0 0 1-6.218-3.109l1.274-2.548c.465-.928.843-1.894 1.133-2.884a6.952 6.952 0 0 1-6.25-6.916Zm17.378 0a6.951 6.951 0 0 1 13.902 0v2.029c0 3.777-.88 7.502-2.568 10.88l-1.274 2.548a3.476 3.476 0 0 1-6.218-3.109l1.274-2.548c.465-.928.843-1.894 1.133-2.884a6.952 6.952 0 0 1-6.25-6.916z"/></svg>

After

Width:  |  Height:  |  Size: 461 B

View File

@ -1,4 +1,4 @@
@import "../../../styles/mixins"; @use "../../../styles/mixins";
.root { .root {
--group-call-panel-color: #212121; --group-call-panel-color: #212121;
@ -74,7 +74,7 @@
border-bottom: 0.0625rem solid transparent; border-bottom: 0.0625rem solid transparent;
padding: 0.375rem 0.875rem; padding: 0.375rem 0.875rem;
@include adapt-padding-to-scrollbar(0.875rem); @include mixins.adapt-padding-to-scrollbar(0.875rem);
user-select: none; user-select: none;
z-index: 1; z-index: 1;
@ -139,7 +139,7 @@
.participants { .participants {
position: relative; position: relative;
margin: 0.125rem 0.5rem 0; margin: 0.125rem 0.5rem 0;
@include adapt-margin-to-scrollbar(0.5rem); @include mixins.adapt-margin-to-scrollbar(0.5rem);
} }
.participantVideos { .participantVideos {

View File

@ -1,4 +1,4 @@
@import '../../../styles/mixins'; @use '../../../styles/mixins';
.participant-menu { .participant-menu {
--color-text: white; --color-text: white;
@ -84,7 +84,7 @@
transition: 0.25s ease-in-out background-color, 0.25s ease-in-out box-shadow; 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 // Apply custom styles
input[type="range"] { input[type="range"] {

View File

@ -14,7 +14,6 @@ import { IS_TEST } from '../../config';
import { import {
getChatAvatarHash, getChatAvatarHash,
getChatTitle, getChatTitle,
getPeerColorKey,
getPeerStoryHtmlId, getPeerStoryHtmlId,
getUserFullName, getUserFullName,
isChatWithRepliesBot, isChatWithRepliesBot,
@ -23,6 +22,7 @@ import {
} from '../../global/helpers'; } from '../../global/helpers';
import buildClassName, { createClassNameBuilder } from '../../util/buildClassName'; import buildClassName, { createClassNameBuilder } from '../../util/buildClassName';
import { getFirstLetters } from '../../util/textFormat'; import { getFirstLetters } from '../../util/textFormat';
import { getPeerColorClass } from './helpers/peerColor';
import renderText from './helpers/renderText'; import renderText from './helpers/renderText';
import { useFastClick } from '../../hooks/useFastClick'; import { useFastClick } from '../../hooks/useFastClick';
@ -210,7 +210,7 @@ const Avatar: FC<OwnProps> = ({
const fullClassName = buildClassName( const fullClassName = buildClassName(
`Avatar size-${size}`, `Avatar size-${size}`,
className, className,
`color-bg-${getPeerColorKey(peer)}`, getPeerColorClass(peer),
isSavedMessages && 'saved-messages', isSavedMessages && 'saved-messages',
isDeleted && 'deleted-account', isDeleted && 'deleted-account',
isReplies && 'replies-bot-account', isReplies && 'replies-bot-account',

View File

@ -70,7 +70,6 @@ import {
selectIsRightColumnShown, selectIsRightColumnShown,
selectNewestMessageWithBotKeyboardButtons, selectNewestMessageWithBotKeyboardButtons,
selectPeerStory, selectPeerStory,
selectReplyingToId,
selectRequestedDraftFiles, selectRequestedDraftFiles,
selectRequestedDraftText, selectRequestedDraftText,
selectScheduledIds, selectScheduledIds,
@ -188,7 +187,6 @@ type StateProps =
isChatWithBot?: boolean; isChatWithBot?: boolean;
isChatWithSelf?: boolean; isChatWithSelf?: boolean;
isChannel?: boolean; isChannel?: boolean;
replyingToId?: number;
isForCurrentMessageList: boolean; isForCurrentMessageList: boolean;
isRightColumnShown?: boolean; isRightColumnShown?: boolean;
isSelectModeActive?: boolean; isSelectModeActive?: boolean;
@ -318,7 +316,6 @@ const Composer: FC<OwnProps & StateProps> = ({
sendAsChat, sendAsChat,
sendAsId, sendAsId,
editingDraft, editingDraft,
replyingToId,
requestedDraftText, requestedDraftText,
requestedDraftFiles, requestedDraftFiles,
botMenuButton, botMenuButton,
@ -695,7 +692,6 @@ const Composer: FC<OwnProps & StateProps> = ({
messageListType, messageListType,
draft, draft,
editingDraft, editingDraft,
replyingToId,
); );
// Handle chat change (should be placed after `useDraft` and `useEditing`) // Handle chat change (should be placed after `useDraft` and `useEditing`)
@ -890,7 +886,7 @@ const Composer: FC<OwnProps & StateProps> = ({
lastMessageSendTimeSeconds.current = getServerTime(); lastMessageSendTimeSeconds.current = getServerTime();
clearDraft({ chatId, localOnly: true }); clearDraft({ chatId, isLocalOnly: true });
// Wait until message animation starts // Wait until message animation starts
requestMeasure(() => { requestMeasure(() => {
@ -971,7 +967,7 @@ const Composer: FC<OwnProps & StateProps> = ({
lastMessageSendTimeSeconds.current = getServerTime(); lastMessageSendTimeSeconds.current = getServerTime();
clearDraft({ chatId, localOnly: true }); clearDraft({ chatId, isLocalOnly: true });
if (IS_IOS && messageInput && messageInput === document.activeElement) { if (IS_IOS && messageInput && messageInput === document.activeElement) {
applyIosAutoCapitalizationFix(messageInput); applyIosAutoCapitalizationFix(messageInput);
@ -1158,14 +1154,14 @@ const Composer: FC<OwnProps & StateProps> = ({
applyIosAutoCapitalizationFix(messageInput); applyIosAutoCapitalizationFix(messageInput);
} }
clearDraft({ chatId, localOnly: true }); clearDraft({ chatId, isLocalOnly: true });
requestMeasure(() => { requestMeasure(() => {
resetComposer(); resetComposer();
}); });
}); });
const handleBotCommandSelect = useLastCallback(() => { const handleBotCommandSelect = useLastCallback(() => {
clearDraft({ chatId, localOnly: true }); clearDraft({ chatId, isLocalOnly: true });
requestMeasure(() => { requestMeasure(() => {
resetComposer(); resetComposer();
}); });
@ -1930,8 +1926,6 @@ export default memo(withGlobal<OwnProps>(
? selectEditingScheduledDraft(global, chatId) ? selectEditingScheduledDraft(global, chatId)
: selectEditingDraft(global, chatId, threadId); : selectEditingDraft(global, chatId, threadId);
const replyingToId = selectReplyingToId(global, chatId, threadId);
const story = storyId && selectPeerStory(global, chatId, storyId); const story = storyId && selectPeerStory(global, chatId, storyId);
const sentStoryReaction = story && 'sentReaction' in story ? story.sentReaction : undefined; const sentStoryReaction = story && 'sentReaction' in story ? story.sentReaction : undefined;
@ -1940,7 +1934,6 @@ export default memo(withGlobal<OwnProps>(
topReactions: type === 'story' ? global.topReactions : undefined, topReactions: type === 'story' ? global.topReactions : undefined,
isOnActiveTab: !tabState.isBlurred, isOnActiveTab: !tabState.isBlurred,
editingMessage: selectEditingMessage(global, chatId, threadId, messageListType), editingMessage: selectEditingMessage(global, chatId, threadId, messageListType),
replyingToId,
draft: selectDraft(global, chatId, threadId), draft: selectDraft(global, chatId, threadId),
chat, chat,
isChatWithBot, isChatWithBot,

View File

@ -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<OwnProps> = ({
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<HTMLDivElement>(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 (
<div
ref={ref}
className={buildClassName(
'EmbeddedMessage',
className,
sender && !noUserColors && `color-${getPeerColorKey(sender)}`,
)}
onClick={message && handleClick}
onMouseDown={message && handleMouseDown}
>
{mediaThumbnail && renderPictogram(mediaThumbnail, mediaBlobUrl, isRoundVideo, isProtected, isSpoiler)}
<div className="message-text">
<p dir="auto">
{!message ? (
customText || NBSP
) : isActionMessage(message) ? (
<ActionMessage
message={message}
isEmbedded
observeIntersectionForLoading={observeIntersectionForLoading}
observeIntersectionForPlaying={observeIntersectionForPlaying}
/>
) : (
<MessageSummary
lang={lang}
message={message}
noEmoji={Boolean(mediaThumbnail)}
translatedText={translatedText}
observeIntersectionForLoading={observeIntersectionForLoading}
observeIntersectionForPlaying={observeIntersectionForPlaying}
/>
)}
</p>
<div className="message-title" dir="auto">
{renderText(senderTitle || title || NBSP)}
{forwardSenderTitle && !areSendersSame && (
<>
<Icon name={forwardSender ? 'share-filled' : 'forward'} className="embedded-origin-icon" />
{renderText(forwardSenderTitle)}
</>
)}
</div>
</div>
{hasContextMenu && <Icon name="more" className="embedded-more" />}
</div>
);
};
function renderPictogram(
thumbDataUri: string,
blobUrl?: string,
isRoundVideo?: boolean,
isProtected?: boolean,
isSpoiler?: boolean,
) {
const { width, height } = getPictogramDimensions();
const srcUrl = blobUrl || thumbDataUri;
return (
<div className={buildClassName('embedded-thumb', isRoundVideo && 'round')}>
{!isSpoiler && (
<img
src={srcUrl}
width={width}
height={height}
alt=""
className="pictogram"
draggable={false}
/>
)}
<MediaSpoiler thumbDataUri={srcUrl} isVisible={Boolean(isSpoiler)} width={width} height={height} />
{isProtected && <span className="protector" />}
</div>
);
}
export default EmbeddedMessage;

View File

@ -1,10 +1,10 @@
@import "../../styles/mixins"; @use "../../styles/mixins";
.container { .container {
padding: 1.5rem 1.5rem 0; padding: 1.5rem 1.5rem 0;
margin-bottom: 0.625rem; margin-bottom: 0.625rem;
@include side-panel-section; @include mixins.side-panel-section;
} }
.header { .header {

View File

@ -6,7 +6,6 @@ import type { ApiChat, ApiPhoto, ApiUser } from '../../api/types';
import { import {
getChatAvatarHash, getChatAvatarHash,
getChatTitle, getChatTitle,
getPeerColorKey,
getUserFullName, getUserFullName,
getVideoAvatarMediaHash, getVideoAvatarMediaHash,
isChatWithRepliesBot, isChatWithRepliesBot,
@ -16,6 +15,7 @@ import {
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import { getFirstLetters } from '../../util/textFormat'; import { getFirstLetters } from '../../util/textFormat';
import { IS_CANVAS_FILTER_SUPPORTED } from '../../util/windowEnvironment'; import { IS_CANVAS_FILTER_SUPPORTED } from '../../util/windowEnvironment';
import { getPeerColorClass } from './helpers/peerColor';
import renderText from './helpers/renderText'; import renderText from './helpers/renderText';
import useAppLayout from '../../hooks/useAppLayout'; import useAppLayout from '../../hooks/useAppLayout';
@ -55,11 +55,11 @@ const ProfilePhoto: FC<OwnProps> = ({
const isDeleted = user && isDeletedUser(user); const isDeleted = user && isDeletedUser(user);
const isRepliesChat = chat && isChatWithRepliesBot(chat.id); const isRepliesChat = chat && isChatWithRepliesBot(chat.id);
const userOrChat = user || chat; const peer = user || chat;
const canHaveMedia = userOrChat && !isSavedMessages && !isDeleted && !isRepliesChat; const canHaveMedia = peer && !isSavedMessages && !isDeleted && !isRepliesChat;
const { isVideo } = photo || {}; const { isVideo } = photo || {};
const avatarHash = canHaveMedia && getChatAvatarHash(userOrChat, 'normal'); const avatarHash = canHaveMedia && getChatAvatarHash(peer, 'normal');
const avatarBlobUrl = useMedia(avatarHash); const avatarBlobUrl = useMedia(avatarHash);
const photoHash = canHaveMedia && photo && !isVideo && `photo${photo.id}?size=c`; const photoHash = canHaveMedia && photo && !isVideo && `photo${photo.id}?size=c`;
@ -139,7 +139,7 @@ const ProfilePhoto: FC<OwnProps> = ({
const fullClassName = buildClassName( const fullClassName = buildClassName(
'ProfilePhoto', 'ProfilePhoto',
`color-bg-${getPeerColorKey(user || chat)}`, getPeerColorClass(peer),
isSavedMessages && 'saved-messages', isSavedMessages && 'saved-messages',
isDeleted && 'deleted-account', isDeleted && 'deleted-account',
isRepliesChat && 'replies-bot-account', isRepliesChat && 'replies-bot-account',

View File

@ -1,7 +1,3 @@
:root {
--thumbs-background: var(--color-background);
}
.thumb { .thumb {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -13,7 +9,6 @@
} }
.thumb-opaque { .thumb-opaque {
background: var(--thumbs-background);
transition-delay: 0s; transition-delay: 0s;
} }

View File

@ -1,21 +1,22 @@
@use "sass:map";
@use "../../../styles/mixins";
@use "../../../styles/icons";
.EmbeddedMessage { .EmbeddedMessage {
display: flex; display: flex;
align-items: center; align-items: center;
font-size: calc(var(--message-text-size, 1rem) - 0.125rem); font-size: calc(var(--message-text-size, 1rem) - 0.125rem);
line-height: 1.125rem; line-height: 1.125rem;
margin: 0 -0.25rem 0.0625rem; margin-bottom: 0.0625rem;
padding: 0.1875rem 0.25rem 0.1875rem 0.4375rem; padding: 0.1875rem 0.375rem 0.1875rem 0.1875rem;
border-radius: var(--border-radius-messages-small); border-radius: var(--border-radius-messages-small);
position: relative; position: relative;
overflow: hidden; overflow: hidden;
cursor: var(--custom-cursor, pointer); cursor: var(--custom-cursor, pointer);
direction: ltr;
@for $i from 1 through 8 { background-color: var(--accent-background-color);
&.color-#{$i} {
--accent-color: var(--color-user-#{$i}); transition: background-color 0.2s ease-in;
}
}
body.no-page-transitions & { body.no-page-transitions & {
.ripple-container { .ripple-container {
@ -25,17 +26,10 @@
.custom-shape & { .custom-shape & {
max-width: 15rem; max-width: 15rem;
padding: 0.5rem;
margin: 0; margin: 0;
background-color: var(--background-color); background-color: var(--color-reply-active);
box-shadow: 0 1px 2px var(--color-default-shadow); box-shadow: 0 1px 2px var(--color-default-shadow);
&::before {
left: 0.625rem;
top: 0.625rem;
bottom: 0.625rem;
}
.embedded-thumb { .embedded-thumb {
margin-inline-start: 0.5rem; margin-inline-start: 0.5rem;
} }
@ -49,38 +43,62 @@
content: ""; content: "";
display: block; display: block;
position: absolute; position: absolute;
top: 0.3125rem; top: 0;
bottom: 0.3125rem; bottom: 0;
left: 0.375rem; inset-inline-start: 0;
width: 2px; width: 3px;
background: var(--accent-color); background: var(--bar-gradient, var(--accent-color));
border-radius: 2px;
}
&:hover {
background-color: var(--hover-color);
} }
&:active { &: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 { .message-title {
font-size: calc(var(--message-text-size, 1rem) - 0.125rem); font-size: calc(var(--message-text-size, 1rem) - 0.125rem);
} }
.embedded-more { .embedded-origin-icon {
font-size: 1.125rem; margin-inline: 0.125rem;
margin-inline-end: 0.125rem; vertical-align: middle;
line-height: 0.9375rem; line-height: 1.25;
vertical-align: -0.1875rem;
} }
.embedded-origin-icon { .embedded-chat-icon {
display: inline-block;
font-size: 0.75rem; font-size: 0.75rem;
margin-inline: 0.125rem; vertical-align: middle;
transform: translateY(1px);
} }
.message-text { .message-text {
@ -90,15 +108,20 @@
flex-direction: column-reverse; flex-direction: column-reverse;
.message-title { .message-title {
display: flex;
align-items: center;
flex-wrap: wrap;
flex: 1;
column-gap: 0.25rem;
}
.message-title, .embedded-sender {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
margin-bottom: 0.125rem;
flex: 1;
display: block;
} }
p { .embedded-text-wrapper {
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;
@ -139,7 +162,9 @@
} }
.embedded-thumb { .embedded-thumb {
position: relative; position: absolute;
top: 0.375rem;
inset-inline-start: 0.375rem;
width: 2rem; width: 2rem;
height: 2rem; height: 2rem;
border-radius: 0.25rem; border-radius: 0.25rem;
@ -159,16 +184,13 @@
object-fit: cover; object-fit: cover;
} }
&--background-icons {
color: var(--accent-color);
}
&.inside-input { &.inside-input {
padding-inline-start: 0.5625rem; padding-inline-start: 0.5625rem;
width: 100%; 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 { .embedded-thumb {
margin-left: 0.125rem; margin-left: 0.125rem;
@ -183,11 +205,5 @@
font-weight: 500; font-weight: 500;
color: var(--accent-color); color: var(--accent-color);
} }
.embedded-more {
font-size: 1.5rem;
opacity: 0.8;
color: var(--color-text-secondary);
}
} }
} }

View File

@ -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<OwnProps> = ({
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<HTMLDivElement>(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 (
<ActionMessage
message={message}
isEmbedded
observeIntersectionForLoading={observeIntersectionForLoading}
observeIntersectionForPlaying={observeIntersectionForPlaying}
/>
);
}
return (
<MessageSummary
lang={lang}
message={message}
noEmoji={Boolean(mediaThumbnail)}
translatedText={translatedText}
observeIntersectionForLoading={observeIntersectionForLoading}
observeIntersectionForPlaying={observeIntersectionForPlaying}
/>
);
}
function renderSender() {
if (forwardSenderTitle && !areSendersSame) {
return (
<>
<Icon name={forwardSender ? 'share-filled' : 'forward'} className="embedded-origin-icon" />
{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 && <span className="embedded-sender">{renderText(senderTitle)}</span>}
{icon && <Icon name={icon} className="embedded-chat-icon" />}
{senderChatTitle && renderText(senderChatTitle)}
</>
);
}
return (
<div
ref={ref}
className={buildClassName(
'EmbeddedMessage',
className,
getPeerColorClass(sender, noUserColors, true),
isQuote && 'is-quote',
mediaThumbnail && 'with-thumb',
)}
dir={lang.isRtl ? 'rtl' : undefined}
onClick={handleClick}
onMouseDown={handleMouseDown}
>
{mediaThumbnail && renderPictogram(mediaThumbnail, mediaBlobUrl, isRoundVideo, isProtected, isSpoiler)}
{sender?.backgroundEmojiId && (
<EmojiIconBackground emojiDocumentId={sender.backgroundEmojiId} className="EmbeddedMessage--background-icons" />
)}
<div className="message-text">
<p className="embedded-text-wrapper">
{renderTextContent()}
</p>
<div className="message-title">
{renderSender()}
</div>
</div>
</div>
);
};
function renderPictogram(
thumbDataUri: string,
blobUrl?: string,
isRoundVideo?: boolean,
isProtected?: boolean,
isSpoiler?: boolean,
) {
const { width, height } = getPictogramDimensions();
const srcUrl = blobUrl || thumbDataUri;
return (
<div className={buildClassName('embedded-thumb', isRoundVideo && 'round')}>
{!isSpoiler && (
<img
src={srcUrl}
width={width}
height={height}
alt=""
className="pictogram"
draggable={false}
/>
)}
<MediaSpoiler thumbDataUri={srcUrl} isVisible={Boolean(isSpoiler)} width={width} height={height} />
{isProtected && <span className="protector" />}
</div>
);
}
export default EmbeddedMessage;

View File

@ -1,24 +1,26 @@
import type { FC } from '../../lib/teact/teact'; import type { FC } from '../../../lib/teact/teact';
import React, { useRef } from '../../lib/teact/teact'; import React, { useRef } from '../../../lib/teact/teact';
import { getActions } from '../../global'; import { getActions } from '../../../global';
import type { ApiPeer, ApiTypeStory } from '../../api/types'; import type { ApiPeer, ApiTypeStory } from '../../../api/types';
import type { ObserveFn } from '../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import { import {
getPeerColorKey,
getSenderTitle, getSenderTitle,
getStoryMediaHash, getStoryMediaHash,
} from '../../global/helpers'; } from '../../../global/helpers';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { getPictogramDimensions } from './helpers/mediaDimensions'; import { getPictogramDimensions } from '../helpers/mediaDimensions';
import renderText from './helpers/renderText'; import { getPeerColorClass } from '../helpers/peerColor';
import renderText from '../helpers/renderText';
import { useFastClick } from '../../hooks/useFastClick'; import { useFastClick } from '../../../hooks/useFastClick';
import { useIsIntersecting } from '../../hooks/useIntersectionObserver'; import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
import useLang from '../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
import useMedia from '../../hooks/useMedia'; import useMedia from '../../../hooks/useMedia';
import Icon from '../Icon';
import './EmbeddedMessage.scss'; import './EmbeddedMessage.scss';
@ -75,23 +77,24 @@ const EmbeddedStory: FC<OwnProps> = ({
ref={ref} ref={ref}
className={buildClassName( className={buildClassName(
'EmbeddedMessage', 'EmbeddedMessage',
sender && !noUserColors && `color-${getPeerColorKey(sender)}`, getPeerColorClass(sender, noUserColors, true),
pictogramUrl && 'with-thumb',
)} )}
onClick={handleClick} onClick={handleClick}
onMouseDown={handleMouseDown} onMouseDown={handleMouseDown}
> >
{pictogramUrl && renderPictogram(pictogramUrl, isProtected)} {pictogramUrl && renderPictogram(pictogramUrl, isProtected)}
<div className="message-text with-message-color"> <div className="message-text with-message-color">
<p dir="auto"> <p className="embedded-text-wrapper">
{isExpiredStory && ( {isExpiredStory && (
<i className="icon icon-story-expired" aria-hidden /> <Icon name="story-expired" className="embedded-origin-icon" />
)} )}
{isFullStory && ( {isFullStory && (
<i className="icon icon-story-reply" aria-hidden /> <Icon name="story-reply" className="embedded-origin-icon" />
)} )}
{lang(title)} {lang(title)}
</p> </p>
<div className="message-title" dir="auto">{renderText(senderTitle || NBSP)}</div> <div className="message-title">{renderText(senderTitle || NBSP)}</div>
</div> </div>
</div> </div>
); );

View File

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

View File

@ -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 (
<div className={buildClassName(styles.root, className)}>
{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 (
<CustomEmoji
documentId={emojiDocumentId}
className={styles.emoji}
noPlay
style={style}
/>
);
})}
</div>
);
};
export default memo(EmojiIconBackground);

View File

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

View File

@ -391,7 +391,13 @@ function processEntity({
case ApiMessageEntityTypes.Bold: case ApiMessageEntityTypes.Bold:
return <strong data-entity-type={entity.type}>{renderNestedMessagePart()}</strong>; return <strong data-entity-type={entity.type}>{renderNestedMessagePart()}</strong>;
case ApiMessageEntityTypes.Blockquote: case ApiMessageEntityTypes.Blockquote:
return <blockquote data-entity-type={entity.type}>{renderNestedMessagePart()}</blockquote>; return (
<div className="message-entity-blockquote-wrapper">
<blockquote data-entity-type={entity.type}>
{renderNestedMessagePart()}
</blockquote>
</div>
);
case ApiMessageEntityTypes.BotCommand: case ApiMessageEntityTypes.BotCommand:
return ( return (
<a <a

View File

@ -4,7 +4,6 @@ import { getActions, withGlobal } from '../../../global';
import type { import type {
ApiChat, ApiChat,
ApiFormattedText,
ApiMessage, ApiMessage,
ApiMessageOutgoingStatus, ApiMessageOutgoingStatus,
ApiPeer, ApiPeer,
@ -13,6 +12,7 @@ import type {
ApiUser, ApiUser,
ApiUserStatus, ApiUserStatus,
} from '../../../api/types'; } from '../../../api/types';
import type { ApiDraft } from '../../../global/types';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import type { ChatAnimationTypes } from './hooks'; import type { ChatAnimationTypes } from './hooks';
import { MAIN_THREAD_ID } from '../../../api/types'; import { MAIN_THREAD_ID } from '../../../api/types';
@ -25,6 +25,7 @@ import {
isUserOnline, isUserOnline,
selectIsChatMuted, selectIsChatMuted,
} from '../../../global/helpers'; } from '../../../global/helpers';
import { getMessageReplyInfo } from '../../../global/helpers/replies';
import { import {
selectCanAnimateInterface, selectCanAnimateInterface,
selectChat, selectChat,
@ -89,7 +90,7 @@ type StateProps = {
actionTargetChatId?: string; actionTargetChatId?: string;
lastMessageSender?: ApiPeer; lastMessageSender?: ApiPeer;
lastMessageOutgoingStatus?: ApiMessageOutgoingStatus; lastMessageOutgoingStatus?: ApiMessageOutgoingStatus;
draft?: ApiFormattedText; draft?: ApiDraft;
isSelected?: boolean; isSelected?: boolean;
isSelectedForum?: boolean; isSelectedForum?: boolean;
isForumPanelOpen?: boolean; isForumPanelOpen?: boolean;
@ -344,10 +345,12 @@ export default memo(withGlobal<OwnProps>(
return {}; return {};
} }
const { senderId, replyToMessageId, isOutgoing } = chat.lastMessage || {}; const { lastMessage } = chat;
const { senderId, isOutgoing } = lastMessage || {};
const replyToMessageId = lastMessage && getMessageReplyInfo(lastMessage)?.replyToMsgId;
const lastMessageSender = senderId const lastMessageSender = senderId
? (selectUser(global, senderId) || selectChat(global, senderId)) : undefined; ? (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 const actionTargetMessage = lastMessageAction && replyToMessageId
? selectChatMessage(global, chat.id, replyToMessageId) ? selectChatMessage(global, chat.id, replyToMessageId)
: undefined; : undefined;
@ -364,7 +367,7 @@ export default memo(withGlobal<OwnProps>(
const user = privateChatUserId ? selectUser(global, privateChatUserId) : undefined; const user = privateChatUserId ? selectUser(global, privateChatUserId) : undefined;
const userStatus = privateChatUserId ? selectUserStatus(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'); const typingStatus = selectThreadParam(global, chatId, MAIN_THREAD_ID, 'typingStatus');
@ -381,8 +384,8 @@ export default memo(withGlobal<OwnProps>(
isForumPanelOpen: selectIsForumPanelOpen(global), isForumPanelOpen: selectIsForumPanelOpen(global),
canScrollDown: isSelected && messageListType === 'thread', canScrollDown: isSelected && messageListType === 'thread',
canChangeFolder: (global.chatFolders.orderedIds?.length || 0) > 1, canChangeFolder: (global.chatFolders.orderedIds?.length || 0) > 1,
...(isOutgoing && chat.lastMessage && { ...(isOutgoing && lastMessage && {
lastMessageOutgoingStatus: selectOutgoingStatus(global, chat.lastMessage), lastMessageOutgoingStatus: selectOutgoingStatus(global, lastMessage),
}), }),
user, user,
userStatus, userStatus,

View File

@ -1,4 +1,4 @@
@import "../../../styles/mixins"; @use "../../../styles/mixins";
#LeftMainHeader { #LeftMainHeader {
position: relative; position: relative;
@ -123,7 +123,7 @@
} }
// @optimization // @optimization
@include while-transition() { @include mixins.while-transition() {
.Menu .bubble { .Menu .bubble {
transition: none !important; transition: none !important;
} }

View File

@ -158,6 +158,7 @@ const LeftSideMenuItems = ({
bot={bot} bot={bot}
theme={theme} theme={theme}
isInSideMenu isInSideMenu
canShowNew
onMenuOpened={onBotMenuOpened} onMenuOpened={onBotMenuOpened}
onMenuClosed={onBotMenuClosed} onMenuClosed={onBotMenuClosed}
/> />

View File

@ -3,13 +3,15 @@ import React, { memo } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import type { import type {
ApiChat, ApiFormattedText, ApiMessage, ApiMessageOutgoingStatus, ApiChat, ApiMessage, ApiMessageOutgoingStatus,
ApiPeer, ApiTopic, ApiTypingStatus, ApiPeer, ApiTopic, ApiTypingStatus,
} from '../../../api/types'; } from '../../../api/types';
import type { ApiDraft } from '../../../global/types';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import type { ChatAnimationTypes } from './hooks'; import type { ChatAnimationTypes } from './hooks';
import { getMessageAction } from '../../../global/helpers'; import { getMessageAction } from '../../../global/helpers';
import { getMessageReplyInfo } from '../../../global/helpers/replies';
import { import {
selectCanAnimateInterface, selectCanAnimateInterface,
selectCanDeleteTopic, selectCanDeleteTopic,
@ -48,7 +50,6 @@ type OwnProps = {
isSelected: boolean; isSelected: boolean;
style: string; style: string;
observeIntersection?: ObserveFn; observeIntersection?: ObserveFn;
orderDiff: number; orderDiff: number;
animationType: ChatAnimationTypes; animationType: ChatAnimationTypes;
}; };
@ -63,7 +64,7 @@ type StateProps = {
lastMessageSender?: ApiPeer; lastMessageSender?: ApiPeer;
actionTargetChatId?: string; actionTargetChatId?: string;
typingStatus?: ApiTypingStatus; typingStatus?: ApiTypingStatus;
draft?: ApiFormattedText; draft?: ApiDraft;
canScrollDown?: boolean; canScrollDown?: boolean;
wasTopicOpened?: boolean; wasTopicOpened?: boolean;
withInterfaceAnimations?: boolean; withInterfaceAnimations?: boolean;
@ -232,8 +233,9 @@ export default memo(withGlobal<OwnProps>(
(global, { chatId, topic, isSelected }) => { (global, { chatId, topic, isSelected }) => {
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
const lastMessage = selectChatMessage(global, chatId, topic.lastMessageId)!; const lastMessage = selectChatMessage(global, chatId, topic.lastMessageId);
const { senderId, replyToMessageId, isOutgoing } = lastMessage || {}; const { senderId, isOutgoing } = lastMessage || {};
const replyToMessageId = lastMessage && getMessageReplyInfo(lastMessage)?.replyToMsgId;
const lastMessageSender = senderId const lastMessageSender = senderId
? (selectUser(global, senderId) || selectChat(global, senderId)) : undefined; ? (selectUser(global, senderId) || selectChat(global, senderId)) : undefined;
const lastMessageAction = lastMessage ? getMessageAction(lastMessage) : undefined; const lastMessageAction = lastMessage ? getMessageAction(lastMessage) : undefined;

View File

@ -6,7 +6,7 @@ import { getGlobal } from '../../../../global';
import type { import type {
ApiChat, ApiMessage, ApiPeer, ApiTopic, ApiTypingStatus, ApiUser, ApiChat, ApiMessage, ApiPeer, ApiTopic, ApiTypingStatus, ApiUser,
} from '../../../../api/types'; } from '../../../../api/types';
import type { Thread } from '../../../../global/types'; import type { ApiDraft } from '../../../../global/types';
import type { ObserveFn } from '../../../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../../../hooks/useIntersectionObserver';
import type { LangFn } from '../../../../hooks/useLang'; import type { LangFn } from '../../../../hooks/useLang';
@ -23,6 +23,7 @@ import {
isActionMessage, isActionMessage,
isChatChannel, isChatChannel,
} from '../../../../global/helpers'; } from '../../../../global/helpers';
import { getMessageReplyInfo } from '../../../../global/helpers/replies';
import buildClassName from '../../../../util/buildClassName'; import buildClassName from '../../../../util/buildClassName';
import { renderActionMessageText } from '../../../common/helpers/renderActionMessageText'; import { renderActionMessageText } from '../../../common/helpers/renderActionMessageText';
import renderText from '../../../common/helpers/renderText'; import renderText from '../../../common/helpers/renderText';
@ -60,7 +61,7 @@ export default function useChatListEntry({
lastMessage?: ApiMessage; lastMessage?: ApiMessage;
chatId: string; chatId: string;
typingStatus?: ApiTypingStatus; typingStatus?: ApiTypingStatus;
draft?: Thread['draft']; draft?: ApiDraft;
actionTargetMessage?: ApiMessage; actionTargetMessage?: ApiMessage;
actionTargetUserIds?: string[]; actionTargetUserIds?: string[];
lastMessageTopic?: ApiTopic; lastMessageTopic?: ApiTopic;
@ -79,7 +80,8 @@ export default function useChatListEntry({
const isAction = lastMessage && isActionMessage(lastMessage); 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) const mediaThumbnail = lastMessage && !getMessageSticker(lastMessage)
? getMessageMediaThumbDataUri(lastMessage) ? getMessageMediaThumbDataUri(lastMessage)
@ -102,13 +104,15 @@ export default function useChatListEntry({
return <TypingStatus typingStatus={typingStatus} />; return <TypingStatus typingStatus={typingStatus} />;
} }
if (draft?.text.length && (!chat?.isForum || isTopic)) { const isDraftReplyToTopic = draft && draft.replyInfo?.replyToMsgId === lastMessageTopic?.id;
if (draft && (!chat?.isForum || (isTopic && !isDraftReplyToTopic))) {
return ( return (
<p className="last-message" dir={lang.isRtl ? 'auto' : 'ltr'}> <p className="last-message" dir={lang.isRtl ? 'auto' : 'ltr'}>
<span className="draft">{lang('Draft')}</span> <span className="draft">{lang('Draft')}</span>
{renderTextWithEntities({ {renderTextWithEntities({
text: draft.text, text: draft.text?.text || '',
entities: draft.entities, entities: draft.text?.entities,
isSimple: true, isSimple: true,
withTranslucentThumbs: true, withTranslucentThumbs: true,
})} })}
@ -153,7 +157,7 @@ export default function useChatListEntry({
</> </>
)} )}
{lastMessage.forwardInfo && (<i className="icon icon-share-filled chat-prefix-icon" />)} {lastMessage.forwardInfo && (<i className="icon icon-share-filled chat-prefix-icon" />)}
{Boolean(lastMessage.replyToStoryId) && (<i className="icon icon-story-reply chat-prefix-icon" />)} {lastMessage.replyInfo?.type === 'story' && (<i className="icon icon-story-reply chat-prefix-icon" />)}
{renderSummary(lang, lastMessage, observeIntersection, mediaBlobUrl || mediaThumbnail, isRoundVideo)} {renderSummary(lang, lastMessage, observeIntersection, mediaBlobUrl || mediaThumbnail, isRoundVideo)}
</p> </p>
); );

View File

@ -1,4 +1,4 @@
@import "../../../styles/mixins"; @use "../../../styles/mixins";
#Settings { #Settings {
height: 100%; height: 100%;
@ -82,7 +82,7 @@
text-align: center; text-align: center;
margin-bottom: 0.625rem; margin-bottom: 0.625rem;
@include side-panel-section; @include mixins.side-panel-section;
&.no-border { &.no-border {
margin-bottom: 0; margin-bottom: 0;
@ -109,12 +109,12 @@
.settings-main-menu { .settings-main-menu {
padding: 0.5rem; padding: 0.5rem;
@include side-panel-section; @include mixins.side-panel-section;
> .ChatExtra { > .ChatExtra {
padding: 0 0.5rem 0.3125rem; padding: 0 0.5rem 0.3125rem;
margin: 0 -0.5rem 0.625rem; margin: 0 -0.5rem 0.625rem;
@include side-panel-section; @include mixins.side-panel-section;
.ListItem.narrow { .ListItem.narrow {
margin-bottom: 0.25rem; margin-bottom: 0.25rem;
@ -125,7 +125,7 @@
.settings-item-simple, .settings-item-simple,
.settings-item { .settings-item {
padding: 1.5rem 1.5rem 1rem; padding: 1.5rem 1.5rem 1rem;
@include side-panel-section; @include mixins.side-panel-section;
} }
.settings-item { .settings-item {

View File

@ -1,4 +1,4 @@
@import "../../../styles/mixins"; @use "../../../styles/mixins";
.SettingsGeneralBackground { .SettingsGeneralBackground {
.settings-wallpapers { .settings-wallpapers {
@ -6,7 +6,7 @@
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
grid-auto-rows: 1fr; grid-auto-rows: 1fr;
grid-gap: 0.0625rem; grid-gap: 0.0625rem;
@include side-panel-section; @include mixins.side-panel-section;
} }
.Loading { .Loading {

View File

@ -1,4 +1,4 @@
@import "../../../styles/mixins"; @use "../../../styles/mixins";
.SettingsGeneralBackgroundColor { .SettingsGeneralBackgroundColor {
&:not(.is-dragging) .handle { &:not(.is-dragging) .handle {
@ -71,7 +71,7 @@
grid-template-columns: repeat(3, 1fr); grid-template-columns: repeat(3, 1fr);
grid-auto-rows: 1fr; grid-auto-rows: 1fr;
grid-gap: 0.0625rem; grid-gap: 0.0625rem;
@include side-panel-section; @include mixins.side-panel-section;
} }
.predefined-color { .predefined-color {

View File

@ -1,4 +1,4 @@
@import '../../../styles/mixins'; @use '../../../styles/mixins';
.root { .root {
--premium-gradient: linear-gradient(88.39deg, #6C93FF -2.56%, #976FFF 51.27%, #DF69D1 107.39%); --premium-gradient: linear-gradient(88.39deg, #6C93FF -2.56%, #976FFF 51.27%, #DF69D1 107.39%);
@ -39,7 +39,7 @@
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
@include adapt-padding-to-scrollbar(0.5rem); @include mixins.adapt-padding-to-scrollbar(0.5rem);
} }
.logo { .logo {

View File

@ -13,6 +13,7 @@ import type { FocusDirection } from '../../types';
import type { PinnedIntersectionChangedCallback } from './hooks/usePinnedMessage'; import type { PinnedIntersectionChangedCallback } from './hooks/usePinnedMessage';
import { getMessageHtmlId, isChatChannel } from '../../global/helpers'; import { getMessageHtmlId, isChatChannel } from '../../global/helpers';
import { getMessageReplyInfo } from '../../global/helpers/replies';
import { import {
selectCanPlayAnimatedEmojis, selectCanPlayAnimatedEmojis,
selectChat, selectChat,
@ -102,7 +103,11 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
useOnIntersect(ref, observeIntersectionForReading); 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); useFocusMessage(ref, message.chatId, isFocused, focusDirection, noFocusHighlight, isJustAdded);
useEffect(() => { useEffect(() => {
@ -263,12 +268,12 @@ const ActionMessage: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global, { message, threadId }): StateProps => { (global, { message, threadId }): StateProps => {
const { const {
chatId, senderId, replyToMessageId, content, chatId, senderId, content,
} = message; } = message;
const userId = senderId; const userId = senderId;
const { targetUserIds, targetChatId } = content.action || {}; const { targetUserIds, targetChatId } = content.action || {};
const targetMessageId = replyToMessageId; const targetMessageId = getMessageReplyInfo(message)?.replyToMsgId;
const targetMessage = targetMessageId const targetMessage = targetMessageId
? selectChatMessage(global, chatId, targetMessageId) ? selectChatMessage(global, chatId, targetMessageId)
: undefined; : undefined;

View File

@ -1,4 +1,4 @@
@import "../../styles/mixins"; @use "../../styles/mixins";
.root { .root {
display: flex; display: flex;
@ -247,7 +247,7 @@
} }
.root { .root {
@include header-mobile(); @include mixins.header-mobile();
} }
} }

View File

@ -4,7 +4,7 @@ import React, {
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global'; import { getActions, withGlobal } from '../../global';
import type { ApiChat, ApiChatBannedRights } from '../../api/types'; import type { ApiChat, ApiChatBannedRights, ApiInputMessageReplyInfo } from '../../api/types';
import type { import type {
ActiveEmojiInteraction, ActiveEmojiInteraction,
MessageListType, MessageListType,
@ -44,12 +44,12 @@ import {
selectChatMessage, selectChatMessage,
selectCurrentMessageList, selectCurrentMessageList,
selectCurrentTextSearch, selectCurrentTextSearch,
selectDraft,
selectIsChatBotNotStarted, selectIsChatBotNotStarted,
selectIsInSelectMode, selectIsInSelectMode,
selectIsRightColumnShown, selectIsRightColumnShown,
selectIsUserBlocked, selectIsUserBlocked,
selectPinnedIds, selectPinnedIds,
selectReplyingToId,
selectTabState, selectTabState,
selectTheme, selectTheme,
selectThreadInfo, selectThreadInfo,
@ -105,7 +105,7 @@ type StateProps = {
threadId?: number; threadId?: number;
messageListType?: MessageListType; messageListType?: MessageListType;
chat?: ApiChat; chat?: ApiChat;
replyingToId?: number; draftReplyInfo?: ApiInputMessageReplyInfo;
isPrivate?: boolean; isPrivate?: boolean;
isPinnedMessageList?: boolean; isPinnedMessageList?: boolean;
canPost?: boolean; canPost?: boolean;
@ -160,7 +160,7 @@ function MiddleColumn({
messageListType, messageListType,
isMobile, isMobile,
chat, chat,
replyingToId, draftReplyInfo,
isPrivate, isPrivate,
isPinnedMessageList, isPinnedMessageList,
canPost, canPost,
@ -433,7 +433,7 @@ function MiddleColumn({
const messageSendingRestrictionReason = getMessageSendingRestrictionReason( const messageSendingRestrictionReason = getMessageSendingRestrictionReason(
lang, currentUserBannedRights, defaultBannedRights, lang, currentUserBannedRights, defaultBannedRights,
); );
const forumComposerPlaceholder = getForumComposerPlaceholder(lang, chat, threadId, Boolean(replyingToId)); const forumComposerPlaceholder = getForumComposerPlaceholder(lang, chat, threadId, Boolean(draftReplyInfo));
const composerRestrictionMessage = messageSendingRestrictionReason || forumComposerPlaceholder; const composerRestrictionMessage = messageSendingRestrictionReason || forumComposerPlaceholder;
@ -752,9 +752,9 @@ export default memo(withGlobal<OwnProps>(
const shouldLoadFullChat = Boolean( const shouldLoadFullChat = Boolean(
chat && isChatGroup(chat) && !selectChatFullInfo(global, chat.id), chat && isChatGroup(chat) && !selectChatFullInfo(global, chat.id),
); );
const replyingToId = selectReplyingToId(global, chatId, threadId); const draftReplyInfo = selectDraft(global, chatId, threadId)?.replyInfo;
const shouldBlockSendInForum = chat?.isForum 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; : false;
const audioMessage = audioChatId && audioMessageId const audioMessage = audioChatId && audioMessageId
? selectChatMessage(global, audioChatId, audioMessageId) ? selectChatMessage(global, audioChatId, audioMessageId)
@ -776,7 +776,7 @@ export default memo(withGlobal<OwnProps>(
threadId, threadId,
messageListType, messageListType,
chat, chat,
replyingToId, draftReplyInfo,
isPrivate, isPrivate,
areChatSettingsLoaded: Boolean(chat?.settings), areChatSettingsLoaded: Boolean(chat?.settings),
canPost: !isPinnedMessageList canPost: !isPinnedMessageList

View File

@ -1,8 +1,8 @@
@import "../../styles/mixins"; @use "../../styles/mixins";
@mixin mobile-header-styles() { @mixin mobile-header-styles() {
.AudioPlayer { .AudioPlayer {
@include header-mobile; @include mixins.header-mobile;
flex-direction: row; flex-direction: row;
margin-top: 0; margin-top: 0;

View File

@ -21,6 +21,7 @@ type OwnProps = {
isInSideMenu?: true; isInSideMenu?: true;
chatId?: string; chatId?: string;
threadId?: number; threadId?: number;
canShowNew?: boolean;
onMenuOpened: VoidFunction; onMenuOpened: VoidFunction;
onMenuClosed: VoidFunction; onMenuClosed: VoidFunction;
}; };
@ -31,6 +32,7 @@ const AttachBotItem: FC<OwnProps> = ({
chatId, chatId,
threadId, threadId,
isInSideMenu, isInSideMenu,
canShowNew,
onMenuOpened, onMenuOpened,
onMenuClosed, onMenuClosed,
}) => { }) => {
@ -93,6 +95,7 @@ const AttachBotItem: FC<OwnProps> = ({
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
> >
{bot.shortName} {bot.shortName}
{canShowNew && bot.isDisclaimerNeeded && <span className="menu-item-badge">{lang('New')}</span>}
{menuPosition && ( {menuPosition && (
<Menu <Menu
isOpen={isMenuOpen} isOpen={isMenuOpen}

View File

@ -1,4 +1,4 @@
@import "../../../styles/mixins"; @use "../../../styles/mixins";
.BotKeyboardMenu { .BotKeyboardMenu {
.bubble { .bubble {
@ -13,7 +13,7 @@
padding: 0.1875rem 0.625rem; padding: 0.1875rem 0.625rem;
max-height: 75vh; max-height: 75vh;
overflow-y: scroll; overflow-y: scroll;
@include adapt-padding-to-scrollbar(0.625rem); @include mixins.adapt-padding-to-scrollbar(0.625rem);
.row { .row {
display: flex; display: flex;

View File

@ -1,4 +1,5 @@
.ComposerEmbeddedMessage { .ComposerEmbeddedMessage {
--accent-color: var(--color-primary);
height: 2.625rem; height: 2.625rem;
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */ /* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
transition: height 150ms ease-out, opacity 150ms ease-out; transition: height 150ms ease-out, opacity 150ms ease-out;
@ -32,7 +33,7 @@
display: grid; display: grid;
place-content: center; place-content: center;
font-size: 1.5rem; font-size: 1.5rem;
color: var(--color-primary); color: var(--accent-color);
@media (max-width: 600px) { @media (max-width: 600px) {
width: 2.875rem; width: 2.875rem;
@ -47,6 +48,7 @@
margin: 0 -0.0625rem 0 0.75rem; margin: 0 -0.0625rem 0 0.75rem;
padding: 0; padding: 0;
align-self: center; align-self: center;
color: var(--accent-color, var(--color-primary));
@media (max-width: 600px) { @media (max-width: 600px) {
width: 1.75rem; width: 1.75rem;

View File

@ -4,13 +4,14 @@ import React, {
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import type { ApiMessage, ApiPeer } from '../../../api/types'; import type { ApiInputMessageReplyInfo, ApiMessage, ApiPeer } from '../../../api/types';
import { stripCustomEmoji } from '../../../global/helpers'; import { stripCustomEmoji } from '../../../global/helpers';
import { import {
selectCanAnimateInterface, selectCanAnimateInterface,
selectChatMessage, selectChatMessage,
selectCurrentMessageList, selectCurrentMessageList,
selectDraft,
selectEditingId, selectEditingId,
selectEditingMessage, selectEditingMessage,
selectEditingScheduledId, selectEditingScheduledId,
@ -18,12 +19,12 @@ import {
selectIsChatWithSelf, selectIsChatWithSelf,
selectIsCurrentUserPremium, selectIsCurrentUserPremium,
selectPeer, selectPeer,
selectReplyingToId,
selectSender, selectSender,
selectTabState, selectTabState,
} from '../../../global/selectors'; } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import captureEscKeyListener from '../../../util/captureEscKeyListener'; import captureEscKeyListener from '../../../util/captureEscKeyListener';
import { getPeerColorClass } from '../../common/helpers/peerColor';
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers'; import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
@ -32,7 +33,7 @@ import useMenuPosition from '../../../hooks/useMenuPosition';
import useShowTransition from '../../../hooks/useShowTransition'; import useShowTransition from '../../../hooks/useShowTransition';
import useAsyncRendering from '../../right/hooks/useAsyncRendering'; import useAsyncRendering from '../../right/hooks/useAsyncRendering';
import EmbeddedMessage from '../../common/EmbeddedMessage'; import EmbeddedMessage from '../../common/embedded/EmbeddedMessage';
import Button from '../../ui/Button'; import Button from '../../ui/Button';
import Menu from '../../ui/Menu'; import Menu from '../../ui/Menu';
import MenuItem from '../../ui/MenuItem'; import MenuItem from '../../ui/MenuItem';
@ -41,7 +42,7 @@ import MenuSeparator from '../../ui/MenuSeparator';
import './ComposerEmbeddedMessage.scss'; import './ComposerEmbeddedMessage.scss';
type StateProps = { type StateProps = {
replyingToId?: number; replyInfo?: ApiInputMessageReplyInfo;
editingId?: number; editingId?: number;
message?: ApiMessage; message?: ApiMessage;
sender?: ApiPeer; sender?: ApiPeer;
@ -62,7 +63,7 @@ type OwnProps = {
const FORWARD_RENDERING_DELAY = 300; const FORWARD_RENDERING_DELAY = 300;
const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
replyingToId, replyInfo,
editingId, editingId,
message, message,
sender, sender,
@ -77,7 +78,7 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
onClear, onClear,
}) => { }) => {
const { const {
setReplyingToId, resetDraftReplyInfo,
setEditingId, setEditingId,
focusMessage, focusMessage,
changeForwardRecipient, changeForwardRecipient,
@ -89,23 +90,31 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
const ref = useRef<HTMLDivElement>(null); const ref = useRef<HTMLDivElement>(null);
const lang = useLang(); const lang = useLang();
const isReplyToTopicStart = message?.content.action?.type === 'topicCreate';
const isForwarding = Boolean(forwardedMessagesCount); const isForwarding = Boolean(forwardedMessagesCount);
const isShown = Boolean( const isShown = Boolean(
((replyingToId || editingId) && message) ((replyInfo || editingId) && message)
|| (sender && forwardedMessagesCount), || (sender && forwardedMessagesCount),
); );
const canAnimate = useAsyncRendering( const canAnimate = useAsyncRendering(
[forwardedMessagesCount], [isShown],
forwardedMessagesCount ? FORWARD_RENDERING_DELAY : undefined, isShown ? FORWARD_RENDERING_DELAY : undefined,
); );
const { const {
shouldRender, transitionClassNames, shouldRender, transitionClassNames,
} = useShowTransition(canAnimate && isShown, undefined, !shouldAnimate, undefined, !shouldAnimate); } = useShowTransition(
canAnimate && isShown && !isReplyToTopicStart,
undefined,
!shouldAnimate,
undefined,
!shouldAnimate,
);
const clearEmbedded = useLastCallback(() => { const clearEmbedded = useLastCallback(() => {
if (replyingToId && !shouldForceShowEditing) { if (replyInfo && !shouldForceShowEditing) {
setReplyingToId({ messageId: undefined }); resetDraftReplyInfo();
} else if (editingId) { } else if (editingId) {
setEditingId({ messageId: undefined }); setEditingId({ messageId: undefined });
} else if (forwardedMessagesCount) { } else if (forwardedMessagesCount) {
@ -153,9 +162,13 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
}, [handleContextMenuClose, shouldRender]); }, [handleContextMenuClose, shouldRender]);
const className = buildClassName('ComposerEmbeddedMessage', transitionClassNames); const className = buildClassName('ComposerEmbeddedMessage', transitionClassNames);
const innerClassName = buildClassName(
'ComposerEmbeddedMessage_inner',
getPeerColorClass(sender),
);
const leftIcon = useMemo(() => { const leftIcon = useMemo(() => {
if (replyingToId && !shouldForceShowEditing) { if (replyInfo && !shouldForceShowEditing) {
return 'icon-reply'; return 'icon-reply';
} }
if (editingId) { if (editingId) {
@ -166,7 +179,7 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
} }
return undefined; return undefined;
}, [editingId, isForwarding, replyingToId, shouldForceShowEditing]); }, [editingId, isForwarding, replyInfo, shouldForceShowEditing]);
const customText = forwardedMessagesCount && forwardedMessagesCount > 1 const customText = forwardedMessagesCount && forwardedMessagesCount > 1
? lang('ForwardedMessageCount', forwardedMessagesCount) ? lang('ForwardedMessageCount', forwardedMessagesCount)
@ -191,18 +204,18 @@ const ComposerEmbeddedMessage: FC<OwnProps & StateProps> = ({
return ( return (
<div className={className} ref={ref} onContextMenu={handleContextMenu} onClick={handleContextMenu}> <div className={className} ref={ref} onContextMenu={handleContextMenu} onClick={handleContextMenu}>
<div className="ComposerEmbeddedMessage_inner"> <div className={innerClassName}>
<div className="embedded-left-icon"> <div className="embedded-left-icon">
<i className={buildClassName('icon', leftIcon)} /> <i className={buildClassName('icon', leftIcon)} />
</div> </div>
<EmbeddedMessage <EmbeddedMessage
className="inside-input" className="inside-input"
replyInfo={replyInfo}
message={strippedMessage} message={strippedMessage}
sender={!noAuthors ? sender : undefined} sender={!noAuthors ? sender : undefined}
customText={customText} customText={customText}
title={editingId ? lang('EditMessage') : noAuthors ? lang('HiddenSendersNameDescription') : undefined} title={editingId ? lang('EditMessage') : noAuthors ? lang('HiddenSendersNameDescription') : undefined}
onClick={handleMessageClick} onClick={handleMessageClick}
hasContextMenu={isForwarding && !isContextMenuDisabled}
/> />
<Button <Button
className="embedded-cancel" className="embedded-cancel"
@ -295,7 +308,6 @@ export default memo(withGlobal<OwnProps>(
}, },
} = selectTabState(global); } = selectTabState(global);
const replyingToId = selectReplyingToId(global, chatId, threadId);
const editingId = messageListType === 'scheduled' const editingId = messageListType === 'scheduled'
? selectEditingScheduledId(global, chatId) ? selectEditingScheduledId(global, chatId)
: selectEditingId(global, chatId, threadId); : selectEditingId(global, chatId, threadId);
@ -303,9 +315,11 @@ export default memo(withGlobal<OwnProps>(
const isForwarding = toChatId === chatId; const isForwarding = toChatId === chatId;
const forwardedMessages = forwardMessageIds?.map((id) => selectChatMessage(global, fromChatId!, id)!); const forwardedMessages = forwardMessageIds?.map((id) => selectChatMessage(global, fromChatId!, id)!);
const draft = selectDraft(global, chatId, threadId);
const replyInfo = draft?.replyInfo;
let message: ApiMessage | undefined; let message: ApiMessage | undefined;
if (replyingToId && !shouldForceShowEditing) { if (replyInfo && !shouldForceShowEditing) {
message = selectChatMessage(global, chatId, replyingToId); message = selectChatMessage(global, replyInfo.replyToPeerId || chatId, replyInfo.replyToMsgId);
} else if (editingId) { } else if (editingId) {
message = selectEditingMessage(global, chatId, threadId, messageListType); message = selectEditingMessage(global, chatId, threadId, messageListType);
} else if (isForwarding && forwardMessageIds!.length === 1) { } else if (isForwarding && forwardMessageIds!.length === 1) {
@ -313,7 +327,7 @@ export default memo(withGlobal<OwnProps>(
} }
let sender: ApiPeer | undefined; let sender: ApiPeer | undefined;
if (replyingToId && message && !shouldForceShowEditing) { if (replyInfo && message && !shouldForceShowEditing) {
const { forwardInfo } = message; const { forwardInfo } = message;
const isChatWithSelf = selectIsChatWithSelf(global, chatId); const isChatWithSelf = selectIsChatWithSelf(global, chatId);
if (forwardInfo && (forwardInfo.isChannelPost || isChatWithSelf)) { if (forwardInfo && (forwardInfo.isChannelPost || isChatWithSelf)) {
@ -343,7 +357,7 @@ export default memo(withGlobal<OwnProps>(
&& Boolean(message?.content.storyData); && Boolean(message?.content.storyData);
return { return {
replyingToId, replyInfo,
editingId, editingId,
message, message,
sender, sender,

View File

@ -1,4 +1,4 @@
@import "../../../styles/mixins"; @use "../../../styles/mixins";
.EmojiPicker { .EmojiPicker {
--emoji-size: 2.25rem; --emoji-size: 2.25rem;
@ -10,11 +10,11 @@
height: calc(100% - 3rem); height: calc(100% - 3rem);
overflow-y: auto; overflow-y: auto;
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
@include adapt-padding-to-scrollbar(0.75rem); @include mixins.adapt-padding-to-scrollbar(0.75rem);
@media (max-width: 600px) { @media (max-width: 600px) {
padding: 0.5rem 0.25rem; padding: 0.5rem 0.25rem;
@include adapt-padding-to-scrollbar(0.25rem); @include mixins.adapt-padding-to-scrollbar(0.25rem);
} }
} }

View File

@ -6,12 +6,13 @@ import React, {
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import type { ApiInputMessageReplyInfo } from '../../../api/types';
import type { IAnchorPosition, ISettings } from '../../../types'; import type { IAnchorPosition, ISettings } from '../../../types';
import type { Signal } from '../../../util/signals'; import type { Signal } from '../../../util/signals';
import { EDITABLE_INPUT_ID } from '../../../config'; import { EDITABLE_INPUT_ID } from '../../../config';
import { requestForcedReflow, requestMutation } from '../../../lib/fasterdom/fasterdom'; import { requestForcedReflow, requestMutation } from '../../../lib/fasterdom/fasterdom';
import { selectCanPlayAnimatedEmojis, selectIsInSelectMode, selectReplyingToId } from '../../../global/selectors'; import { selectCanPlayAnimatedEmojis, selectDraft, selectIsInSelectMode } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import captureKeyboardListeners from '../../../util/captureKeyboardListeners'; import captureKeyboardListeners from '../../../util/captureKeyboardListeners';
import { getIsDirectTextInputDisabled } from '../../../util/directInputManager'; import { getIsDirectTextInputDisabled } from '../../../util/directInputManager';
@ -74,7 +75,7 @@ type OwnProps = {
}; };
type StateProps = { type StateProps = {
replyingToId?: number; replyInfo?: ApiInputMessageReplyInfo;
isSelectModeActive?: boolean; isSelectModeActive?: boolean;
messageSendKeyCombo?: ISettings['messageSendKeyCombo']; messageSendKeyCombo?: ISettings['messageSendKeyCombo'];
canPlayAnimatedEmojis: boolean; canPlayAnimatedEmojis: boolean;
@ -126,7 +127,7 @@ const MessageInput: FC<OwnProps & StateProps> = ({
noFocusInterception, noFocusInterception,
shouldSuppressFocus, shouldSuppressFocus,
shouldSuppressTextFormatter, shouldSuppressTextFormatter,
replyingToId, replyInfo,
isSelectModeActive, isSelectModeActive,
canPlayAnimatedEmojis, canPlayAnimatedEmojis,
messageSendKeyCombo, messageSendKeyCombo,
@ -461,7 +462,7 @@ const MessageInput: FC<OwnProps & StateProps> = ({
if (canAutoFocus) { if (canAutoFocus) {
focusInput(); focusInput();
} }
}, [chatId, focusInput, replyingToId, canAutoFocus]); }, [chatId, focusInput, replyInfo, canAutoFocus]);
useEffect(() => { useEffect(() => {
if ( if (
@ -626,7 +627,7 @@ export default memo(withGlobal<OwnProps>(
return { return {
messageSendKeyCombo, messageSendKeyCombo,
replyingToId: chatId && threadId ? selectReplyingToId(global, chatId, threadId) : undefined, replyInfo: chatId && threadId ? selectDraft(global, chatId, threadId)?.replyInfo : undefined,
isSelectModeActive: selectIsInSelectMode(global), isSelectModeActive: selectIsInSelectMode(global),
canPlayAnimatedEmojis: selectCanPlayAnimatedEmojis(global), canPlayAnimatedEmojis: selectCanPlayAnimatedEmojis(global),
}; };

View File

@ -1,4 +1,4 @@
@import "../../../styles/mixins"; @use "../../../styles/mixins";
.root { .root {
--color-primary: var(--color-text); --color-primary: var(--color-text);
@ -17,11 +17,11 @@
overflow-x: hidden; overflow-x: hidden;
padding: 0.5rem 0.25rem; padding: 0.5rem 0.25rem;
@include adapt-padding-to-scrollbar(0.25rem); @include mixins.adapt-padding-to-scrollbar(0.25rem);
&_customEmoji { &_customEmoji {
padding: 0.5rem 0.75rem; padding: 0.5rem 0.75rem;
@include adapt-padding-to-scrollbar(0.75rem); @include mixins.adapt-padding-to-scrollbar(0.75rem);
} }
:global(.bubble) { :global(.bubble) {

View File

@ -1,4 +1,4 @@
@import "../../../styles/mixins"; @use "../../../styles/mixins";
.SymbolMenu { .SymbolMenu {
&.attachment-modal-symbol-menu { &.attachment-modal-symbol-menu {
@ -277,7 +277,7 @@
padding: 0; padding: 0;
} }
@include while-transition() { @include mixins.while-transition() {
overflow: hidden; overflow: hidden;
} }
} }

View File

@ -56,7 +56,7 @@ const useDraft = ({
useEffect(() => { useEffect(() => {
const html = getHtml(); const html = getHtml();
const isLocalDraft = draft?.isLocal !== undefined; const isLocalDraft = draft?.isLocal !== undefined;
if (getTextWithEntitiesAsHtml(draft) === html && !isLocalDraft) { if (getTextWithEntitiesAsHtml(draft?.text) === html && !isLocalDraft) {
isTouchedRef.current = false; isTouchedRef.current = false;
} else { } else {
isTouchedRef.current = true; isTouchedRef.current = true;
@ -77,12 +77,13 @@ const useDraft = ({
saveDraft({ saveDraft({
chatId: prevState.chatId ?? chatId, chatId: prevState.chatId ?? chatId,
threadId: prevState.threadId ?? threadId, threadId: prevState.threadId ?? threadId,
draft: parseMessageInput(html), text: parseMessageInput(html),
}); });
} else { } else {
clearDraft({ clearDraft({
chatId: prevState.chatId ?? chatId, chatId: prevState.chatId ?? chatId,
threadId: prevState.threadId ?? threadId, threadId: prevState.threadId ?? threadId,
shouldKeepReply: true,
}); });
} }
}); });
@ -109,9 +110,9 @@ const useDraft = ({
return; return;
} }
setHtml(getTextWithEntitiesAsHtml(draft)); setHtml(getTextWithEntitiesAsHtml(draft.text));
const customEmojiIds = draft.entities const customEmojiIds = draft.text?.entities
?.map((entity) => entity.type === ApiMessageEntityTypes.CustomEmoji && entity.documentId) ?.map((entity) => entity.type === ApiMessageEntityTypes.CustomEmoji && entity.documentId)
.filter(Boolean) || []; .filter(Boolean) || [];
if (customEmojiIds.length) loadCustomEmojis({ ids: customEmojiIds }); if (customEmojiIds.length) loadCustomEmojis({ ids: customEmojiIds });

View File

@ -2,7 +2,7 @@ import { useEffect, useState } from '../../../../lib/teact/teact';
import { getActions } from '../../../../global'; import { getActions } from '../../../../global';
import type { ApiFormattedText, ApiMessage } from '../../../../api/types'; import type { ApiFormattedText, ApiMessage } from '../../../../api/types';
import type { MessageListType } from '../../../../global/types'; import type { ApiDraft, MessageListType } from '../../../../global/types';
import type { Signal } from '../../../../util/signals'; import type { Signal } from '../../../../util/signals';
import { ApiMessageEntityTypes } from '../../../../api/types'; import { ApiMessageEntityTypes } from '../../../../api/types';
@ -32,13 +32,14 @@ const useEditing = (
chatId: string, chatId: string,
threadId: number, threadId: number,
type: MessageListType, type: MessageListType,
draft?: ApiFormattedText, draft?: ApiDraft,
editingDraft?: ApiFormattedText, editingDraft?: ApiFormattedText,
replyingToId?: number,
): [VoidFunction, VoidFunction, boolean] => { ): [VoidFunction, VoidFunction, boolean] => {
const { editMessage, setEditingDraft, toggleMessageWebPage } = getActions(); const { editMessage, setEditingDraft, toggleMessageWebPage } = getActions();
const [shouldForceShowEditing, setShouldForceShowEditing] = useState(false); const [shouldForceShowEditing, setShouldForceShowEditing] = useState(false);
const replyingToId = draft?.replyInfo?.replyToMsgId;
useEffectWithPrevDeps(([prevEditedMessage, prevReplyingToId]) => { useEffectWithPrevDeps(([prevEditedMessage, prevReplyingToId]) => {
if (!editedMessage) { if (!editedMessage) {
return; return;
@ -125,7 +126,7 @@ const useEditing = (
// Run one frame after editing draft reset // Run one frame after editing draft reset
requestMeasure(() => { requestMeasure(() => {
setHtml(getTextWithEntitiesAsHtml(draft)); setHtml(getTextWithEntitiesAsHtml(draft.text));
// Wait one more frame until new HTML is rendered // Wait one more frame until new HTML is rendered
requestNextMutation(() => { requestNextMutation(() => {

View File

@ -167,7 +167,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
}) => { }) => {
const { const {
openChat, openChat,
setReplyingToId, updateDraftReplyInfo,
setEditingId, setEditingId,
pinMessage, pinMessage,
openForwardMenu, openForwardMenu,
@ -296,7 +296,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
}); });
const handleReply = useLastCallback(() => { const handleReply = useLastCallback(() => {
setReplyingToId({ messageId: message.id }); updateDraftReplyInfo({ replyToMsgId: message.id });
closeMenu(); closeMenu();
}); });

View File

@ -1,4 +1,4 @@
@import "message-content"; @use "message-content";
// General styles // General styles
.Message { .Message {

View File

@ -6,6 +6,7 @@ import { getActions, withGlobal } from '../../../global';
import type { import type {
ApiAvailableReaction, ApiAvailableReaction,
ApiChat,
ApiChatMember, ApiChatMember,
ApiMessage, ApiMessage,
ApiMessageOutgoingStatus, ApiMessageOutgoingStatus,
@ -35,23 +36,23 @@ import {
getMessageCustomShape, getMessageCustomShape,
getMessageHtmlId, getMessageHtmlId,
getMessageKey, getMessageKey,
getMessageLocation,
getMessageSingleCustomEmoji, getMessageSingleCustomEmoji,
getMessageSingleRegularEmoji, getMessageSingleRegularEmoji,
getPeerColorKey,
getSenderTitle, getSenderTitle,
hasMessageText, hasMessageText,
isAnonymousOwnMessage, isAnonymousOwnMessage,
isChatChannel, isChatChannel,
isChatGroup, isChatGroup,
isChatPublic,
isChatWithRepliesBot, isChatWithRepliesBot,
isGeoLiveExpired, isGeoLiveExpired,
isMessageLocal, isMessageLocal,
isMessageTranslatable, isMessageTranslatable,
isOwnMessage, isOwnMessage,
isReplyMessage, isReplyToMessage,
isUserId, isUserId,
} from '../../../global/helpers'; } from '../../../global/helpers';
import { getMessageReplyInfo, getStoryReplyInfo } from '../../../global/helpers/replies';
import { import {
selectAllowedMessageActions, selectAllowedMessageActions,
selectAnimatedEmoji, selectAnimatedEmoji,
@ -81,6 +82,7 @@ import {
selectRequestedChatTranslationLanguage, selectRequestedChatTranslationLanguage,
selectRequestedMessageTranslationLanguage, selectRequestedMessageTranslationLanguage,
selectSender, selectSender,
selectSenderFromHeader,
selectShouldDetectChatLanguage, selectShouldDetectChatLanguage,
selectShouldLoopStickers, selectShouldLoopStickers,
selectTabState, selectTabState,
@ -101,6 +103,7 @@ import {
REM, REM,
ROUND_VIDEO_DIMENSIONS_PX, ROUND_VIDEO_DIMENSIONS_PX,
} from '../../common/helpers/mediaDimensions'; } from '../../common/helpers/mediaDimensions';
import { getPeerColorClass } from '../../common/helpers/peerColor';
import renderText from '../../common/helpers/renderText'; import renderText from '../../common/helpers/renderText';
import { getCustomEmojiSize } from '../composer/helpers/customEmoji'; import { getCustomEmojiSize } from '../composer/helpers/customEmoji';
import { buildContentClassName } from './helpers/buildContentClassName'; import { buildContentClassName } from './helpers/buildContentClassName';
@ -133,9 +136,10 @@ import Avatar from '../../common/Avatar';
import CustomEmoji from '../../common/CustomEmoji'; import CustomEmoji from '../../common/CustomEmoji';
import Document from '../../common/Document'; import Document from '../../common/Document';
import DotAnimation from '../../common/DotAnimation'; import DotAnimation from '../../common/DotAnimation';
import EmbeddedMessage from '../../common/EmbeddedMessage'; import EmbeddedMessage from '../../common/embedded/EmbeddedMessage';
import EmbeddedStory from '../../common/EmbeddedStory'; import EmbeddedStory from '../../common/embedded/EmbeddedStory';
import FakeIcon from '../../common/FakeIcon'; import FakeIcon from '../../common/FakeIcon';
import Icon from '../../common/Icon';
import MessageText from '../../common/MessageText'; import MessageText from '../../common/MessageText';
import PremiumIcon from '../../common/PremiumIcon'; import PremiumIcon from '../../common/PremiumIcon';
import ReactionStaticEmoji from '../../common/ReactionStaticEmoji'; import ReactionStaticEmoji from '../../common/ReactionStaticEmoji';
@ -208,6 +212,8 @@ type StateProps = {
replyMessage?: ApiMessage; replyMessage?: ApiMessage;
replyMessageSender?: ApiPeer; replyMessageSender?: ApiPeer;
replyMessageForwardSender?: ApiPeer; replyMessageForwardSender?: ApiPeer;
replyMessageChat?: ApiChat;
isReplyPrivate?: boolean;
replyStory?: ApiTypeStory; replyStory?: ApiTypeStory;
storySender?: ApiUser; storySender?: ApiUser;
outgoingStatus?: ApiMessageOutgoingStatus; outgoingStatus?: ApiMessageOutgoingStatus;
@ -318,7 +324,9 @@ const Message: FC<OwnProps & StateProps> = ({
replyMessage, replyMessage,
replyMessageSender, replyMessageSender,
replyMessageForwardSender, replyMessageForwardSender,
replyMessageChat,
replyStory, replyStory,
isReplyPrivate,
storySender, storySender,
outgoingStatus, outgoingStatus,
uploadProgress, uploadProgress,
@ -454,8 +462,12 @@ const Message: FC<OwnProps & StateProps> = ({
const isLocal = isMessageLocal(message); const isLocal = isMessageLocal(message);
const isOwn = isOwnMessage(message); const isOwn = isOwnMessage(message);
const isScheduled = messageListType === 'scheduled' || message.isScheduled; const isScheduled = messageListType === 'scheduled' || message.isScheduled;
const hasReply = isReplyMessage(message) && !shouldHideReply; const hasMessageReply = isReplyToMessage(message) && !shouldHideReply;
const hasStoryReply = Boolean(message.replyToStoryId);
const messageReplyInfo = getMessageReplyInfo(message);
const storyReplyInfo = getStoryReplyInfo(message);
const hasStoryReply = Boolean(storyReplyInfo);
const hasThread = Boolean(repliesThreadInfo) && messageListType === 'thread'; const hasThread = Boolean(repliesThreadInfo) && messageListType === 'thread';
const isCustomShape = getMessageCustomShape(message); const isCustomShape = getMessageCustomShape(message);
const hasAnimatedEmoji = isCustomShape && (animatedEmoji || animatedCustomEmoji); const hasAnimatedEmoji = isCustomShape && (animatedEmoji || animatedCustomEmoji);
@ -485,7 +497,9 @@ const Message: FC<OwnProps & StateProps> = ({
&& forwardInfo.fromMessageId && forwardInfo.fromMessageId
)); ));
const hasSubheader = hasTopicChip || hasReply || hasStoryReply; const noUserColors = isOwn && !isCustomShape;
const hasSubheader = hasTopicChip || hasMessageReply || hasStoryReply;
const selectMessage = useLastCallback((e?: React.MouseEvent<HTMLDivElement, MouseEvent>, groupedId?: string) => { const selectMessage = useLastCallback((e?: React.MouseEvent<HTMLDivElement, MouseEvent>, groupedId?: string) => {
toggleMessageSelection({ toggleMessageSelection({
@ -501,6 +515,7 @@ const Message: FC<OwnProps & StateProps> = ({
const shouldPreferOriginSender = forwardInfo && (isChatWithSelf || isRepliesChat || !messageSender); const shouldPreferOriginSender = forwardInfo && (isChatWithSelf || isRepliesChat || !messageSender);
const avatarPeer = shouldPreferOriginSender ? originSender : messageSender; const avatarPeer = shouldPreferOriginSender ? originSender : messageSender;
const messageColorPeer = originSender || sender;
const senderPeer = (forwardInfo || message.content.storyData) ? originSender : messageSender; const senderPeer = (forwardInfo || message.content.storyData) ? originSender : messageSender;
const { const {
@ -561,7 +576,6 @@ const Message: FC<OwnProps & StateProps> = ({
isInDocumentGroup, isInDocumentGroup,
asForwarded, asForwarded,
isScheduled, isScheduled,
isRepliesChat,
album, album,
avatarPeer, avatarPeer,
senderPeer, senderPeer,
@ -569,6 +583,7 @@ const Message: FC<OwnProps & StateProps> = ({
messageTopic, messageTopic,
Boolean(requestedChatTranslationLanguage), Boolean(requestedChatTranslationLanguage),
replyStory && 'content' in replyStory ? replyStory : undefined, replyStory && 'content' in replyStory ? replyStory : undefined,
isReplyPrivate,
); );
useEffect(() => { useEffect(() => {
@ -592,7 +607,7 @@ const Message: FC<OwnProps & StateProps> = ({
isOwn && 'own', isOwn && 'own',
Boolean(message.views) && 'has-views', Boolean(message.views) && 'has-views',
message.isEdited && 'was-edited', message.isEdited && 'was-edited',
hasReply && 'has-reply', hasMessageReply && 'has-reply',
isContextMenuOpen && 'has-menu-open', isContextMenuOpen && 'has-menu-open',
isFocused && !noFocusHighlight && 'focused', isFocused && !noFocusHighlight && 'focused',
isForwarding && 'is-forwarding', isForwarding && 'is-forwarding',
@ -618,6 +633,9 @@ const Message: FC<OwnProps & StateProps> = ({
action, game, storyData, action, game, storyData,
} = getMessageContent(message); } = getMessageContent(message);
const { replyToMsgId, replyToPeerId, isQuote } = messageReplyInfo || {};
const { userId: storyReplyUserId, storyId: storyReplyId } = storyReplyInfo || {};
const detectedLanguage = useTextLanguage( const detectedLanguage = useTextLanguage(
text?.text, text?.text,
!(areTranslationsEnabled || shouldDetectChatLanguage), !(areTranslationsEnabled || shouldDetectChatLanguage),
@ -657,6 +675,7 @@ const Message: FC<OwnProps & StateProps> = ({
hasReactions, hasReactions,
isGeoLiveActive: location?.type === 'geoLive' && !isGeoLiveExpired(message), isGeoLiveActive: location?.type === 'geoLive' && !isGeoLiveExpired(message),
withVoiceTranscription, withVoiceTranscription,
peerColorClass: getPeerColorClass(messageColorPeer, noUserColors),
}); });
const withAppendix = contentClassName.includes('has-appendix'); const withAppendix = contentClassName.includes('has-appendix');
@ -676,7 +695,7 @@ const Message: FC<OwnProps & StateProps> = ({
let reactionsPosition!: ReactionsPosition; let reactionsPosition!: ReactionsPosition;
if (hasReactions) { if (hasReactions) {
if (isCustomShape || ((photo || video || storyData || (location && location.type === 'geo')) && !hasText)) { if (isCustomShape || ((photo || video || storyData || (location?.type === 'geo')) && !hasText)) {
reactionsPosition = 'outside'; reactionsPosition = 'outside';
} else if (asForwarded) { } else if (asForwarded) {
metaPosition = 'standalone'; metaPosition = 'standalone';
@ -691,15 +710,16 @@ const Message: FC<OwnProps & StateProps> = ({
const quickReactionPosition: QuickReactionPosition = isCustomShape ? 'in-meta' : 'in-content'; const quickReactionPosition: QuickReactionPosition = isCustomShape ? 'in-meta' : 'in-content';
useEnsureMessage( useEnsureMessage(
isRepliesChat && message.replyToChatId ? message.replyToChatId : chatId, replyToPeerId || chatId,
hasReply ? message.replyToMessageId : undefined, replyToMsgId,
replyMessage, replyMessage,
message.id, message.id,
isQuote || isReplyPrivate,
); );
useEnsureStory( useEnsureStory(
message.replyToStoryUserId ? message.replyToStoryUserId : chatId, storyReplyUserId || chatId,
message.replyToStoryId, storyReplyId,
replyStory, replyStory,
); );
@ -939,12 +959,14 @@ const Message: FC<OwnProps & StateProps> = ({
className="message-topic" className="message-topic"
/> />
)} )}
{hasReply && ( {hasMessageReply && (
<EmbeddedMessage <EmbeddedMessage
message={replyMessage} message={replyMessage}
noUserColors={isOwn || isChannel} replyInfo={messageReplyInfo}
noUserColors={noUserColors}
isProtected={isProtected} isProtected={isProtected}
sender={replyMessageSender} sender={replyMessageSender}
senderChat={replyMessageChat}
forwardSender={replyMessageForwardSender} forwardSender={replyMessageForwardSender}
chatTranslations={chatTranslations} chatTranslations={chatTranslations}
requestedChatTranslationLanguage={requestedChatTranslationLanguage} requestedChatTranslationLanguage={requestedChatTranslationLanguage}
@ -957,7 +979,7 @@ const Message: FC<OwnProps & StateProps> = ({
<EmbeddedStory <EmbeddedStory
story={replyStory} story={replyStory}
sender={storySender} sender={storySender}
noUserColors={isOwn || isChannel} noUserColors={noUserColors}
isProtected={isProtected} isProtected={isProtected}
observeIntersectionForLoading={observeIntersectionForLoading} observeIntersectionForLoading={observeIntersectionForLoading}
onClick={handleStoryClick} onClick={handleStoryClick}
@ -1169,6 +1191,7 @@ const Message: FC<OwnProps & StateProps> = ({
theme={theme} theme={theme}
story={webPageStory} story={webPageStory}
isConnected={isConnected} isConnected={isConnected}
noUserColors={isOwn}
onMediaClick={handleMediaClick} onMediaClick={handleMediaClick}
onCancelMediaTransfer={handleCancelUpload} onCancelMediaTransfer={handleCancelUpload}
/> />
@ -1200,7 +1223,7 @@ const Message: FC<OwnProps & StateProps> = ({
const media = photo || video || location; const media = photo || video || location;
const shouldRender = !(isCustomShape && !viaBotId) && ( const shouldRender = !(isCustomShape && !viaBotId) && (
(withSenderName && (!media || hasTopicChip)) || asForwarded || viaBotId || forceSenderName (withSenderName && (!media || hasTopicChip)) || asForwarded || viaBotId || forceSenderName
) && !isInDocumentGroupNotFirst && !(hasReply && isCustomShape); ) && !isInDocumentGroupNotFirst && !(hasMessageReply && isCustomShape);
if (!shouldRender) { if (!shouldRender) {
return undefined; return undefined;
@ -1210,10 +1233,6 @@ const Message: FC<OwnProps & StateProps> = ({
let senderColor; let senderColor;
if (senderPeer && !(isCustomShape && viaBotId)) { if (senderPeer && !(isCustomShape && viaBotId)) {
senderTitle = getSenderTitle(lang, senderPeer); senderTitle = getSenderTitle(lang, senderPeer);
if (!asForwarded && !isOwn) {
senderColor = `color-${getPeerColorKey(senderPeer)}`;
}
} else if (forwardInfo?.hiddenUserName) { } else if (forwardInfo?.hiddenUserName) {
senderTitle = forwardInfo.hiddenUserName; senderTitle = forwardInfo.hiddenUserName;
} else if (storyData && originSender) { } else if (storyData && originSender) {
@ -1235,8 +1254,9 @@ const Message: FC<OwnProps & StateProps> = ({
dir="ltr" dir="ltr"
> >
{asForwarded && ( {asForwarded && (
<i className={`icon ${forwardInfo?.hiddenUserName ? 'icon-forward' : 'icon-share-filled'}`} /> <Icon name={forwardInfo?.hiddenUserName ? 'forward' : 'share-filled'} />
)} )}
{storyData && <Icon name="play-story" />}
{senderTitle ? renderText(senderTitle) : (asForwarded ? NBSP : undefined)} {senderTitle ? renderText(senderTitle) : (asForwarded ? NBSP : undefined)}
{!asForwarded && senderEmojiStatus && ( {!asForwarded && senderEmojiStatus && (
<CustomEmoji <CustomEmoji
@ -1431,8 +1451,7 @@ export default memo(withGlobal<OwnProps>(
message, album, withSenderName, withAvatar, threadId, messageListType, isLastInDocumentGroup, isFirstInGroup, message, album, withSenderName, withAvatar, threadId, messageListType, isLastInDocumentGroup, isFirstInGroup,
} = ownProps; } = ownProps;
const { const {
id, chatId, viaBotId, replyToChatId, replyToMessageId, isOutgoing, forwardInfo, id, chatId, viaBotId, isOutgoing, forwardInfo, transcriptionId, isPinned, repliesThreadInfo,
transcriptionId, isPinned, replyToStoryUserId, replyToStoryId, repliesThreadInfo,
} = message; } = message;
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
@ -1459,17 +1478,25 @@ export default memo(withGlobal<OwnProps>(
const threadTopMessageId = threadId ? selectThreadTopMessageId(global, chatId, threadId) : undefined; const threadTopMessageId = threadId ? selectThreadTopMessageId(global, chatId, threadId) : undefined;
const isThreadTop = message.id === threadTopMessageId; const isThreadTop = message.id === threadTopMessageId;
const shouldHideReply = replyToMessageId === threadTopMessageId; const { replyToMsgId, replyToPeerId, replyFrom } = getMessageReplyInfo(message) || {};
const replyMessage = replyToMessageId && !shouldHideReply const { userId: storyReplyUserId, storyId: storyReplyId } = getStoryReplyInfo(message) || {};
? selectChatMessage(global, isRepliesChat && replyToChatId ? replyToChatId : chatId, replyToMessageId)
const shouldHideReply = replyToMsgId && replyToMsgId === threadTopMessageId;
const replyMessage = replyToMsgId && !shouldHideReply
? selectChatMessage(global, replyToPeerId || chatId, replyToMsgId)
: undefined; : undefined;
const replyMessageSender = replyMessage && selectReplySender(global, replyMessage, Boolean(forwardInfo)); const forwardHeader = forwardInfo || replyFrom;
const replyMessageSender = replyMessage ? selectReplySender(global, replyMessage) : forwardHeader
? selectSenderFromHeader(global, forwardHeader) : undefined;
const replyMessageForwardSender = replyMessage && selectForwardedSender(global, replyMessage); const replyMessageForwardSender = replyMessage && selectForwardedSender(global, replyMessage);
const replyMessageChat = replyToPeerId ? selectChat(global, replyToPeerId) : undefined;
const isReplyPrivate = replyMessageChat && !isChatPublic(replyMessageChat)
&& (replyMessageChat.isNotJoined || replyMessageChat.isRestricted);
const isReplyToTopicStart = replyMessage?.content.action?.type === 'topicCreate'; const isReplyToTopicStart = replyMessage?.content.action?.type === 'topicCreate';
const replyStory = replyToStoryId && replyToStoryUserId const replyStory = storyReplyId && storyReplyUserId
? selectPeerStory(global, replyToStoryUserId, replyToStoryId) ? selectPeerStory(global, storyReplyUserId, storyReplyId)
: undefined; : undefined;
const storySender = replyToStoryUserId ? selectUser(global, replyToStoryUserId) : undefined; const storySender = storyReplyUserId ? selectUser(global, storyReplyUserId) : undefined;
const uploadProgress = selectUploadProgress(global, message); const uploadProgress = selectUploadProgress(global, message);
const isFocused = messageListType === 'thread' && ( const isFocused = messageListType === 'thread' && (
@ -1515,7 +1542,6 @@ export default memo(withGlobal<OwnProps>(
const messageTopic = hasTopicChip ? (selectTopicFromMessage(global, message) || chat?.topics?.[GENERAL_TOPIC_ID]) const messageTopic = hasTopicChip ? (selectTopicFromMessage(global, message) || chat?.topics?.[GENERAL_TOPIC_ID])
: undefined; : undefined;
const isLocation = Boolean(getMessageLocation(message));
const chatTranslations = selectChatTranslations(global, chatId); const chatTranslations = selectChatTranslations(global, chatId);
const requestedTranslationLanguage = selectRequestedMessageTranslationLanguage(global, chatId, message.id); const requestedTranslationLanguage = selectRequestedMessageTranslationLanguage(global, chatId, message.id);
@ -1531,6 +1557,7 @@ export default memo(withGlobal<OwnProps>(
return { return {
theme: selectTheme(global), theme: selectTheme(global),
forceSenderName, forceSenderName,
sender,
canShowSender, canShowSender,
originSender, originSender,
botSender, botSender,
@ -1539,7 +1566,9 @@ export default memo(withGlobal<OwnProps>(
replyMessage, replyMessage,
replyMessageSender, replyMessageSender,
replyMessageForwardSender, replyMessageForwardSender,
replyMessageChat,
replyStory, replyStory,
isReplyPrivate,
storySender, storySender,
isInDocumentGroup, isInDocumentGroup,
isProtected: selectIsMessageProtected(global, message), isProtected: selectIsMessageProtected(global, message),
@ -1593,7 +1622,6 @@ export default memo(withGlobal<OwnProps>(
webPageStory, webPageStory,
isConnected, isConnected,
shouldWarnAboutSvg: global.settings.byKey.shouldWarnAboutSvg, shouldWarnAboutSvg: global.settings.byKey.shouldWarnAboutSvg,
...((canShowSender || isLocation) && { sender }),
...(isOutgoing && { outgoingStatus: selectOutgoingStatus(global, message, messageListType === 'scheduled') }), ...(isOutgoing && { outgoingStatus: selectOutgoingStatus(global, message, messageListType === 'scheduled') }),
...(typeof uploadProgress === 'number' && { uploadProgress }), ...(typeof uploadProgress === 'number' && { uploadProgress }),
...(isFocused && { ...(isFocused && {

View File

@ -1,26 +1,29 @@
.WebPage { .WebPage {
margin-top: 0.25rem; margin-top: 0.25rem;
margin-bottom: 0.125rem; margin-bottom: 0.125rem;
padding: 0.375rem 0.375rem 0.375rem 0.625rem;
font-size: calc(var(--message-text-size, 1rem) - 0.125rem); font-size: calc(var(--message-text-size, 1rem) - 0.125rem);
line-height: 1.125rem; line-height: 1.125rem;
max-width: 29rem; max-width: 29rem;
background-color: var(--accent-background-color);
border-radius: 0.25rem;
position: relative;
overflow: hidden;
&::before {
content: "";
display: block;
position: absolute;
top: 0;
inset-inline-start: 0;
bottom: 0;
width: 3px;
background: var(--bar-gradient, var(--accent-color));
}
.WebPage--content { .WebPage--content {
padding-left: 0.625rem;
position: relative; position: relative;
&::before {
content: "";
display: block;
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: 0.125rem;
background: var(--accent-color);
border-radius: 0.125rem;
}
&.is-story { &.is-story {
display: flex; display: flex;
flex-direction: column-reverse; flex-direction: column-reverse;
@ -28,10 +31,19 @@
} }
&--quick-button { &--quick-button {
margin-top: 0.375rem; --riple-color: var(var(--accent-background-active-color));
.theme-dark .Message.own &:hover { margin-top: 0.375rem;
color: var(--background-color); margin-bottom: -0.375rem;
border-top: 1px solid var(--accent-background-active-color, var(--active-color));
color: var(--accent-color) !important;
transition: opacity 0.2s ease-in;
&:hover, &:active {
background-color: transparent !important;
opacity: 0.85;
} }
} }
@ -71,9 +83,7 @@
margin-bottom: 1rem !important; margin-bottom: 1rem !important;
} }
.message-content:not(.has-reactions) &.no-article:last-child, .message-content:not(.has-reactions) & {
.message-content:not(.has-reactions) &.with-quick-button,
.message-content:not(.has-reactions) &.with-square-photo {
margin-bottom: 1rem !important; margin-bottom: 1rem !important;
} }
@ -150,15 +160,6 @@
} }
} }
&:dir(rtl) {
padding-inline-start: 0.625rem;
&::before {
left: auto;
right: 0;
}
}
@media (min-width: 1921px) { @media (min-width: 1921px) {
@supports (aspect-ratio: 1) { @supports (aspect-ratio: 1) {
&:not(.in-preview) { &:not(.in-preview) {

View File

@ -40,6 +40,7 @@ type OwnProps = {
isDownloading?: boolean; isDownloading?: boolean;
isProtected?: boolean; isProtected?: boolean;
isConnected?: boolean; isConnected?: boolean;
noUserColors?: boolean;
theme: ISettings['theme']; theme: ISettings['theme'];
story?: ApiTypeStory; story?: ApiTypeStory;
onMediaClick?: () => void; onMediaClick?: () => void;
@ -124,7 +125,8 @@ const WebPage: FC<OwnProps> = ({
<Button <Button
className="WebPage--quick-button" className="WebPage--quick-button"
size="tiny" size="tiny"
color="translucent-bordered" color="translucent"
isRectangular
onClick={handleQuickButtonClick} onClick={handleQuickButtonClick}
> >
{lang(langKey)} {lang(langKey)}

View File

@ -1,3 +1,6 @@
@use "sass:map";
@use "../../../styles/icons";
.message-content { .message-content {
position: relative; position: relative;
max-width: var(--max-width); max-width: var(--max-width);
@ -280,24 +283,6 @@
font-weight: normal; font-weight: normal;
} }
@for $i from 1 through 8 {
& > .color-#{$i} {
color: var(--color-user-#{$i});
.custom-emoji {
color: var(--color-user-#{$i});
}
.PremiumIcon {
--color-fill: var(--color-user-#{$i});
}
}
}
.theme-dark .Message.own & > .color-1 {
color: var(--accent-color);
}
& + .File { & + .File {
margin-top: 0.25rem; margin-top: 0.25rem;
} }
@ -313,9 +298,11 @@
.custom-emoji { .custom-emoji {
margin-left: 0.25rem; margin-left: 0.25rem;
color: var(--accent-color);
} }
.PremiumIcon { .PremiumIcon {
--color-fill: var(--accent-color);
vertical-align: middle; vertical-align: middle;
opacity: 0.5; opacity: 0.5;
margin-left: 0.25rem; margin-left: 0.25rem;
@ -709,8 +696,6 @@
} }
.EmbeddedMessage { .EmbeddedMessage {
border-radius: var(--border-radius-messages);
@media (max-width: 600px) { @media (max-width: 600px) {
max-width: calc(90vw - 13rem); max-width: calc(90vw - 13rem);
} }
@ -950,6 +935,38 @@
font-size: 0.875rem; font-size: 0.875rem;
} }
blockquote {
display: inline-block;
position: relative;
padding-inline: 0.5rem 1rem;
margin-block: 0.125rem;
border-radius: 0.25rem;
overflow: hidden;
background-color: var(--accent-background-color);
&::before {
content: "";
position: absolute;
top: 0;
inset-inline-start: 0;
bottom: 0;
width: 3px;
background: var(--bar-gradient, var(--accent-color));
}
&::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;
}
}
@keyframes text-loading { @keyframes text-loading {
0% { 0% {
background-position-x: 0; background-position-x: 0;

View File

@ -17,6 +17,7 @@ export function buildContentClassName(
hasReactions, hasReactions,
isGeoLiveActive, isGeoLiveActive,
withVoiceTranscription, withVoiceTranscription,
peerColorClass,
}: { }: {
hasSubheader?: boolean; hasSubheader?: boolean;
isCustomShape?: boolean | number; isCustomShape?: boolean | number;
@ -29,6 +30,7 @@ export function buildContentClassName(
hasReactions?: boolean; hasReactions?: boolean;
isGeoLiveActive?: boolean; isGeoLiveActive?: boolean;
withVoiceTranscription?: boolean; withVoiceTranscription?: boolean;
peerColorClass?: string;
} = {}, } = {},
) { ) {
const { const {
@ -41,6 +43,10 @@ export function buildContentClassName(
const isMediaWithNoText = isMedia && !hasText; const isMediaWithNoText = isMedia && !hasText;
const isViaBot = Boolean(message.viaBotId); const isViaBot = Boolean(message.viaBotId);
if (peerColorClass) {
classNames.push(peerColorClass);
}
if (!isMedia && message.emojiOnlyCount) { if (!isMedia && message.emojiOnlyCount) {
classNames.push('emoji-only'); classNames.push('emoji-only');
if (message.emojiOnlyCount <= EMOJI_SIZES) { if (message.emojiOnlyCount <= EMOJI_SIZES) {

View File

@ -9,6 +9,8 @@ import type { IAlbum } from '../../../../types';
import { MAIN_THREAD_ID } from '../../../../api/types'; import { MAIN_THREAD_ID } from '../../../../api/types';
import { MediaViewerOrigin } from '../../../../types'; import { MediaViewerOrigin } from '../../../../types';
import { getMessageReplyInfo } from '../../../../global/helpers/replies';
import useLastCallback from '../../../../hooks/useLastCallback'; import useLastCallback from '../../../../hooks/useLastCallback';
export default function useInnerHandlers( export default function useInnerHandlers(
@ -20,7 +22,6 @@ export default function useInnerHandlers(
isInDocumentGroup: boolean, isInDocumentGroup: boolean,
asForwarded?: boolean, asForwarded?: boolean,
isScheduled?: boolean, isScheduled?: boolean,
isChatWithRepliesBot?: boolean,
album?: IAlbum, album?: IAlbum,
avatarPeer?: ApiPeer, avatarPeer?: ApiPeer,
senderPeer?: ApiPeer, senderPeer?: ApiPeer,
@ -28,6 +29,7 @@ export default function useInnerHandlers(
messageTopic?: ApiTopic, messageTopic?: ApiTopic,
isTranslatingChat?: boolean, isTranslatingChat?: boolean,
story?: ApiStory, story?: ApiStory,
isReplyPrivate?: boolean,
) { ) {
const { const {
openChat, showNotification, focusMessage, openMediaViewer, openAudioPlayer, openChat, showNotification, focusMessage, openMediaViewer, openAudioPlayer,
@ -36,9 +38,13 @@ export default function useInnerHandlers(
} = getActions(); } = getActions();
const { const {
id: messageId, forwardInfo, replyToMessageId, replyToChatId, replyToTopMessageId, groupedId, id: messageId, forwardInfo, groupedId,
} = message; } = message;
const {
replyToMsgId, replyToPeerId, replyToTopId, isQuote,
} = getMessageReplyInfo(message) || {};
const handleAvatarClick = useLastCallback(() => { const handleAvatarClick = useLastCallback(() => {
if (!avatarPeer) { if (!avatarPeer) {
return; return;
@ -70,12 +76,19 @@ export default function useInnerHandlers(
}); });
const handleReplyClick = useLastCallback((): void => { const handleReplyClick = useLastCallback((): void => {
if (!replyToMsgId || isReplyPrivate) {
showNotification({
message: isQuote ? lang('QuotePrivate') : lang('ReplyPrivate'),
});
return;
}
focusMessage({ focusMessage({
chatId: isChatWithRepliesBot && replyToChatId ? replyToChatId : chatId, chatId: replyToPeerId || chatId,
threadId, threadId,
messageId: replyToMessageId!, messageId: replyToMsgId,
replyMessageId: isChatWithRepliesBot && replyToChatId ? undefined : messageId, replyMessageId: replyToPeerId ? undefined : messageId,
noForumTopicPanel: true, noForumTopicPanel: !replyToPeerId ? true : undefined, // Open topic panel for cross-chat replies
}); });
}); });
@ -140,10 +153,10 @@ export default function useInnerHandlers(
return; return;
} }
if (isChatWithRepliesBot && replyToChatId) { if (replyToPeerId && replyToTopId) {
focusMessageInComments({ focusMessageInComments({
chatId: replyToChatId, chatId: replyToPeerId,
threadId: replyToTopMessageId!, threadId: replyToTopId,
messageId: forwardInfo!.fromMessageId!, messageId: forwardInfo!.fromMessageId!,
}); });
} else { } else {
@ -175,7 +188,7 @@ export default function useInnerHandlers(
const handleTopicChipClick = useLastCallback(() => { const handleTopicChipClick = useLastCallback(() => {
if (!messageTopic) return; if (!messageTopic) return;
focusMessage({ focusMessage({
chatId: isChatWithRepliesBot && replyToChatId ? replyToChatId : chatId, chatId: replyToPeerId || chatId,
threadId: messageTopic.id, threadId: messageTopic.id,
messageId, messageId,
}); });

View File

@ -38,7 +38,7 @@ export default function useOuterHandlers(
shouldHandleMouseLeave: boolean, shouldHandleMouseLeave: boolean,
getIsMessageListReady: Signal<boolean>, getIsMessageListReady: Signal<boolean>,
) { ) {
const { setReplyingToId, sendDefaultReaction } = getActions(); const { updateDraftReplyInfo, sendDefaultReaction } = getActions();
const [isQuickReactionVisible, markQuickReactionVisible, unmarkQuickReactionVisible] = useFlag(); const [isQuickReactionVisible, markQuickReactionVisible, unmarkQuickReactionVisible] = useFlag();
const [isSwiped, markSwiped, unmarkSwiped] = useFlag(); const [isSwiped, markSwiped, unmarkSwiped] = useFlag();
@ -138,7 +138,7 @@ export default function useOuterHandlers(
function handleContainerDoubleClick() { function handleContainerDoubleClick() {
if (IS_TOUCH_ENV || !canReply) return; if (IS_TOUCH_ENV || !canReply) return;
setReplyingToId({ messageId }); updateDraftReplyInfo({ replyToMsgId: messageId });
} }
function stopPropagation(e: React.MouseEvent<HTMLDivElement, MouseEvent>) { function stopPropagation(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
@ -172,14 +172,14 @@ export default function useOuterHandlers(
return; return;
} }
setReplyingToId({ messageId }); updateDraftReplyInfo({ replyToMsgId: messageId });
setTimeout(unmarkSwiped, Math.max(0, SWIPE_ANIMATION_DURATION - (Date.now() - startedAt))); setTimeout(unmarkSwiped, Math.max(0, SWIPE_ANIMATION_DURATION - (Date.now() - startedAt)));
startedAt = undefined; startedAt = undefined;
}, },
}); });
}, [ }, [
containerRef, isInSelectMode, messageId, setReplyingToId, markSwiped, unmarkSwiped, canReply, isContextMenuShown, containerRef, isInSelectMode, messageId, markSwiped, unmarkSwiped, canReply, isContextMenuShown,
getIsMessageListReady, getIsMessageListReady,
]); ]);

View File

@ -1,7 +1,7 @@
import React, { memo, useMemo } from '../../../lib/teact/teact'; import React, { memo, useMemo } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import type { ApiApplyBoostInfo, ApiChat } from '../../../api/types'; import type { ApiChat, ApiMyBoost } from '../../../api/types';
import type { TabState } from '../../../global/types'; import type { TabState } from '../../../global/types';
import { getChatTitle } from '../../../global/helpers'; import { getChatTitle } from '../../../global/helpers';
@ -26,7 +26,7 @@ import Modal from '../../ui/Modal';
import styles from './BoostModal.module.scss'; import styles from './BoostModal.module.scss';
type LoadedParams = { type LoadedParams = {
applyInfo?: ApiApplyBoostInfo; boost?: ApiMyBoost;
leftText: string; leftText: string;
rightText?: string; rightText?: string;
value: string; value: string;
@ -93,7 +93,7 @@ const BoostModal = ({
const { const {
isStatusLoaded, isStatusLoaded,
isBoosted, isBoosted,
applyInfo, boost,
title, title,
leftText, leftText,
rightText, rightText,
@ -112,6 +112,8 @@ const BoostModal = ({
level, currentLevelBoosts, hasMyBoost, level, currentLevelBoosts, hasMyBoost,
} = info.boostStatus; } = info.boostStatus;
const firstBoost = info?.myBoosts && getFirstAvailableBoost(info.myBoosts);
const { const {
boosts, boosts,
currentLevel, currentLevel,
@ -120,7 +122,7 @@ const BoostModal = ({
remainingBoosts, remainingBoosts,
} = getBoostProgressInfo(info.boostStatus, true); } = getBoostProgressInfo(info.boostStatus, true);
const hasBoost = hasMyBoost || info.applyInfo?.type === 'already'; const hasBoost = hasMyBoost;
const isJustUpgraded = boosts === currentLevelBoosts && hasBoost; const isJustUpgraded = boosts === currentLevelBoosts && hasBoost;
const left = lang('BoostsLevel', currentLevel); const left = lang('BoostsLevel', currentLevel);
@ -159,16 +161,17 @@ const BoostModal = ({
progress: levelProgress, progress: levelProgress,
remainingBoosts, remainingBoosts,
descriptionText: description, descriptionText: description,
applyInfo: info.applyInfo, boost: firstBoost,
isBoosted: hasBoost, isBoosted: hasBoost,
}; };
}, [chat, chatTitle, info, lang]); }, [chat, chatTitle, info, lang]);
const isBoostDisabled = !applyInfo && isCurrentUserPremium; const isBoostDisabled = !boost && isCurrentUserPremium;
const isReplacingBoost = boost?.chatId && boost.chatId !== info?.chatId;
const handleApplyBoost = useLastCallback(() => { const handleApplyBoost = useLastCallback(() => {
closeReplaceModal(); closeReplaceModal();
applyBoost({ chatId: chat!.id }); applyBoost({ chatId: chat!.id, slots: [boost!.slot] });
requestConfetti(); requestConfetti();
}); });
@ -179,8 +182,11 @@ const BoostModal = ({
}); });
const handleButtonClick = useLastCallback(() => { const handleButtonClick = useLastCallback(() => {
if (!isCurrentUserPremium) { if (!boost) {
openPremiumDialog(); if (!isCurrentUserPremium) {
openPremiumDialog();
}
return; return;
} }
@ -189,17 +195,17 @@ const BoostModal = ({
return; return;
} }
if (applyInfo?.type === 'ok') { if (boost.cooldownUntil) {
handleApplyBoost();
}
if (applyInfo?.type === 'replace') {
openReplaceModal();
}
if (applyInfo?.type === 'wait') {
openWaitDialog(); openWaitDialog();
return;
} }
if (isReplacingBoost) {
openReplaceModal();
return;
}
handleApplyBoost();
}); });
const handleCloseClick = useLastCallback(() => { const handleCloseClick = useLastCallback(() => {
@ -249,7 +255,7 @@ const BoostModal = ({
onClose={closeBoostModal} onClose={closeBoostModal}
> >
{renderContent()} {renderContent()}
{applyInfo?.type === 'replace' && boostedChatTitle && ( {isReplacingBoost && boostedChatTitle && (
<Modal <Modal
isOpen={isReplaceModalOpen} isOpen={isReplaceModalOpen}
className={styles.replaceModal} className={styles.replaceModal}
@ -277,7 +283,7 @@ const BoostModal = ({
</div> </div>
</Modal> </Modal>
)} )}
{applyInfo?.type === 'wait' && ( {boost?.cooldownUntil && (
<ConfirmDialog <ConfirmDialog
isOpen={isWaitDialogOpen} isOpen={isWaitDialogOpen}
isOnlyConfirm isOnlyConfirm
@ -289,7 +295,7 @@ const BoostModal = ({
{renderText( {renderText(
lang( lang(
'ChannelBoost.Error.BoostTooOftenText', 'ChannelBoost.Error.BoostTooOftenText',
formatDateInFuture(lang, getServerTime(), applyInfo.waitUntil), formatDateInFuture(lang, getServerTime(), boost.cooldownUntil),
), ),
['simple_markdown', 'emoji'], ['simple_markdown', 'emoji'],
)} )}
@ -310,11 +316,15 @@ const BoostModal = ({
); );
}; };
function getFirstAvailableBoost(myBoosts: ApiMyBoost[]) {
return myBoosts.find((boost) => !boost.chatId) || myBoosts.sort((a, b) => a.date - b.date)[0];
}
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global, { info }): StateProps => { (global, { info }): StateProps => {
const chat = info && selectChat(global, info?.chatId); const chat = info && selectChat(global, info?.chatId);
const boostedChat = info?.applyInfo?.type === 'replace' const firstBoost = info?.myBoosts && getFirstAvailableBoost(info.myBoosts);
? selectChat(global, info.applyInfo.boostedChatId) : undefined; const boostedChat = firstBoost?.chatId ? selectChat(global, firstBoost?.chatId) : undefined;
return { return {
chat, chat,

View File

@ -107,6 +107,7 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
} = getActions(); } = getActions();
const [mainButton, setMainButton] = useState<WebAppButton | undefined>(); const [mainButton, setMainButton] = useState<WebAppButton | undefined>();
const [isBackButtonVisible, setIsBackButtonVisible] = useState(false); const [isBackButtonVisible, setIsBackButtonVisible] = useState(false);
const [isSettingsButtonVisible, setIsSettingsButtonVisible] = useState(false);
const [backgroundColor, setBackgroundColor] = useState<string>(); const [backgroundColor, setBackgroundColor] = useState<string>();
const [headerColor, setHeaderColor] = useState<string>(); const [headerColor, setHeaderColor] = useState<string>();
@ -136,7 +137,7 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
const lang = useLang(); const lang = useLang();
const { const {
url, buttonText, queryId, replyToMessageId, threadId, url, buttonText, queryId, replyInfo,
} = webApp || {}; } = webApp || {};
const isOpen = Boolean(url); const isOpen = Boolean(url);
const isSimple = Boolean(buttonText); const isSimple = Boolean(buttonText);
@ -152,8 +153,7 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
botId: bot!.id, botId: bot!.id,
queryId: queryId!, queryId: queryId!,
peerId: chat!.id, peerId: chat!.id,
replyToMessageId, replyInfo,
threadId,
}); });
}, queryId ? PROLONG_INTERVAL : undefined, true); }, queryId ? PROLONG_INTERVAL : undefined, true);
@ -345,6 +345,7 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
setIsRequestingWriteAccess(false); setIsRequestingWriteAccess(false);
setMainButton(undefined); setMainButton(undefined);
setIsBackButtonVisible(false); setIsBackButtonVisible(false);
setIsSettingsButtonVisible(false);
setBackgroundColor(themeParams.bg_color); setBackgroundColor(themeParams.bg_color);
setHeaderColor(themeParams.bg_color); setHeaderColor(themeParams.bg_color);
markUnloaded(); markUnloaded();
@ -363,6 +364,10 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
setIsBackButtonVisible(eventData.is_visible); setIsBackButtonVisible(eventData.is_visible);
} }
if (eventType === 'web_app_setup_settings_button') {
setIsSettingsButtonVisible(eventData.is_visible);
}
if (eventType === 'web_app_set_background_color') { if (eventType === 'web_app_set_background_color') {
const themeParams = extractCurrentThemeParams(); const themeParams = extractCurrentThemeParams();
const color = validateHexColor(eventData.color) ? eventData.color : themeParams.bg_color; const color = validateHexColor(eventData.color) ? eventData.color : themeParams.bg_color;
@ -520,7 +525,7 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
<MenuItem icon="bots" onClick={openBotChat}>{lang('BotWebViewOpenBot')}</MenuItem> <MenuItem icon="bots" onClick={openBotChat}>{lang('BotWebViewOpenBot')}</MenuItem>
)} )}
<MenuItem icon="reload" onClick={handleRefreshClick}>{lang('WebApp.ReloadPage')}</MenuItem> <MenuItem icon="reload" onClick={handleRefreshClick}>{lang('WebApp.ReloadPage')}</MenuItem>
{attachBot?.hasSettings && ( {isSettingsButtonVisible && (
<MenuItem icon="settings" onClick={handleSettingsButtonClick}> <MenuItem icon="settings" onClick={handleSettingsButtonClick}>
{lang('Settings')} {lang('Settings')}
</MenuItem> </MenuItem>

View File

@ -1,4 +1,4 @@
@import '../../styles/mixins'; @use '../../styles/mixins';
.root { .root {
position: relative; position: relative;
@ -19,7 +19,7 @@
justify-content: center; justify-content: center;
flex-direction: column; flex-direction: column;
@include side-panel-section; @include mixins.side-panel-section;
} }
.general { .general {

View File

@ -1,4 +1,4 @@
@import '../../styles/mixins'; @use '../../styles/mixins';
.Profile { .Profile {
height: 100%; height: 100%;
@ -18,7 +18,7 @@
> .profile-info > .ChatExtra { > .profile-info > .ChatExtra {
padding: 0.875rem 0.5rem 0.5rem; padding: 0.875rem 0.5rem 0.5rem;
@include side-panel-section; @include mixins.side-panel-section;
.narrow { .narrow {
margin-bottom: 0; margin-bottom: 0;

View File

@ -1,4 +1,4 @@
@import "../../../styles/mixins"; @use "../../../styles/mixins";
.Management { .Management {
position: relative; position: relative;
@ -17,7 +17,7 @@
.section { .section {
padding: 1rem 1.5rem; padding: 1rem 1.5rem;
@include side-panel-section; @include mixins.side-panel-section;
&.wide { &.wide {
padding: 1.5rem; padding: 1.5rem;
@ -165,7 +165,7 @@
padding: 0 1rem 0.25rem 0.75rem; padding: 0 1rem 0.25rem 0.75rem;
margin-bottom: 0.625rem; margin-bottom: 0.625rem;
@include side-panel-section; @include mixins.side-panel-section;
display: flex; display: flex;
flex-flow: row wrap; flex-flow: row wrap;
@ -339,7 +339,7 @@
margin: 0 -1.5rem; margin: 0 -1.5rem;
padding: 0 1.5rem 1rem; padding: 0 1.5rem 1rem;
@include side-panel-section; @include mixins.side-panel-section;
} }
.section, .part { .section, .part {

View File

@ -1,4 +1,4 @@
@import '../../../styles/mixins'; @use '../../../styles/mixins';
.root { .root {
overflow-y: scroll; overflow-y: scroll;
@ -18,7 +18,7 @@
.section { .section {
padding: 1rem; padding: 1rem;
@include side-panel-section; @include mixins.side-panel-section;
} }
.user :global(.status) { .user :global(.status) {

View File

@ -1,4 +1,4 @@
@import "../../styles/mixins"; @use "../../styles/mixins";
.root { .root {
--color-story-meta: rgb(242, 242, 242); --color-story-meta: rgb(242, 242, 242);
@ -532,7 +532,7 @@
overflow-y: scroll; overflow-y: scroll;
scrollbar-gutter: stable; scrollbar-gutter: stable;
@include adapt-padding-to-scrollbar(2rem); @include mixins.adapt-padding-to-scrollbar(2rem);
} }
.captionContent { .captionContent {
@ -552,7 +552,7 @@
.expanded { .expanded {
transition: transform 400ms; transition: transform 400ms;
@include gradient-border-top(2rem); @include mixins.gradient-border-top(2rem);
&::before { &::before {
opacity: 1; opacity: 1;

View File

@ -1,4 +1,4 @@
@import "../../styles/mixins"; @use "../../styles/mixins";
.AvatarEditable { .AvatarEditable {
label { label {
@ -39,7 +39,7 @@
} }
// @optimization The weirdest workaround for screen animation lag // @optimization The weirdest workaround for screen animation lag
@include while-transition() { @include mixins.while-transition() {
input, input,
.icon, .icon,
&::after { &::after {

View File

@ -397,4 +397,8 @@
&.premium { &.premium {
background: var(--premium-gradient); background: var(--premium-gradient);
} }
&.rectangular {
border-radius: 0;
}
} }

View File

@ -43,6 +43,7 @@ export type OwnProps = {
tabIndex?: number; tabIndex?: number;
isRtl?: boolean; isRtl?: boolean;
isShiny?: boolean; isShiny?: boolean;
isRectangular?: boolean;
withPremiumGradient?: boolean; withPremiumGradient?: boolean;
noPreventDefault?: boolean; noPreventDefault?: boolean;
shouldStopPropagation?: boolean; shouldStopPropagation?: boolean;
@ -96,6 +97,7 @@ const Button: FC<OwnProps> = ({
faded, faded,
tabIndex, tabIndex,
isRtl, isRtl,
isRectangular,
noPreventDefault, noPreventDefault,
shouldStopPropagation, shouldStopPropagation,
style, style,
@ -126,6 +128,7 @@ const Button: FC<OwnProps> = ({
backgroundImage && 'with-image', backgroundImage && 'with-image',
isShiny && 'shiny', isShiny && 'shiny',
withPremiumGradient && 'premium', withPremiumGradient && 'premium',
isRectangular && 'rectangular',
); );
const handleClick = useLastCallback((e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => { const handleClick = useLastCallback((e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => {

View File

@ -1,4 +1,4 @@
@import "../../styles/mixins"; @use "../../styles/mixins";
@mixin thumb-styles() { @mixin thumb-styles() {
background: var(--slider-color); background: var(--slider-color);
@ -86,7 +86,7 @@
} }
// Reset range input browser styles // Reset range input browser styles
@include reset-range(); @include mixins.reset-range();
// Apply custom styles // Apply custom styles
input[type="range"] { input[type="range"] {

View File

@ -52,7 +52,7 @@ export const CUSTOM_EMOJI_PREVIEW_CACHE_DISABLED = false;
export const CUSTOM_EMOJI_PREVIEW_CACHE_NAME = 'tt-custom-emoji-preview'; export const CUSTOM_EMOJI_PREVIEW_CACHE_NAME = 'tt-custom-emoji-preview';
export const MEDIA_CACHE_MAX_BYTES = 512 * 1024; // 512 KB export const MEDIA_CACHE_MAX_BYTES = 512 * 1024; // 512 KB
export const CUSTOM_BG_CACHE_NAME = 'tt-custom-bg'; export const CUSTOM_BG_CACHE_NAME = 'tt-custom-bg';
export const LANG_CACHE_NAME = 'tt-lang-packs-v24'; export const LANG_CACHE_NAME = 'tt-lang-packs-v25';
export const ASSET_CACHE_NAME = 'tt-assets'; export const ASSET_CACHE_NAME = 'tt-assets';
export const AUTODOWNLOAD_FILESIZE_MB_LIMITS = [1, 5, 10, 50, 100, 500]; export const AUTODOWNLOAD_FILESIZE_MB_LIMITS = [1, 5, 10, 50, 100, 500];
export const DATA_BROADCAST_CHANNEL_NAME = 'tt-global'; export const DATA_BROADCAST_CHANNEL_NAME = 'tt-global';
@ -311,6 +311,9 @@ export const LIGHT_THEME_BG_COLOR = '#99BA92';
export const DARK_THEME_BG_COLOR = '#0F0F0F'; export const DARK_THEME_BG_COLOR = '#0F0F0F';
export const DEFAULT_PATTERN_COLOR = '#4A8E3A8C'; export const DEFAULT_PATTERN_COLOR = '#4A8E3A8C';
export const DARK_THEME_PATTERN_COLOR = '#0A0A0A8C'; export const DARK_THEME_PATTERN_COLOR = '#0A0A0A8C';
export const PEER_COLOR_BG_OPACITY = '1a';
export const PEER_COLOR_BG_ACTIVE_OPACITY = '2b';
export const PEER_COLOR_GRADIENT_STEP = 5; // px
export const MAX_UPLOAD_FILEPART_SIZE = 524288; export const MAX_UPLOAD_FILEPART_SIZE = 524288;
// Group calls // Group calls

View File

@ -1,10 +1,9 @@
import type { import type {
ApiChat, ApiChatType, ApiContact, ApiPeer, ApiUrlAuthResult, ApiChat, ApiChatType, ApiContact, ApiInputMessageReplyInfo, ApiPeer, ApiUrlAuthResult,
} from '../../../api/types'; } from '../../../api/types';
import type { InlineBotSettings } from '../../../types'; import type { InlineBotSettings } from '../../../types';
import type { RequiredGlobalActions } from '../../index'; import type { RequiredGlobalActions } from '../../index';
import type { ActionReturnType, GlobalState, TabArgs } from '../../types'; import type { ActionReturnType, GlobalState, TabArgs } from '../../types';
import { MAIN_THREAD_ID } from '../../../api/types';
import { GENERAL_REFETCH_INTERVAL } from '../../../config'; import { GENERAL_REFETCH_INTERVAL } from '../../../config';
import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { getCurrentTabId } from '../../../util/establishMultitabRole';
@ -23,8 +22,8 @@ import { addChats, addUsers, removeBlockedUser } from '../../reducers';
import { replaceInlineBotSettings, replaceInlineBotsIsLoading } from '../../reducers/bots'; import { replaceInlineBotSettings, replaceInlineBotsIsLoading } from '../../reducers/bots';
import { updateTabState } from '../../reducers/tabs'; import { updateTabState } from '../../reducers/tabs';
import { import {
selectBot, selectChat, selectChatMessage, selectCurrentChat, selectCurrentMessageList, selectIsTrustedBot, selectBot, selectChat, selectChatMessage, selectCurrentChat, selectCurrentMessageList, selectDraft,
selectReplyingToId, selectSendAs, selectTabState, selectThreadTopMessageId, selectUser, selectUserFullInfo, selectIsTrustedBot, selectMessageReplyInfo, selectSendAs, selectTabState, selectUser, selectUserFullInfo,
} from '../../selectors'; } from '../../selectors';
const GAMEE_URL = 'https://prizes.gamee.com/'; const GAMEE_URL = 'https://prizes.gamee.com/';
@ -185,11 +184,11 @@ addActionHandler('sendBotCommand', (global, actions, payload): ActionReturnType
} }
const { threadId } = currentMessageList; const { threadId } = currentMessageList;
actions.setReplyingToId({ messageId: undefined, tabId }); actions.resetDraftReplyInfo({ tabId });
actions.clearWebPagePreview({ tabId }); actions.clearWebPagePreview({ tabId });
void sendBotCommand( void sendBotCommand(
chat, threadId, command, selectReplyingToId(global, chat.id, threadId), selectSendAs(global, chat.id), chat, command, selectDraft(global, chat.id, threadId)?.replyInfo, selectSendAs(global, chat.id),
); );
}); });
@ -210,7 +209,7 @@ addActionHandler('restartBot', async (global, actions, payload): Promise<void> =
global = getGlobal(); global = getGlobal();
global = removeBlockedUser(global, bot.id); global = removeBlockedUser(global, bot.id);
setGlobal(global); setGlobal(global);
void sendBotCommand(chat, MAIN_THREAD_ID, '/start', undefined, selectSendAs(global, chatId)); void sendBotCommand(chat, '/start', undefined, selectSendAs(global, chatId));
}); });
addActionHandler('loadTopInlineBots', async (global): Promise<void> => { addActionHandler('loadTopInlineBots', async (global): Promise<void> => {
@ -339,21 +338,18 @@ addActionHandler('sendInlineBotResult', (global, actions, payload): ActionReturn
const { chatId, threadId } = messageList; const { chatId, threadId } = messageList;
const chat = selectChat(global, chatId)!; const chat = selectChat(global, chatId)!;
const replyingToId = selectReplyingToId(global, chatId, threadId); const draftReplyInfo = selectDraft(global, chatId, threadId)?.replyInfo;
const replyingToMessage = replyingToId ? selectChatMessage(global, chatId, replyingToId) : undefined;
const replyingToTopId = (chat.isForum || threadId !== MAIN_THREAD_ID)
? selectThreadTopMessageId(global, chatId, threadId)
: replyingToMessage?.replyToTopMessageId || replyingToMessage?.replyToMessageId;
actions.setReplyingToId({ messageId: undefined, tabId }); const replyInfo = selectMessageReplyInfo(global, chatId, threadId, draftReplyInfo);
actions.resetDraftReplyInfo({ tabId });
actions.clearWebPagePreview({ tabId }); actions.clearWebPagePreview({ tabId });
void callApi('sendInlineBotResult', { void callApi('sendInlineBotResult', {
chat, chat,
resultId: id, resultId: id,
queryId, queryId,
replyingTo: replyingToId || replyingToTopId, replyInfo,
replyingToTopId,
sendAs: selectSendAs(global, chatId), sendAs: selectSendAs(global, chatId),
isSilent, isSilent,
scheduleDate: scheduledAt, scheduleDate: scheduledAt,
@ -531,7 +527,9 @@ addActionHandler('requestWebView', async (global, actions, payload): Promise<voi
} }
const { chatId, threadId } = currentMessageList; const { chatId, threadId } = currentMessageList;
const reply = chatId && selectReplyingToId(global, chatId, threadId); const draftReplyInfo = chatId ? selectDraft(global, chatId, threadId)?.replyInfo : undefined;
const replyInfo = selectMessageReplyInfo(global, chatId, threadId, draftReplyInfo);
const sendAs = selectSendAs(global, chatId); const sendAs = selectSendAs(global, chatId);
const result = await callApi('requestWebView', { const result = await callApi('requestWebView', {
url, url,
@ -539,8 +537,7 @@ addActionHandler('requestWebView', async (global, actions, payload): Promise<voi
peer, peer,
theme, theme,
isSilent, isSilent,
replyToMessageId: reply || undefined, replyInfo,
threadId,
isFromBotMenu, isFromBotMenu,
startParam, startParam,
sendAs, sendAs,
@ -557,8 +554,7 @@ addActionHandler('requestWebView', async (global, actions, payload): Promise<voi
url: webViewUrl, url: webViewUrl,
botId, botId,
queryId, queryId,
replyToMessageId: reply || undefined, replyInfo,
threadId,
buttonText, buttonText,
}, },
}, tabId); }, tabId);
@ -626,8 +622,7 @@ addActionHandler('requestAppWebView', async (global, actions, payload): Promise<
addActionHandler('prolongWebView', async (global, actions, payload): Promise<void> => { addActionHandler('prolongWebView', async (global, actions, payload): Promise<void> => {
const { const {
botId, peerId, isSilent, replyToMessageId, queryId, threadId, botId, peerId, isSilent, replyInfo, queryId, tabId = getCurrentTabId(),
tabId = getCurrentTabId(),
} = payload; } = payload;
const bot = selectUser(global, botId); const bot = selectUser(global, botId);
@ -641,8 +636,7 @@ addActionHandler('prolongWebView', async (global, actions, payload): Promise<voi
bot, bot,
peer, peer,
isSilent, isSilent,
replyToMessageId, replyInfo,
threadId,
queryId, queryId,
sendAs, sendAs,
}); });
@ -769,7 +763,8 @@ addActionHandler('callAttachBot', (global, actions, payload): ActionReturnType =
const isFromBotMenu = !bot; const isFromBotMenu = !bot;
const shouldDisplayDisclaimer = (!isFromBotMenu && !global.attachMenu.bots[bot.id]) const shouldDisplayDisclaimer = (!isFromBotMenu && !global.attachMenu.bots[bot.id])
|| (isFromSideMenu && (bot?.isInactive || bot?.isDisclaimerNeeded)); || bot?.isInactive || bot?.isDisclaimerNeeded;
if (!isFromConfirm && shouldDisplayDisclaimer) { if (!isFromConfirm && shouldDisplayDisclaimer) {
return updateTabState(global, { return updateTabState(global, {
requestedAttachBotInstall: { requestedAttachBotInstall: {
@ -1068,14 +1063,11 @@ async function searchInlineBot<T extends GlobalState>(global: T, {
} }
async function sendBotCommand( async function sendBotCommand(
chat: ApiChat, threadId = MAIN_THREAD_ID, command: string, replyingTo?: number, sendAs?: ApiPeer, chat: ApiChat, command: string, replyInfo?: ApiInputMessageReplyInfo, sendAs?: ApiPeer,
) { ) {
await callApi('sendMessage', { await callApi('sendMessage', {
chat, chat,
replyingTo: replyingTo ? { replyInfo,
replyingTo,
replyingToTopId: threadId,
} : undefined,
text: command, text: command,
sendAs, sendAs,
}); });

View File

@ -294,7 +294,7 @@ addActionHandler('loadAllChats', async (global, actions, payload): Promise<void>
let i = 0; let i = 0;
const getOrderDate = (chat: ApiChat) => { const getOrderDate = (chat: ApiChat) => {
return chat.lastMessage?.date || chat.joinDate; return chat.lastMessage?.date || chat.creationDate;
}; };
while (shouldReplace || !global.chats.isFullyLoaded[listType]) { while (shouldReplace || !global.chats.isFullyLoaded[listType]) {
@ -1831,8 +1831,7 @@ addActionHandler('loadTopics', async (global, actions, payload): Promise<void> =
global = updateTopics(global, chatId, result.count, result.topics); global = updateTopics(global, chatId, result.count, result.topics);
global = updateListedTopicIds(global, chatId, result.topics.map((topic) => topic.id)); global = updateListedTopicIds(global, chatId, result.topics.map((topic) => topic.id));
Object.entries(result.draftsById || {}).forEach(([threadId, draft]) => { Object.entries(result.draftsById || {}).forEach(([threadId, draft]) => {
global = replaceThreadParam(global, chatId, Number(threadId), 'draft', draft?.formattedText); global = replaceThreadParam(global, chatId, Number(threadId), 'draft', draft);
global = replaceThreadParam(global, chatId, Number(threadId), 'replyingToId', draft?.replyingToId);
}); });
Object.entries(result.readInboxMessageIdByTopicId || {}).forEach(([topicId, messageId]) => { Object.entries(result.readInboxMessageIdByTopicId || {}).forEach(([topicId, messageId]) => {
global = updateThreadInfo(global, chatId, Number(topicId), { lastReadInboxMessageId: messageId }); global = updateThreadInfo(global, chatId, Number(topicId), { lastReadInboxMessageId: messageId });
@ -2419,18 +2418,6 @@ async function loadChats(
} }
}); });
const idsToUpdateReplyingToId = isFullDraftSync ? result.chatIds : Object.keys(result.replyingToById);
idsToUpdateReplyingToId.forEach((chatId) => {
const replyingToById = result.replyingToById[chatId];
const thread = selectThread(global, chatId, MAIN_THREAD_ID);
if (!replyingToById && !thread) return;
global = replaceThreadParam(
global, chatId, MAIN_THREAD_ID, 'replyingToId', replyingToById,
);
});
if (chatIds.length === 0 && !global.chats.isFullyLoaded[listType]) { if (chatIds.length === 0 && !global.chats.isFullyLoaded[listType]) {
global = { global = {
...global, ...global,

View File

@ -1,6 +1,9 @@
import type { import type {
ApiAttachment, ApiAttachment,
ApiChat, ApiChat,
ApiInputMessageReplyInfo,
ApiInputReplyInfo,
ApiInputStoryReplyInfo,
ApiMessage, ApiMessage,
ApiMessageEntity, ApiMessageEntity,
ApiNewPoll, ApiNewPoll,
@ -9,12 +12,11 @@ import type {
ApiSticker, ApiSticker,
ApiStory, ApiStory,
ApiStorySkipped, ApiStorySkipped,
ApiTypeReplyTo,
ApiVideo, ApiVideo,
} from '../../../api/types'; } from '../../../api/types';
import type { RequiredGlobalActions } from '../../index'; import type { RequiredGlobalActions } from '../../index';
import type { import type {
ActionReturnType, GlobalState, TabArgs, ActionReturnType, ApiDraft, GlobalState, TabArgs,
} from '../../types'; } from '../../types';
import { MAIN_THREAD_ID, MESSAGE_DELETED } from '../../../api/types'; import { MAIN_THREAD_ID, MESSAGE_DELETED } from '../../../api/types';
import { LoadMoreDirection } from '../../../types'; import { LoadMoreDirection } from '../../../types';
@ -93,12 +95,12 @@ import {
selectIsCurrentUserPremium, selectIsCurrentUserPremium,
selectLanguageCode, selectLanguageCode,
selectListedIds, selectListedIds,
selectMessageReplyInfo,
selectNoWebPage, selectNoWebPage,
selectOutlyingListByMessageId, selectOutlyingListByMessageId,
selectPeerStory, selectPeerStory,
selectPinnedIds, selectPinnedIds,
selectRealLastReadId, selectRealLastReadId,
selectReplyingToId,
selectScheduledMessage, selectScheduledMessage,
selectSendAs, selectSendAs,
selectSponsoredMessage, selectSponsoredMessage,
@ -268,27 +270,30 @@ addActionHandler('sendMessage', (global, actions, payload): ActionReturnType =>
} }
const chat = selectChat(global, chatId!)!; const chat = selectChat(global, chatId!)!;
const replyingToId = !isStoryReply ? selectReplyingToId(global, chatId!, threadId!) : undefined; const draftReplyInfo = !isStoryReply ? selectDraft(global, chatId!, threadId!)?.replyInfo : undefined;
const replyingToMessage = replyingToId ? selectChatMessage(global, chatId!, replyingToId) : undefined;
const replyingToTopId = chat.isForum const storyReplyInfo = isStoryReply ? {
? selectThreadTopMessageId(global, chatId!, threadId!) type: 'story',
: replyingToMessage?.replyToTopMessageId || replyingToMessage?.replyToMessageId; userId: storyPeerId!,
const replyingTo: ApiTypeReplyTo | undefined = replyingToId storyId: storyId!,
? { replyingTo: replyingToId, replyingToTopId } } satisfies ApiInputStoryReplyInfo : undefined;
: (isStoryReply ? { userId: storyPeerId!, storyId: storyId! } : undefined);
const messageReplyInfo = selectMessageReplyInfo(global, chatId!, threadId!, draftReplyInfo);
const replyInfo = storyReplyInfo || messageReplyInfo;
const params = { const params = {
...payload, ...payload,
chat, chat,
currentThreadId: threadId!, replyInfo,
replyingTo,
noWebPage: selectNoWebPage(global, chatId!, threadId!), noWebPage: selectNoWebPage(global, chatId!, threadId!),
sendAs: selectSendAs(global, chatId!), sendAs: selectSendAs(global, chatId!),
}; };
actions.setReplyingToId({ messageId: undefined, tabId }); if (!isStoryReply) {
actions.clearWebPagePreview({ tabId }); actions.resetDraftReplyInfo({ tabId });
actions.clearWebPagePreview({ tabId });
}
const isSingle = !payload.attachments || payload.attachments.length <= 1; const isSingle = !payload.attachments || payload.attachments.length <= 1;
const isGrouped = !isSingle && payload.shouldGroupMessages; const isGrouped = !isSingle && payload.shouldGroupMessages;
@ -332,7 +337,7 @@ addActionHandler('sendMessage', (global, actions, payload): ActionReturnType =>
}); });
} else { } else {
const { const {
text, entities, attachments, replyingTo: replyingToForFirstMessage, ...commonParams text, entities, attachments, replyInfo: replyToForFirstMessage, ...commonParams
} = params; } = params;
if (text) { if (text) {
@ -340,7 +345,7 @@ addActionHandler('sendMessage', (global, actions, payload): ActionReturnType =>
...commonParams, ...commonParams,
text, text,
entities, entities,
replyingTo: replyingToForFirstMessage, replyInfo: replyToForFirstMessage,
}); });
} }
@ -393,63 +398,120 @@ addActionHandler('cancelSendingMessage', (global, actions, payload): ActionRetur
}); });
}); });
addActionHandler('saveDraft', async (global, actions, payload): Promise<void> => { addActionHandler('saveDraft', (global, actions, payload): ActionReturnType => {
const { const {
chatId, threadId, draft, chatId, threadId, text,
} = payload; } = payload;
if (!draft) { if (!text) {
return; return;
} }
const { text, entities } = draft; const currentDraft = selectDraft(global, chatId, threadId);
const chat = selectChat(global, chatId)!;
const user = selectUser(global, chatId)!;
if (user && isDeletedUser(user)) return;
draft.isLocal = true; const newDraft: ApiDraft = {
global = replaceThreadParam(global, chatId, threadId, 'draft', draft); text,
global = updateChat(global, chatId, { draftDate: Math.round(Date.now() / 1000) }); replyInfo: currentDraft?.replyInfo,
};
saveDraft(global, chatId, threadId, newDraft);
});
addActionHandler('clearDraft', (global, actions, payload): ActionReturnType => {
const {
chatId, threadId = MAIN_THREAD_ID, isLocalOnly, shouldKeepReply,
} = payload;
const currentDraft = selectDraft(global, chatId, threadId);
if (!currentDraft) {
return;
}
const newDraft: ApiDraft | undefined = shouldKeepReply ? {
replyInfo: currentDraft.replyInfo,
} : undefined;
if (!isLocalOnly) {
saveDraft(global, chatId, threadId, newDraft);
}
});
addActionHandler('updateDraftReplyInfo', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId(), ...update } = payload;
const currentMessageList = selectCurrentMessageList(global, tabId);
if (!currentMessageList) {
return;
}
const { chatId, threadId } = currentMessageList;
const currentDraft = selectDraft(global, chatId, threadId);
const updatedReplyInfo = {
type: 'message',
...currentDraft?.replyInfo,
...update,
} as ApiInputMessageReplyInfo;
if (!updatedReplyInfo.replyToMsgId) return;
const newDraft: ApiDraft = {
...currentDraft,
replyInfo: updatedReplyInfo,
};
saveDraft(global, chatId, threadId, newDraft);
});
addActionHandler('resetDraftReplyInfo', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload || {};
const currentMessageList = selectCurrentMessageList(global, tabId);
if (!currentMessageList) {
return;
}
const { chatId, threadId } = currentMessageList;
const currentDraft = selectDraft(global, chatId, threadId);
const newDraft: ApiDraft | undefined = !currentDraft?.text ? undefined : {
...currentDraft,
replyInfo: undefined,
};
saveDraft(global, chatId, threadId, newDraft);
});
async function saveDraft<T extends GlobalState>(global: T, chatId: string, threadId: number, draft?: ApiDraft) {
const chat = selectChat(global, chatId);
const user = selectUser(global, chatId);
if (!chat || (user && isDeletedUser(user))) return;
const replyInfo = selectMessageReplyInfo(global, chatId, threadId, draft?.replyInfo);
const newDraft: ApiDraft | undefined = draft ? {
...draft,
replyInfo,
date: Math.floor(Date.now() / 1000),
isLocal: true,
} : undefined;
global = replaceThreadParam(global, chatId, threadId, 'draft', newDraft);
global = updateChat(global, chatId, { draftDate: newDraft?.date });
setGlobal(global); setGlobal(global);
const result = await callApi('saveDraft', { const result = await callApi('saveDraft', {
chat, chat,
text, draft: newDraft,
entities,
replyToMsgId: selectReplyingToId(global, chatId, threadId),
threadId: selectThreadTopMessageId(global, chatId, threadId),
}); });
if (result) { if (result && newDraft) {
draft.isLocal = false; newDraft.isLocal = false;
} }
global = getGlobal(); global = getGlobal();
global = replaceThreadParam(global, chatId, threadId, 'draft', draft); global = replaceThreadParam(global, chatId, threadId, 'draft', newDraft);
global = updateChat(global, chatId, { draftDate: Math.round(Date.now() / 1000) }); global = updateChat(global, chatId, { draftDate: newDraft?.date });
setGlobal(global); setGlobal(global);
}); }
addActionHandler('clearDraft', (global, actions, payload): ActionReturnType => {
const {
chatId, threadId = MAIN_THREAD_ID, localOnly,
} = payload;
if (!selectDraft(global, chatId, threadId)) {
return undefined;
}
const chat = selectChat(global, chatId)!;
if (!localOnly) {
void callApi('clearDraft', chat, selectThreadTopMessageId(global, chatId, threadId));
}
global = replaceThreadParam(global, chatId, threadId, 'draft', undefined);
global = updateChat(global, chatId, { draftDate: undefined });
return global;
});
addActionHandler('toggleMessageWebPage', (global, actions, payload): ActionReturnType => { addActionHandler('toggleMessageWebPage', (global, actions, payload): ActionReturnType => {
const { chatId, threadId, noWebPage } = payload!; const { chatId, threadId, noWebPage } = payload!;
@ -837,10 +899,11 @@ addActionHandler('forwardMessages', (global, actions, payload): ActionReturnType
const { text, entities } = message.content.text || {}; const { text, entities } = message.content.text || {};
const { sticker, poll } = message.content; const { sticker, poll } = message.content;
const replyInfo = selectMessageReplyInfo(global, toChat.id, toThreadId);
void sendMessage(global, { void sendMessage(global, {
chat: toChat, chat: toChat,
replyingTo: toThreadId ? { replyingTo: toThreadId, replyingToTopId: toThreadId } : undefined, replyInfo,
currentThreadId: toThreadId || MAIN_THREAD_ID,
text, text,
entities, entities,
sticker, sticker,
@ -1103,7 +1166,7 @@ async function loadMessage<T extends GlobalState>(
const replyMessage = selectChatMessage(global, chat.id, replyOriginForId); const replyMessage = selectChatMessage(global, chat.id, replyOriginForId);
global = updateChatMessage(global, chat.id, replyOriginForId, { global = updateChatMessage(global, chat.id, replyOriginForId, {
...replyMessage, ...replyMessage,
replyToMessageId: undefined, replyInfo: undefined,
}); });
setGlobal(global); setGlobal(global);
} }
@ -1174,7 +1237,7 @@ async function sendMessage<T extends GlobalState>(global: T, params: {
chat: ApiChat; chat: ApiChat;
text?: string; text?: string;
entities?: ApiMessageEntity[]; entities?: ApiMessageEntity[];
replyingTo?: ApiTypeReplyTo; replyInfo?: ApiInputReplyInfo;
attachment?: ApiAttachment; attachment?: ApiAttachment;
sticker?: ApiSticker; sticker?: ApiSticker;
story?: ApiStory | ApiStorySkipped; story?: ApiStory | ApiStorySkipped;
@ -1183,7 +1246,6 @@ async function sendMessage<T extends GlobalState>(global: T, params: {
isSilent?: boolean; isSilent?: boolean;
scheduledAt?: number; scheduledAt?: number;
sendAs?: ApiPeer; sendAs?: ApiPeer;
currentThreadId: number;
groupedId?: string; groupedId?: string;
}) { }) {
let localId: number | undefined; let localId: number | undefined;
@ -1208,29 +1270,10 @@ async function sendMessage<T extends GlobalState>(global: T, params: {
} : undefined; } : undefined;
// @optimization // @optimization
if (params.replyingTo || IS_IOS) { if (params.replyInfo || IS_IOS) {
await rafPromise(); await rafPromise();
} }
if (params.currentThreadId === undefined) {
return;
}
if (params.currentThreadId !== MAIN_THREAD_ID) {
if (!params.replyingTo || !('replyingTo' in params.replyingTo)) {
params.replyingTo = {
replyingTo: params.currentThreadId,
};
}
if (!params.replyingTo.replyingTo) {
params.replyingTo.replyingTo = params.currentThreadId;
}
if (params.replyingTo.replyingTo && !params.replyingTo.replyingToTopId) {
params.replyingTo.replyingToTopId = params.currentThreadId;
}
}
await callApi('sendMessage', params, progressCallback); await callApi('sendMessage', params, progressCallback);
if (progressCallback && localId) { if (progressCallback && localId) {
@ -1533,7 +1576,6 @@ addActionHandler('forwardStory', (global, actions, payload): ActionReturnType =>
const { text, entities } = (story as ApiStory).content.text || {}; const { text, entities } = (story as ApiStory).content.text || {};
void sendMessage(global, { void sendMessage(global, {
chat: toChat, chat: toChat,
currentThreadId: MAIN_THREAD_ID,
text, text,
entities, entities,
story, story,

View File

@ -15,6 +15,7 @@ import { setTimeFormat } from '../../../util/langProvider';
import { requestPermission, subscribe, unsubscribe } from '../../../util/notifications'; import { requestPermission, subscribe, unsubscribe } from '../../../util/notifications';
import requestActionTimeout from '../../../util/requestActionTimeout'; import requestActionTimeout from '../../../util/requestActionTimeout';
import { getServerTime } from '../../../util/serverTime'; import { getServerTime } from '../../../util/serverTime';
import { updatePeerColors } from '../../../util/theme';
import { callApi } from '../../../api/gramjs'; import { callApi } from '../../../api/gramjs';
import { buildApiInputPrivacyRules } from '../../helpers'; import { buildApiInputPrivacyRules } from '../../helpers';
import { addActionHandler, getGlobal, setGlobal } from '../../index'; import { addActionHandler, getGlobal, setGlobal } from '../../index';
@ -622,6 +623,10 @@ addActionHandler('loadAppConfig', async (global, actions, payload): Promise<void
appConfig, appConfig,
}; };
setGlobal(global); setGlobal(global);
if (appConfig.peerColors) {
updatePeerColors(appConfig.peerColors, appConfig.darkPeerColors);
}
}); });
addActionHandler('loadConfig', async (global): Promise<void> => { addActionHandler('loadConfig', async (global): Promise<void> => {

View File

@ -535,23 +535,20 @@ addActionHandler('openBoostModal', async (global, actions, payload): Promise<voi
}, tabId); }, tabId);
setGlobal(global); setGlobal(global);
const applyInfoResult = await callApi('fetchCanApplyBoost', { const myBoosts = await callApi('fetchMyBoosts');
chat,
});
if (!applyInfoResult?.info) return; if (!myBoosts) return;
const applyInfo = applyInfoResult.info;
global = getGlobal(); global = getGlobal();
const tabState = selectTabState(global, tabId); const tabState = selectTabState(global, tabId);
if (!tabState.boostModal) return; if (!tabState.boostModal) return;
global = addChats(global, buildCollectionByKey(applyInfoResult.chats, 'id')); global = addChats(global, buildCollectionByKey(myBoosts.chats, 'id'));
global = addUsers(global, buildCollectionByKey(myBoosts.users, 'id'));
global = updateTabState(global, { global = updateTabState(global, {
boostModal: { boostModal: {
...tabState.boostModal, ...tabState.boostModal,
applyInfo, myBoosts: myBoosts.boosts,
}, },
}, tabId); }, tabId);
setGlobal(global); setGlobal(global);
@ -643,12 +640,13 @@ addActionHandler('loadMoreBoosters', async (global, actions, payload): Promise<v
}); });
addActionHandler('applyBoost', async (global, actions, payload): Promise<void> => { addActionHandler('applyBoost', async (global, actions, payload): Promise<void> => {
const { chatId, tabId = getCurrentTabId() } = payload; const { chatId, slots, tabId = getCurrentTabId() } = payload;
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
if (!chat) return; if (!chat) return;
const result = await callApi('applyBoost', { const result = await callApi('applyBoost', {
slots,
chat, chat,
}); });

View File

@ -28,7 +28,7 @@ import {
selectCurrentMessageList, selectCurrentMessageList,
selectDraft, selectDraft,
selectEditingDraft, selectEditingDraft,
selectEditingId, selectReplyingToId, selectEditingId,
selectTabState, selectTabState,
selectThreadInfo, selectThreadInfo,
} from '../../selectors'; } from '../../selectors';
@ -111,7 +111,6 @@ async function loadAndReplaceMessages<T extends GlobalState>(global: T, actions:
draft: selectDraft(global, chatId, Number(threadId)), draft: selectDraft(global, chatId, Number(threadId)),
editingId: selectEditingId(global, chatId, Number(threadId)), editingId: selectEditingId(global, chatId, Number(threadId)),
editingDraft: selectEditingDraft(global, chatId, Number(threadId)), editingDraft: selectEditingDraft(global, chatId, Number(threadId)),
replyingToId: selectReplyingToId(global, chatId, Number(threadId)),
}; };
return acc2; return acc2;

View File

@ -379,16 +379,15 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
case 'draftMessage': { case 'draftMessage': {
const { const {
chatId, formattedText, date, replyingToId, threadId, chatId, threadId, draft,
} = update; } = update;
const chat = global.chats.byId[chatId]; const chat = global.chats.byId[chatId];
if (!chat) { if (!chat) {
return undefined; return undefined;
} }
global = replaceThreadParam(global, chatId, threadId || MAIN_THREAD_ID, 'draft', formattedText); global = replaceThreadParam(global, chatId, threadId || MAIN_THREAD_ID, 'draft', draft);
global = replaceThreadParam(global, chatId, threadId || MAIN_THREAD_ID, 'replyingToId', replyingToId); global = updateChat(global, chatId, { draftDate: draft?.date });
global = updateChat(global, chatId, { draftDate: date });
return global; return global;
} }

View File

@ -17,6 +17,7 @@ import {
checkIfHasUnreadReactions, getMessageContent, getMessageText, isActionMessage, checkIfHasUnreadReactions, getMessageContent, getMessageText, isActionMessage,
isMessageLocal, isUserId, isMessageLocal, isUserId,
} from '../../helpers'; } from '../../helpers';
import { getMessageReplyInfo } from '../../helpers/replies';
import { addActionHandler, getGlobal, setGlobal } from '../../index'; import { addActionHandler, getGlobal, setGlobal } from '../../index';
import { import {
addViewportId, addViewportId,
@ -84,18 +85,19 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
} }
const newMessage = selectChatMessage(global, chatId, id)!; const newMessage = selectChatMessage(global, chatId, id)!;
const replyInfo = getMessageReplyInfo(newMessage);
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
if (chat?.isForum if (chat?.isForum
&& newMessage.isTopicReply && replyInfo?.isForumTopic
&& !selectTopicFromMessage(global, newMessage) && !selectTopicFromMessage(global, newMessage)
&& newMessage.replyToMessageId) { && replyInfo.replyToMsgId) {
actions.loadTopicById({ chatId, topicId: newMessage.replyToMessageId }); actions.loadTopicById({ chatId, topicId: replyInfo.replyToMsgId });
} }
Object.values(global.byTabId).forEach(({ id: tabId }) => { Object.values(global.byTabId).forEach(({ id: tabId }) => {
const isLocal = isMessageLocal(message as ApiMessage); const isLocal = isMessageLocal(message as ApiMessage);
if (selectIsMessageInCurrentMessageList(global, chatId, message as ApiMessage, tabId)) { if (selectIsMessageInCurrentMessageList(global, chatId, message as ApiMessage, tabId)) {
if (isLocal && message.isOutgoing && !(message.content?.action) && !message.replyToStoryId if (isLocal && message.isOutgoing && !(message.content?.action) && !replyInfo?.replyToMsgId
&& !message.content?.storyData) { && !message.content?.storyData) {
const currentMessageList = selectCurrentMessageList(global, tabId); const currentMessageList = selectCurrentMessageList(global, tabId);
if (currentMessageList) { if (currentMessageList) {
@ -122,7 +124,10 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
setTimeout(() => { setTimeout(() => {
global = getGlobal(); global = getGlobal();
if (shouldForceReply) { if (shouldForceReply) {
global = replaceThreadParam(global, chatId, MAIN_THREAD_ID, 'replyingToId', id); actions.updateDraftReplyInfo({
replyToMsgId: id,
tabId,
});
} }
global = updateChatLastMessage(global, chatId, newMessage); global = updateChatLastMessage(global, chatId, newMessage);
setGlobal(global); setGlobal(global);
@ -773,16 +778,18 @@ function updateThreadUnread<T extends GlobalState>(
) { ) {
const { chatId } = message; const { chatId } = message;
const replyInfo = getMessageReplyInfo(message);
const { threadInfo } = selectThreadByMessage(global, message) || {}; const { threadInfo } = selectThreadByMessage(global, message) || {};
if (!threadInfo && message.replyToMessageId) { if (!threadInfo && replyInfo?.replyToMsgId) {
const originMessage = selectChatMessage(global, chatId, message.replyToMessageId); const originMessage = selectChatMessage(global, chatId, replyInfo.replyToMsgId);
if (originMessage) { if (originMessage) {
global = updateThreadUnreadFromForwardedMessage(global, originMessage, chatId, message.id, isDeleting); global = updateThreadUnreadFromForwardedMessage(global, originMessage, chatId, message.id, isDeleting);
} else { } else {
actions.loadMessage({ actions.loadMessage({
chatId, chatId,
messageId: message.replyToMessageId, messageId: replyInfo.replyToMsgId,
threadUpdate: { threadUpdate: {
isDeleting, isDeleting,
lastMessageId: message.id, lastMessageId: message.id,

View File

@ -112,7 +112,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
case 'updateWebViewResultSent': case 'updateWebViewResultSent':
Object.values(global.byTabId).forEach((tabState) => { Object.values(global.byTabId).forEach((tabState) => {
if (tabState.webApp?.queryId === update.queryId) { if (tabState.webApp?.queryId === update.queryId) {
actions.setReplyingToId({ messageId: undefined, tabId: tabState.id }); actions.resetDraftReplyInfo({ tabId: tabState.id });
actions.closeWebApp({ tabId: tabState.id }); actions.closeWebApp({ tabId: tabState.id });
} }
}); });

View File

@ -42,12 +42,12 @@ import {
selectChatScheduledMessages, selectChatScheduledMessages,
selectCurrentChat, selectCurrentChat,
selectCurrentMessageList, selectCurrentMessageList,
selectDraft,
selectForwardedMessageIdsByGroupId, selectForwardedMessageIdsByGroupId,
selectIsRightColumnShown, selectIsRightColumnShown,
selectIsViewportNewest, selectIsViewportNewest,
selectMessageIdsByGroupId, selectMessageIdsByGroupId,
selectPinnedIds, selectPinnedIds,
selectReplyingToId,
selectReplyStack, selectReplyStack,
selectRequestedChatTranslationLanguage, selectRequestedChatTranslationLanguage,
selectRequestedMessageTranslationLanguage, selectRequestedMessageTranslationLanguage,
@ -77,17 +77,6 @@ addActionHandler('setScrollOffset', (global, actions, payload): ActionReturnType
return replaceTabThreadParam(global, chatId, threadId, 'scrollOffset', scrollOffset, tabId); return replaceTabThreadParam(global, chatId, threadId, 'scrollOffset', scrollOffset, tabId);
}); });
addActionHandler('setReplyingToId', (global, actions, payload): ActionReturnType => {
const { messageId, tabId = getCurrentTabId() } = payload;
const currentMessageList = selectCurrentMessageList(global, tabId);
if (!currentMessageList) {
return undefined;
}
const { chatId, threadId } = currentMessageList;
return replaceThreadParam(global, chatId, threadId, 'replyingToId', messageId);
});
addActionHandler('setEditingId', (global, actions, payload): ActionReturnType => { addActionHandler('setEditingId', (global, actions, payload): ActionReturnType => {
const { messageId, tabId = getCurrentTabId() } = payload; const { messageId, tabId = getCurrentTabId() } = payload;
const currentMessageList = selectCurrentMessageList(global, tabId); const currentMessageList = selectCurrentMessageList(global, tabId);
@ -148,12 +137,12 @@ addActionHandler('replyToNextMessage', (global, actions, payload): ActionReturnT
return; return;
} }
const replyingToId = selectReplyingToId(global, chatId, threadId); const replyInfo = selectDraft(global, chatId, threadId)?.replyInfo;
const isLatest = selectIsViewportNewest(global, chatId, threadId, tabId); const isLatest = selectIsViewportNewest(global, chatId, threadId, tabId);
let messageId: number | undefined; let messageId: number | undefined;
if (!isLatest || !replyingToId) { if (!isLatest || !replyInfo) {
if (threadId === MAIN_THREAD_ID) { if (threadId === MAIN_THREAD_ID) {
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
@ -165,13 +154,13 @@ addActionHandler('replyToNextMessage', (global, actions, payload): ActionReturnT
} }
} else { } else {
const chatMessageKeys = Object.keys(chatMessages); const chatMessageKeys = Object.keys(chatMessages);
const indexOfCurrent = chatMessageKeys.indexOf(replyingToId.toString()); const indexOfCurrent = chatMessageKeys.indexOf(replyInfo.toString());
const newIndex = indexOfCurrent + targetIndexDelta; const newIndex = indexOfCurrent + targetIndexDelta;
messageId = newIndex <= chatMessageKeys.length + 1 && newIndex >= 0 messageId = newIndex <= chatMessageKeys.length + 1 && newIndex >= 0
? Number(chatMessageKeys[newIndex]) ? Number(chatMessageKeys[newIndex])
: undefined; : undefined;
} }
actions.setReplyingToId({ messageId, tabId }); actions.updateDraftReplyInfo({ replyToMsgId: messageId, tabId });
actions.focusMessage({ actions.focusMessage({
chatId, chatId,
threadId, threadId,

View File

@ -34,6 +34,7 @@ import {
selectChatMessages, selectChatMessages,
selectCurrentMessageList, selectCurrentMessageList,
selectThreadOriginChat, selectThreadOriginChat,
selectViewportIds,
selectVisibleUsers, selectVisibleUsers,
} from './selectors'; } from './selectors';
@ -348,15 +349,24 @@ function reduceChats<T extends GlobalState>(global: T): GlobalState['chats'] {
}), }),
).map(({ chatId }) => chatId); ).map(({ chatId }) => chatId);
const chatStoriesChannelIds = currentChatIds const messagesChatIds = compact(Object.values(global.byTabId).flatMap(({ id: tabId }) => {
.flatMap((chatId) => Object.values(selectChatMessages(global, chatId) || {})) const messageList = selectCurrentMessageList(global, tabId);
.map((message) => message.content.storyData?.peerId || message.content.webPage?.story?.peerId) if (!messageList) return undefined;
.filter((id): id is string => Boolean(id) && !isUserId(id));
const messages = selectChatMessages(global, messageList.chatId);
const viewportIds = selectViewportIds(global, messageList.chatId, messageList.threadId, tabId);
return viewportIds?.map((id) => {
const message = messages[id];
const content = message?.content;
const replyPeer = message.replyInfo?.type === 'message' && message.replyInfo.replyToPeerId;
return content.storyData?.peerId || content.webPage?.story?.peerId || replyPeer;
});
}));
const idsToSave = unique([ const idsToSave = unique([
...currentUserId ? [currentUserId] : [], ...currentUserId ? [currentUserId] : [],
...currentChatIds, ...currentChatIds,
...chatStoriesChannelIds, ...messagesChatIds,
...getOrderedIds(ALL_FOLDER_ID) || [], ...getOrderedIds(ALL_FOLDER_ID) || [],
...getOrderedIds(ARCHIVED_FOLDER_ID) || [], ...getOrderedIds(ARCHIVED_FOLDER_ID) || [],
...global.recentlyFoundChatIds || [], ...global.recentlyFoundChatIds || [],

View File

@ -19,13 +19,13 @@ import {
import { formatDateToString, formatTime } from '../../util/dateFormat'; import { formatDateToString, formatTime } from '../../util/dateFormat';
import { orderBy } from '../../util/iteratees'; import { orderBy } from '../../util/iteratees';
import { prepareSearchWordsForNeedle } from '../../util/searchWords'; import { prepareSearchWordsForNeedle } from '../../util/searchWords';
import { getGlobal } from '..';
import { getMainUsername, getUserFirstOrLastName } from './users'; import { getMainUsername, getUserFirstOrLastName } from './users';
const FOREVER_BANNED_DATE = Date.now() / 1000 + 31622400; // 366 days const FOREVER_BANNED_DATE = Date.now() / 1000 + 31622400; // 366 days
const VERIFIED_PRIORITY_BASE = 3e9; const VERIFIED_PRIORITY_BASE = 3e9;
const PINNED_PRIORITY_BASE = 3e8; const PINNED_PRIORITY_BASE = 3e8;
const USER_COLOR_KEYS = [1, 8, 5, 2, 7, 4, 6];
export function isUserId(entityId: string) { export function isUserId(entityId: string) {
return !entityId.startsWith('-'); return !entityId.startsWith('-');
@ -39,6 +39,10 @@ export function toChannelId(mtpId: string) {
return `-100${mtpId}`; return `-100${mtpId}`;
} }
export function isApiPeerChat(peer: ApiPeer): peer is ApiChat {
return 'title' in peer;
}
export function isChatGroup(chat: ApiChat) { export function isChatGroup(chat: ApiChat) {
return isChatBasicGroup(chat) || isChatSuperGroup(chat); return isChatBasicGroup(chat) || isChatSuperGroup(chat);
} }
@ -467,9 +471,14 @@ export function getPeerIdDividend(peerId: string) {
return Math.abs(Number(getCleanPeerId(peerId))); return Math.abs(Number(getCleanPeerId(peerId)));
} }
// https://github.com/telegramdesktop/tdesktop/blob/371510cfe23b0bd226de8c076bc49248fbe40c26/Telegram/SourceFiles/data/data_peer.cpp#L53
export function getPeerColorKey(peer: ApiPeer | undefined) { export function getPeerColorKey(peer: ApiPeer | undefined) {
const index = peer ? getPeerIdDividend(peer.id) % 7 : 0; if (peer?.color) return peer.color;
return USER_COLOR_KEYS[index]; const index = peer ? getPeerIdDividend(peer.id) % 7 : 0;
return index;
}
export function getPeerColorCount(peer: ApiPeer) {
const key = getPeerColorKey(peer);
return getGlobal().appConfig?.peerColors?.[key]?.length || 1;
} }

View File

@ -1,9 +1,9 @@
import type { ApiMessage } from '../../api/types'; import type { MediaContent } from '../../api/types';
import { ApiMessageEntityTypes } from '../../api/types'; import { ApiMessageEntityTypes } from '../../api/types';
import parseEmojiOnlyString from '../../util/parseEmojiOnlyString'; import parseEmojiOnlyString from '../../util/parseEmojiOnlyString';
export function getEmojiOnlyCountForMessage(content: ApiMessage['content'], groupedId?: string): number | undefined { export function getEmojiOnlyCountForMessage(content: MediaContent, groupedId?: string): number | undefined {
if (!content.text) return undefined; if (!content.text) return undefined;
return ( return (
!groupedId !groupedId

View File

@ -9,6 +9,7 @@ import type {
ApiPhoto, ApiPhoto,
ApiVideo, ApiVideo,
ApiWebDocument, ApiWebDocument,
MediaContent,
} from '../../api/types'; } from '../../api/types';
import { ApiMediaFormat } from '../../api/types'; import { ApiMediaFormat } from '../../api/types';
@ -22,6 +23,10 @@ import {
import { getDocumentHasPreview } from '../../components/common/helpers/documentInfo'; import { getDocumentHasPreview } from '../../components/common/helpers/documentInfo';
import { getMessageKey, isMessageLocal, matchLinkInMessageText } from './messages'; import { getMessageKey, isMessageLocal, matchLinkInMessageText } from './messages';
type MediaContainer = {
content: MediaContent;
};
type Target = type Target =
'micro' 'micro'
| 'pictogram' | 'pictogram'
@ -30,11 +35,11 @@ type Target =
| 'full' | 'full'
| 'download'; | 'download';
export function getMessageContent(message: ApiMessage) { export function getMessageContent(message: MediaContainer) {
return message.content; return message.content;
} }
export function hasMessageMedia(message: ApiMessage) { export function hasMessageMedia(message: MediaContainer) {
return Boolean(( return Boolean((
getMessagePhoto(message) getMessagePhoto(message)
|| getMessageVideo(message) || getMessageVideo(message)
@ -48,91 +53,91 @@ export function hasMessageMedia(message: ApiMessage) {
)); ));
} }
export function getMessagePhoto(message: ApiMessage) { export function getMessagePhoto(message: MediaContainer) {
return message.content.photo; return message.content.photo;
} }
export function getMessageActionPhoto(message: ApiMessage) { export function getMessageActionPhoto(message: MediaContainer) {
return message.content.action?.type === 'suggestProfilePhoto' ? message.content.action.photo : undefined; return message.content.action?.type === 'suggestProfilePhoto' ? message.content.action.photo : undefined;
} }
export function getMessageVideo(message: ApiMessage) { export function getMessageVideo(message: MediaContainer) {
return message.content.video; return message.content.video;
} }
export function getMessageRoundVideo(message: ApiMessage) { export function getMessageRoundVideo(message: MediaContainer) {
const { video } = message.content; const { video } = message.content;
return video?.isRound ? video : undefined; return video?.isRound ? video : undefined;
} }
export function getMessageAction(message: ApiMessage) { export function getMessageAction(message: MediaContainer) {
return message.content.action; return message.content.action;
} }
export function getMessageAudio(message: ApiMessage) { export function getMessageAudio(message: MediaContainer) {
return message.content.audio; return message.content.audio;
} }
export function getMessageVoice(message: ApiMessage) { export function getMessageVoice(message: MediaContainer) {
return message.content.voice; return message.content.voice;
} }
export function getMessageSticker(message: ApiMessage) { export function getMessageSticker(message: MediaContainer) {
return message.content.sticker; return message.content.sticker;
} }
export function getMessageDocument(message: ApiMessage) { export function getMessageDocument(message: MediaContainer) {
return message.content.document; return message.content.document;
} }
export function isMessageDocumentPhoto(message: ApiMessage) { export function isMessageDocumentPhoto(message: MediaContainer) {
const document = getMessageDocument(message); const document = getMessageDocument(message);
return document ? document.mediaType === 'photo' : undefined; return document ? document.mediaType === 'photo' : undefined;
} }
export function isMessageDocumentVideo(message: ApiMessage) { export function isMessageDocumentVideo(message: MediaContainer) {
const document = getMessageDocument(message); const document = getMessageDocument(message);
return document ? document.mediaType === 'video' : undefined; return document ? document.mediaType === 'video' : undefined;
} }
export function getMessageContact(message: ApiMessage) { export function getMessageContact(message: MediaContainer) {
return message.content.contact; return message.content.contact;
} }
export function getMessagePoll(message: ApiMessage) { export function getMessagePoll(message: MediaContainer) {
return message.content.poll; return message.content.poll;
} }
export function getMessageInvoice(message: ApiMessage) { export function getMessageInvoice(message: MediaContainer) {
return message.content.invoice; return message.content.invoice;
} }
export function getMessageLocation(message: ApiMessage) { export function getMessageLocation(message: MediaContainer) {
return message.content.location; return message.content.location;
} }
export function getMessageWebPage(message: ApiMessage) { export function getMessageWebPage(message: MediaContainer) {
return message.content.webPage; return message.content.webPage;
} }
export function getMessageWebPagePhoto(message: ApiMessage) { export function getMessageWebPagePhoto(message: MediaContainer) {
return getMessageWebPage(message)?.photo; return getMessageWebPage(message)?.photo;
} }
export function getMessageDocumentPhoto(message: ApiMessage) { export function getMessageDocumentPhoto(message: MediaContainer) {
return isMessageDocumentPhoto(message) ? getMessageDocument(message) : undefined; return isMessageDocumentPhoto(message) ? getMessageDocument(message) : undefined;
} }
export function getMessageWebPageVideo(message: ApiMessage) { export function getMessageWebPageVideo(message: MediaContainer) {
return getMessageWebPage(message)?.video; return getMessageWebPage(message)?.video;
} }
export function getMessageDocumentVideo(message: ApiMessage) { export function getMessageDocumentVideo(message: MediaContainer) {
return isMessageDocumentVideo(message) ? getMessageDocument(message) : undefined; return isMessageDocumentVideo(message) ? getMessageDocument(message) : undefined;
} }
export function getMessageMediaThumbnail(message: ApiMessage) { export function getMessageMediaThumbnail(message: MediaContainer) {
const media = getMessagePhoto(message) const media = getMessagePhoto(message)
|| getMessageVideo(message) || getMessageVideo(message)
|| getMessageDocument(message) || getMessageDocument(message)
@ -148,11 +153,11 @@ export function getMessageMediaThumbnail(message: ApiMessage) {
return media.thumbnail; return media.thumbnail;
} }
export function getMessageMediaThumbDataUri(message: ApiMessage) { export function getMessageMediaThumbDataUri(message: MediaContainer) {
return getMessageMediaThumbnail(message)?.dataUri; return getMessageMediaThumbnail(message)?.dataUri;
} }
export function getMessageIsSpoiler(message: ApiMessage) { export function getMessageIsSpoiler(message: MediaContainer) {
const media = getMessagePhoto(message) const media = getMessagePhoto(message)
|| getMessageVideo(message); || getMessageVideo(message);

View File

@ -164,8 +164,8 @@ export function isOwnMessage(message: ApiMessage) {
return message.isOutgoing; return message.isOutgoing;
} }
export function isReplyMessage(message: ApiMessage) { export function isReplyToMessage(message: ApiMessage) {
return Boolean(message.replyToMessageId); return Boolean(message.replyInfo?.type === 'message');
} }
export function isForwardedMessage(message: ApiMessage) { export function isForwardedMessage(message: ApiMessage) {

View File

@ -0,0 +1,13 @@
import type { ApiMessage, ApiMessageReplyInfo, ApiStoryReplyInfo } from '../../api/types';
export function getMessageReplyInfo(message: ApiMessage): ApiMessageReplyInfo | undefined {
const { replyInfo } = message;
if (!replyInfo || replyInfo.type !== 'message') return undefined;
return replyInfo;
}
export function getStoryReplyInfo(message: ApiMessage): ApiStoryReplyInfo | undefined {
const { replyInfo } = message;
if (!replyInfo || replyInfo.type !== 'story') return undefined;
return replyInfo;
}

View File

@ -9,6 +9,7 @@ import { cloneDeep } from '../util/iteratees';
import { Bundles, loadBundle } from '../util/moduleLoader'; import { Bundles, loadBundle } from '../util/moduleLoader';
import { parseLocationHash } from '../util/routing'; import { parseLocationHash } from '../util/routing';
import { clearStoredSession } from '../util/sessions'; import { clearStoredSession } from '../util/sessions';
import { updatePeerColors } from '../util/theme';
import { IS_MULTITAB_SUPPORTED } from '../util/windowEnvironment'; import { IS_MULTITAB_SUPPORTED } from '../util/windowEnvironment';
import { updateTabState } from './reducers/tabs'; import { updateTabState } from './reducers/tabs';
import { initCache, loadCache } from './cache'; import { initCache, loadCache } from './cache';
@ -43,6 +44,10 @@ addActionHandler('initShared', (prevGlobal, actions, payload): ActionReturnType
global.byTabId = prevGlobal.byTabId; global.byTabId = prevGlobal.byTabId;
} }
if (global.appConfig?.peerColors) {
updatePeerColors(global.appConfig.peerColors, global.appConfig.darkPeerColors);
}
return global; return global;
}); });

View File

@ -1,7 +1,9 @@
import type { import type {
ApiChat, ApiChat,
ApiInputMessageReplyInfo,
ApiMessage, ApiMessage,
ApiMessageEntityCustomEmoji, ApiMessageEntityCustomEmoji,
ApiMessageForwardInfo,
ApiMessageOutgoingStatus, ApiMessageOutgoingStatus,
ApiPeer, ApiPeer,
ApiStickerSetInfo, ApiStickerSetInfo,
@ -49,6 +51,7 @@ import {
isUserId, isUserId,
isUserRightBanned, isUserRightBanned,
} from '../helpers'; } from '../helpers';
import { getMessageReplyInfo } from '../helpers/replies';
import { import {
selectChat, selectChatFullInfo, selectIsChatWithSelf, selectPeer, selectRequestedChatTranslationLanguage, selectChat, selectChatFullInfo, selectIsChatWithSelf, selectPeer, selectRequestedChatTranslationLanguage,
} from './chats'; } from './chats';
@ -197,10 +200,6 @@ export function selectLastScrollOffset<T extends GlobalState>(global: T, chatId:
return selectThreadParam(global, chatId, threadId, 'lastScrollOffset'); return selectThreadParam(global, chatId, threadId, 'lastScrollOffset');
} }
export function selectReplyingToId<T extends GlobalState>(global: T, chatId: string, threadId: number) {
return selectThreadParam(global, chatId, threadId, 'replyingToId');
}
export function selectEditingId<T extends GlobalState>(global: T, chatId: string, threadId: number) { export function selectEditingId<T extends GlobalState>(global: T, chatId: string, threadId: number) {
return selectThreadParam(global, chatId, threadId, 'editingId'); return selectThreadParam(global, chatId, threadId, 'editingId');
} }
@ -425,15 +424,9 @@ export function selectSender<T extends GlobalState>(global: T, message: ApiMessa
return selectPeer(global, senderId); return selectPeer(global, senderId);
} }
export function selectReplySender<T extends GlobalState>(global: T, message: ApiMessage, isForwarded = false) { export function selectReplySender<T extends GlobalState>(
if (isForwarded) { global: T, message: ApiMessage,
const { senderUserId, hiddenUserName } = message.forwardInfo || {}; ) {
if (senderUserId) {
return selectPeer(global, senderUserId);
}
if (hiddenUserName) return undefined;
}
const { senderId } = message; const { senderId } = message;
if (!senderId) { if (!senderId) {
return undefined; return undefined;
@ -442,6 +435,18 @@ export function selectReplySender<T extends GlobalState>(global: T, message: Api
return selectPeer(global, senderId); return selectPeer(global, senderId);
} }
export function selectSenderFromHeader<T extends GlobalState>(
global: T,
header: ApiMessageForwardInfo,
) {
const { senderUserId } = header;
if (senderUserId) {
return selectPeer(global, senderUserId);
}
return undefined;
}
export function selectForwardedSender<T extends GlobalState>( export function selectForwardedSender<T extends GlobalState>(
global: T, message: ApiMessage, global: T, message: ApiMessage,
): ApiPeer | undefined { ): ApiPeer | undefined {
@ -507,9 +512,8 @@ export function selectCanDeleteTopic<T extends GlobalState>(global: T, chatId: s
export function selectThreadIdFromMessage<T extends GlobalState>(global: T, message: ApiMessage): number { export function selectThreadIdFromMessage<T extends GlobalState>(global: T, message: ApiMessage): number {
const chat = selectChat(global, message.chatId); const chat = selectChat(global, message.chatId);
const { const { content } = message;
replyToMessageId, replyToTopMessageId, isTopicReply, content, const { replyToMsgId, replyToTopId, isForumTopic } = getMessageReplyInfo(message) || {};
} = message;
if ('action' in content && content.action?.type === 'topicCreate') { if ('action' in content && content.action?.type === 'topicCreate') {
return message.id; return message.id;
} }
@ -518,12 +522,12 @@ export function selectThreadIdFromMessage<T extends GlobalState>(global: T, mess
if (chat && isChatBasicGroup(chat)) return MAIN_THREAD_ID; if (chat && isChatBasicGroup(chat)) return MAIN_THREAD_ID;
if (chat && isChatSuperGroup(chat)) { if (chat && isChatSuperGroup(chat)) {
return replyToTopMessageId || replyToMessageId || MAIN_THREAD_ID; return replyToTopId || replyToMsgId || MAIN_THREAD_ID;
} }
return MAIN_THREAD_ID; return MAIN_THREAD_ID;
} }
if (!isTopicReply) return GENERAL_TOPIC_ID; if (!isForumTopic) return GENERAL_TOPIC_ID;
return replyToTopMessageId || replyToMessageId || GENERAL_TOPIC_ID; return replyToTopId || replyToMsgId || GENERAL_TOPIC_ID;
} }
export function selectTopicFromMessage<T extends GlobalState>(global: T, message: ApiMessage) { export function selectTopicFromMessage<T extends GlobalState>(global: T, message: ApiMessage) {
@ -985,11 +989,12 @@ function selectShouldHideReplyKeyboard<T extends GlobalState>(global: T, message
const { const {
shouldHideKeyboardButtons, shouldHideKeyboardButtons,
isHideKeyboardSelective, isHideKeyboardSelective,
replyToMessageId,
isMentioned, isMentioned,
} = message; } = message;
if (!shouldHideKeyboardButtons) return false; if (!shouldHideKeyboardButtons) return false;
const replyToMessageId = getMessageReplyInfo(message)?.replyToMsgId;
if (isHideKeyboardSelective) { if (isHideKeyboardSelective) {
if (isMentioned) return true; if (isMentioned) return true;
if (!replyToMessageId) return false; if (!replyToMessageId) return false;
@ -1006,10 +1011,11 @@ function selectShouldDisplayReplyKeyboard<T extends GlobalState>(global: T, mess
shouldHideKeyboardButtons, shouldHideKeyboardButtons,
isKeyboardSelective, isKeyboardSelective,
isMentioned, isMentioned,
replyToMessageId,
} = message; } = message;
if (!keyboardButtons || shouldHideKeyboardButtons) return false; if (!keyboardButtons || shouldHideKeyboardButtons) return false;
const replyToMessageId = getMessageReplyInfo(message)?.replyToMsgId;
if (isKeyboardSelective) { if (isKeyboardSelective) {
if (isMentioned) return true; if (isMentioned) return true;
if (!replyToMessageId) return false; if (!replyToMessageId) return false;
@ -1380,3 +1386,23 @@ export function selectTopicLink<T extends GlobalState>(
) { ) {
return selectMessageLink(global, chatId, topicId); return selectMessageLink(global, chatId, topicId);
} }
export function selectMessageReplyInfo<T extends GlobalState>(
global: T, chatId: string, threadId: number = MAIN_THREAD_ID, additionalReplyInfo?: ApiInputMessageReplyInfo,
) {
const chat = selectChat(global, chatId);
if (!chat) return undefined;
const replyingToTopId = selectThreadTopMessageId(global, chatId, threadId);
if (!additionalReplyInfo && !replyingToTopId) return undefined;
const replyInfo: ApiInputMessageReplyInfo = {
type: 'message',
...additionalReplyInfo,
replyToMsgId: additionalReplyInfo?.replyToMsgId || replyingToTopId!,
replyToTopId: additionalReplyInfo?.replyToTopId || replyingToTopId,
};
return replyInfo;
}

Some files were not shown because too many files have changed in this diff Show More