Message: Reply re-design and blockquotes (#3926)
This commit is contained in:
parent
f894010a70
commit
330bc42c98
@ -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 }}",
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@ -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!,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
@ -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(),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) }),
|
||||
}));
|
||||
}
|
||||
|
||||
@ -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),
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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' });
|
||||
|
||||
@ -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[];
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -195,6 +195,8 @@ export interface ApiAppConfig {
|
||||
storyExpirePeriod: number;
|
||||
storyViewersExpirePeriod: number;
|
||||
storyChangelogUserId: string;
|
||||
peerColors: Record<string, string[]>;
|
||||
darkPeerColors: Record<string, string[]>;
|
||||
}
|
||||
|
||||
export interface ApiConfig {
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
1
src/assets/font-icons/quote.svg
Normal file
1
src/assets/font-icons/quote.svg
Normal 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 |
@ -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 {
|
||||
|
||||
@ -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"] {
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
@ -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 {
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
249
src/components/common/embedded/EmbeddedMessage.tsx
Normal file
249
src/components/common/embedded/EmbeddedMessage.tsx
Normal 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;
|
||||
@ -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>
|
||||
);
|
||||
@ -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;
|
||||
}
|
||||
86
src/components/common/embedded/EmojiIconBackground.tsx
Normal file
86
src/components/common/embedded/EmojiIconBackground.tsx
Normal 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);
|
||||
11
src/components/common/helpers/peerColor.ts
Normal file
11
src/components/common/helpers/peerColor.ts
Normal 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)}`;
|
||||
}
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -158,6 +158,7 @@ const LeftSideMenuItems = ({
|
||||
bot={bot}
|
||||
theme={theme}
|
||||
isInSideMenu
|
||||
canShowNew
|
||||
onMenuOpened={onBotMenuOpened}
|
||||
onMenuClosed={onBotMenuClosed}
|
||||
/>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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),
|
||||
};
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 });
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
@import "message-content";
|
||||
@use "message-content";
|
||||
|
||||
// General styles
|
||||
.Message {
|
||||
|
||||
@ -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 && {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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)}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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,
|
||||
]);
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -397,4 +397,8 @@
|
||||
&.premium {
|
||||
background: var(--premium-gradient);
|
||||
}
|
||||
|
||||
&.rectangular {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>) => {
|
||||
|
||||
@ -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"] {
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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> => {
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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 || [],
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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) {
|
||||
|
||||
13
src/global/helpers/replies.ts
Normal file
13
src/global/helpers/replies.ts
Normal 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;
|
||||
}
|
||||
@ -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;
|
||||
});
|
||||
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user