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

View File

@ -52,6 +52,8 @@ export interface GramJsAppConfig extends LimitsConfig {
story_expire_period: number;
story_viewers_expire_period: number;
stories_changelog_user_id?: number;
peer_colors: Record<string, string[]>;
dark_peer_colors: Record<string, string[]>;
}
function buildEmojiSounds(appConfig: GramJsAppConfig) {
@ -117,5 +119,7 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp
storyExpirePeriod: appConfig.story_expire_period ?? STORY_EXPIRE_PERIOD,
storyViewersExpirePeriod: appConfig.story_viewers_expire_period ?? STORY_VIEWERS_EXPIRE_PERIOD,
storyChangelogUserId: appConfig.stories_changelog_user_id?.toString() ?? SERVICE_NOTIFICATIONS_USER_ID,
peerColors: appConfig.peer_colors,
darkPeerColors: appConfig.dark_peer_colors,
};
}

View File

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

View File

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

View File

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

View File

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

View File

@ -1,18 +1,17 @@
import { Api as GramJs, errors } from '../../../lib/gramjs';
import { Api as GramJs } from '../../../lib/gramjs';
import type {
ApiApplyBoostInfo,
ApiBoostsStatus,
ApiMediaArea,
ApiMediaAreaCoordinates,
ApiMessage,
ApiMyBoost,
ApiStealthMode,
ApiStoryView,
ApiTypeStory,
MediaContent,
} from '../../types';
import { buildCollectionByCallback } from '../../../util/iteratees';
import { getServerTime } from '../../../util/serverTime';
import { buildPrivacyRules } from './common';
import { buildGeoPoint, buildMessageMediaContent, buildMessageTextContent } from './messageContent';
import { buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
@ -49,7 +48,7 @@ export function buildApiStory(peerId: string, story: GramJs.TypeStoryItem): ApiT
mediaAreas, sentReaction, out,
} = story;
const content: ApiMessage['content'] = {
const content: MediaContent = {
...buildMessageMediaContent(media),
};
@ -171,45 +170,7 @@ export function buildApiPeerStories(peerStories: GramJs.PeerStories) {
return buildCollectionByCallback(peerStories.stories, (story) => [story.id, buildApiStory(peerId, story)]);
}
export function buildApiApplyBoostInfo(
applyBoostInfo: GramJs.stories.TypeCanApplyBoostResult,
): ApiApplyBoostInfo | undefined {
if (applyBoostInfo instanceof GramJs.stories.CanApplyBoostOk) {
return { type: 'ok' };
}
if (applyBoostInfo instanceof GramJs.stories.CanApplyBoostReplace) {
return {
type: 'replace',
boostedChatId: getApiChatIdFromMtpPeer(applyBoostInfo.currentBoost),
};
}
return undefined;
}
export function buildApiApplyBoostInfoFromError(
error: unknown,
): ApiApplyBoostInfo | undefined {
if (error instanceof errors.FloodWaitError) {
return {
type: 'wait',
waitUntil: getServerTime() + error.seconds,
};
}
if (error instanceof Error) {
if (error.message === 'BOOST_NOT_MODIFIED') {
return {
type: 'already',
};
}
}
return undefined;
}
export function buildApiBoostsStatus(boostStatus: GramJs.stories.BoostsStatus): ApiBoostsStatus {
export function buildApiBoostsStatus(boostStatus: GramJs.premium.BoostsStatus): ApiBoostsStatus {
const {
level, boostUrl, boosts, myBoost, currentLevelBoosts, nextLevelBoosts, premiumAudience,
} = boostStatus;
@ -223,3 +184,17 @@ export function buildApiBoostsStatus(boostStatus: GramJs.stories.BoostsStatus):
...(premiumAudience && { premiumSubscribers: buildStatisticsPercentage(premiumAudience) }),
};
}
export function buildApiMyBoost(myBoost: GramJs.MyBoost): ApiMyBoost {
const {
date, expires, slot, cooldownUntilDate, peer,
} = myBoost;
return {
date,
expires,
slot,
cooldownUntil: cooldownUntilDate,
chatId: peer && getApiChatIdFromMtpPeer(peer),
};
}

View File

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

View File

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

View File

@ -50,29 +50,10 @@ export function addMessageToLocalDb(message: GramJs.Message | GramJs.MessageServ
localDb.messages[messageFullId] = mockMessage;
if (mockMessage instanceof GramJs.Message) {
if (mockMessage.media instanceof GramJs.MessageMediaDocument
&& mockMessage.media.document instanceof GramJs.Document
) {
localDb.documents[String(mockMessage.media.document.id)] = mockMessage.media.document;
}
if (mockMessage.media) addMediaToLocalDb(mockMessage.media);
if (mockMessage.media instanceof GramJs.MessageMediaWebPage
&& mockMessage.media.webpage instanceof GramJs.WebPage
&& mockMessage.media.webpage.document instanceof GramJs.Document
) {
localDb.documents[String(mockMessage.media.webpage.document.id)] = mockMessage.media.webpage.document;
}
if (mockMessage.media instanceof GramJs.MessageMediaGame) {
if (mockMessage.media.game.document instanceof GramJs.Document) {
localDb.documents[String(mockMessage.media.game.document.id)] = mockMessage.media.game.document;
}
addPhotoToLocalDb(mockMessage.media.game.photo);
}
if (mockMessage.media instanceof GramJs.MessageMediaInvoice
&& mockMessage.media.photo) {
localDb.webDocuments[String(mockMessage.media.photo.url)] = mockMessage.media.photo;
if (mockMessage.replyTo instanceof GramJs.MessageReplyHeader && mockMessage.replyTo.replyMedia) {
addMediaToLocalDb(mockMessage.replyTo.replyMedia);
}
}
@ -81,6 +62,33 @@ export function addMessageToLocalDb(message: GramJs.Message | GramJs.MessageServ
}
}
function addMediaToLocalDb(media: GramJs.TypeMessageMedia) {
if (media instanceof GramJs.MessageMediaDocument
&& media.document instanceof GramJs.Document
) {
localDb.documents[String(media.document.id)] = media.document;
}
if (media instanceof GramJs.MessageMediaWebPage
&& media.webpage instanceof GramJs.WebPage
&& media.webpage.document instanceof GramJs.Document
) {
localDb.documents[String(media.webpage.document.id)] = media.webpage.document;
}
if (media instanceof GramJs.MessageMediaGame) {
if (media.game.document instanceof GramJs.Document) {
localDb.documents[String(media.game.document.id)] = media.game.document;
}
addPhotoToLocalDb(media.game.photo);
}
if (media instanceof GramJs.MessageMediaInvoice
&& media.photo) {
localDb.webDocuments[String(media.photo.url)] = media.photo;
}
}
export function addStoryToLocalDb(story: GramJs.TypeStoryItem, peerId: string) {
if (!(story instanceof GramJs.StoryItem)) {
return;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -298,18 +298,42 @@ export interface ApiWebPage {
story?: ApiWebPageStoryData;
}
export type ApiTypeReplyTo = ApiMessageReplyTo | ApiStoryReplyTo;
export type ApiReplyInfo = ApiMessageReplyInfo | ApiStoryReplyInfo;
export interface ApiMessageReplyTo {
replyingTo: number;
replyingToTopId?: number;
export interface ApiMessageReplyInfo {
type: 'message';
replyToMsgId?: number;
replyToPeerId?: string;
replyFrom?: ApiMessageForwardInfo;
replyMedia?: MediaContent;
replyToTopId?: number;
isForumTopic?: true;
isQuote?: true;
quoteText?: ApiFormattedText;
}
export interface ApiStoryReplyTo {
export interface ApiStoryReplyInfo {
type: 'story';
userId: string;
storyId: number;
}
export interface ApiInputMessageReplyInfo {
type: 'message';
replyToMsgId: number;
replyToTopId?: number;
replyToPeerId?: string;
quoteText?: ApiFormattedText;
}
export interface ApiInputStoryReplyInfo {
type: 'story';
userId: string;
storyId: number;
}
export type ApiInputReplyInfo = ApiInputMessageReplyInfo | ApiInputStoryReplyInfo;
export interface ApiMessageForwardInfo {
date: number;
isImported?: boolean;
@ -391,36 +415,33 @@ export interface ApiFormattedText {
entities?: ApiMessageEntity[];
}
export type MediaContent = {
text?: ApiFormattedText;
photo?: ApiPhoto;
video?: ApiVideo;
altVideo?: ApiVideo;
document?: ApiDocument;
sticker?: ApiSticker;
contact?: ApiContact;
poll?: ApiPoll;
action?: ApiAction;
webPage?: ApiWebPage;
audio?: ApiAudio;
voice?: ApiVoice;
invoice?: ApiInvoice;
location?: ApiLocation;
game?: ApiGame;
storyData?: ApiMessageStoryData;
};
export interface ApiMessage {
id: number;
chatId: string;
content: {
text?: ApiFormattedText;
photo?: ApiPhoto;
video?: ApiVideo;
altVideo?: ApiVideo;
document?: ApiDocument;
sticker?: ApiSticker;
contact?: ApiContact;
poll?: ApiPoll;
action?: ApiAction;
webPage?: ApiWebPage;
audio?: ApiAudio;
voice?: ApiVoice;
invoice?: ApiInvoice;
location?: ApiLocation;
game?: ApiGame;
storyData?: ApiMessageStoryData;
};
content: MediaContent;
date: number;
isOutgoing: boolean;
senderId?: string;
replyToChatId?: string;
replyToMessageId?: number;
replyToTopMessageId?: number;
isTopicReply?: true;
replyToStoryUserId?: string;
replyToStoryId?: number;
replyInfo?: ApiReplyInfo;
sendingState?: 'messageSendingStatePending' | 'messageSendingStateFailed';
forwardInfo?: ApiMessageForwardInfo;
isDeleting?: boolean;
@ -657,6 +678,12 @@ export type ApiThemeParameters = {
button_color: string;
button_text_color: string;
secondary_bg_color: string;
header_bg_color: string;
accent_text_color: string;
section_bg_color: string;
section_header_text_color: string;
subtitle_text_color: string;
destructive_text_color: string;
};
export type ApiBotApp = {

View File

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

View File

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

View File

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

View File

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

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 {
--group-call-panel-color: #212121;
@ -74,7 +74,7 @@
border-bottom: 0.0625rem solid transparent;
padding: 0.375rem 0.875rem;
@include adapt-padding-to-scrollbar(0.875rem);
@include mixins.adapt-padding-to-scrollbar(0.875rem);
user-select: none;
z-index: 1;
@ -139,7 +139,7 @@
.participants {
position: relative;
margin: 0.125rem 0.5rem 0;
@include adapt-margin-to-scrollbar(0.5rem);
@include mixins.adapt-margin-to-scrollbar(0.5rem);
}
.participantVideos {

View File

@ -1,4 +1,4 @@
@import '../../../styles/mixins';
@use '../../../styles/mixins';
.participant-menu {
--color-text: white;
@ -84,7 +84,7 @@
transition: 0.25s ease-in-out background-color, 0.25s ease-in-out box-shadow;
}
@include reset-range();
@include mixins.reset-range();
// Apply custom styles
input[type="range"] {

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
@import "../../../styles/mixins";
@use "../../../styles/mixins";
.EmojiPicker {
--emoji-size: 2.25rem;
@ -10,11 +10,11 @@
height: calc(100% - 3rem);
overflow-y: auto;
padding: 0.5rem 0.75rem;
@include adapt-padding-to-scrollbar(0.75rem);
@include mixins.adapt-padding-to-scrollbar(0.75rem);
@media (max-width: 600px) {
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';
import { getActions, withGlobal } from '../../../global';
import type { ApiInputMessageReplyInfo } from '../../../api/types';
import type { IAnchorPosition, ISettings } from '../../../types';
import type { Signal } from '../../../util/signals';
import { EDITABLE_INPUT_ID } from '../../../config';
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 captureKeyboardListeners from '../../../util/captureKeyboardListeners';
import { getIsDirectTextInputDisabled } from '../../../util/directInputManager';
@ -74,7 +75,7 @@ type OwnProps = {
};
type StateProps = {
replyingToId?: number;
replyInfo?: ApiInputMessageReplyInfo;
isSelectModeActive?: boolean;
messageSendKeyCombo?: ISettings['messageSendKeyCombo'];
canPlayAnimatedEmojis: boolean;
@ -126,7 +127,7 @@ const MessageInput: FC<OwnProps & StateProps> = ({
noFocusInterception,
shouldSuppressFocus,
shouldSuppressTextFormatter,
replyingToId,
replyInfo,
isSelectModeActive,
canPlayAnimatedEmojis,
messageSendKeyCombo,
@ -461,7 +462,7 @@ const MessageInput: FC<OwnProps & StateProps> = ({
if (canAutoFocus) {
focusInput();
}
}, [chatId, focusInput, replyingToId, canAutoFocus]);
}, [chatId, focusInput, replyInfo, canAutoFocus]);
useEffect(() => {
if (
@ -626,7 +627,7 @@ export default memo(withGlobal<OwnProps>(
return {
messageSendKeyCombo,
replyingToId: chatId && threadId ? selectReplyingToId(global, chatId, threadId) : undefined,
replyInfo: chatId && threadId ? selectDraft(global, chatId, threadId)?.replyInfo : undefined,
isSelectModeActive: selectIsInSelectMode(global),
canPlayAnimatedEmojis: selectCanPlayAnimatedEmojis(global),
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,26 +1,29 @@
.WebPage {
margin-top: 0.25rem;
margin-bottom: 0.125rem;
padding: 0.375rem 0.375rem 0.375rem 0.625rem;
font-size: calc(var(--message-text-size, 1rem) - 0.125rem);
line-height: 1.125rem;
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 {
padding-left: 0.625rem;
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 {
display: flex;
flex-direction: column-reverse;
@ -28,10 +31,19 @@
}
&--quick-button {
margin-top: 0.375rem;
--riple-color: var(var(--accent-background-active-color));
.theme-dark .Message.own &:hover {
color: var(--background-color);
margin-top: 0.375rem;
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;
}
.message-content:not(.has-reactions) &.no-article:last-child,
.message-content:not(.has-reactions) &.with-quick-button,
.message-content:not(.has-reactions) &.with-square-photo {
.message-content:not(.has-reactions) & {
margin-bottom: 1rem !important;
}
@ -150,15 +160,6 @@
}
}
&:dir(rtl) {
padding-inline-start: 0.625rem;
&::before {
left: auto;
right: 0;
}
}
@media (min-width: 1921px) {
@supports (aspect-ratio: 1) {
&:not(.in-preview) {

View File

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

View File

@ -1,3 +1,6 @@
@use "sass:map";
@use "../../../styles/icons";
.message-content {
position: relative;
max-width: var(--max-width);
@ -280,24 +283,6 @@
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 {
margin-top: 0.25rem;
}
@ -313,9 +298,11 @@
.custom-emoji {
margin-left: 0.25rem;
color: var(--accent-color);
}
.PremiumIcon {
--color-fill: var(--accent-color);
vertical-align: middle;
opacity: 0.5;
margin-left: 0.25rem;
@ -709,8 +696,6 @@
}
.EmbeddedMessage {
border-radius: var(--border-radius-messages);
@media (max-width: 600px) {
max-width: calc(90vw - 13rem);
}
@ -950,6 +935,38 @@
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 {
0% {
background-position-x: 0;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,4 +1,4 @@
@import "../../styles/mixins";
@use "../../styles/mixins";
@mixin thumb-styles() {
background: var(--slider-color);
@ -86,7 +86,7 @@
}
// Reset range input browser styles
@include reset-range();
@include mixins.reset-range();
// Apply custom styles
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 MEDIA_CACHE_MAX_BYTES = 512 * 1024; // 512 KB
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 AUTODOWNLOAD_FILESIZE_MB_LIMITS = [1, 5, 10, 50, 100, 500];
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 DEFAULT_PATTERN_COLOR = '#4A8E3A8C';
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;
// Group calls

View File

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

View File

@ -294,7 +294,7 @@ addActionHandler('loadAllChats', async (global, actions, payload): Promise<void>
let i = 0;
const getOrderDate = (chat: ApiChat) => {
return chat.lastMessage?.date || chat.joinDate;
return chat.lastMessage?.date || chat.creationDate;
};
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 = updateListedTopicIds(global, chatId, result.topics.map((topic) => topic.id));
Object.entries(result.draftsById || {}).forEach(([threadId, draft]) => {
global = replaceThreadParam(global, chatId, Number(threadId), 'draft', draft?.formattedText);
global = replaceThreadParam(global, chatId, Number(threadId), 'replyingToId', draft?.replyingToId);
global = replaceThreadParam(global, chatId, Number(threadId), 'draft', draft);
});
Object.entries(result.readInboxMessageIdByTopicId || {}).forEach(([topicId, 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]) {
global = {
...global,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -19,13 +19,13 @@ import {
import { formatDateToString, formatTime } from '../../util/dateFormat';
import { orderBy } from '../../util/iteratees';
import { prepareSearchWordsForNeedle } from '../../util/searchWords';
import { getGlobal } from '..';
import { getMainUsername, getUserFirstOrLastName } from './users';
const FOREVER_BANNED_DATE = Date.now() / 1000 + 31622400; // 366 days
const VERIFIED_PRIORITY_BASE = 3e9;
const PINNED_PRIORITY_BASE = 3e8;
const USER_COLOR_KEYS = [1, 8, 5, 2, 7, 4, 6];
export function isUserId(entityId: string) {
return !entityId.startsWith('-');
@ -39,6 +39,10 @@ export function toChannelId(mtpId: string) {
return `-100${mtpId}`;
}
export function isApiPeerChat(peer: ApiPeer): peer is ApiChat {
return 'title' in peer;
}
export function isChatGroup(chat: ApiChat) {
return isChatBasicGroup(chat) || isChatSuperGroup(chat);
}
@ -467,9 +471,14 @@ export function getPeerIdDividend(peerId: string) {
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) {
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 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;
return (
!groupedId

View File

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

View File

@ -164,8 +164,8 @@ export function isOwnMessage(message: ApiMessage) {
return message.isOutgoing;
}
export function isReplyMessage(message: ApiMessage) {
return Boolean(message.replyToMessageId);
export function isReplyToMessage(message: ApiMessage) {
return Boolean(message.replyInfo?.type === 'message');
}
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 { parseLocationHash } from '../util/routing';
import { clearStoredSession } from '../util/sessions';
import { updatePeerColors } from '../util/theme';
import { IS_MULTITAB_SUPPORTED } from '../util/windowEnvironment';
import { updateTabState } from './reducers/tabs';
import { initCache, loadCache } from './cache';
@ -43,6 +44,10 @@ addActionHandler('initShared', (prevGlobal, actions, payload): ActionReturnType
global.byTabId = prevGlobal.byTabId;
}
if (global.appConfig?.peerColors) {
updatePeerColors(global.appConfig.peerColors, global.appConfig.darkPeerColors);
}
return global;
});

View File

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