Revert "Support Bot Forums (#6407)"

This reverts commit c3b850e6767b0210ebc9f49177cfbb6086930c75.
This commit is contained in:
Alexander Zinchuk 2025-11-13 11:23:49 +01:00
parent a4c655637f
commit fe69e6b50d
69 changed files with 500 additions and 1126 deletions

View File

@ -175,6 +175,7 @@ interface BooleanConstructor {
interface Array<T> { interface Array<T> {
filter<S extends T>(predicate: BooleanConstructor, thisArg?: unknown): Exclude<S, Falsy>[]; filter<S extends T>(predicate: BooleanConstructor, thisArg?: unknown): Exclude<S, Falsy>[];
at(index: number): T; // Make it behave like arr[arr.length - 1]
} }
interface ReadonlyArray<T> { interface ReadonlyArray<T> {
filter<S extends T>(predicate: BooleanConstructor, thisArg?: unknown): Exclude<S, Falsy>[]; filter<S extends T>(predicate: BooleanConstructor, thisArg?: unknown): Exclude<S, Falsy>[];

View File

@ -118,7 +118,6 @@ export interface GramJsAppConfig extends LimitsConfig {
verify_age_bot_username?: string; verify_age_bot_username?: string;
verify_age_country?: string; verify_age_country?: string;
verify_age_min?: number; verify_age_min?: number;
message_typing_draft_ttl?: number;
contact_note_length_limit?: number; contact_note_length_limit?: number;
} }
@ -242,7 +241,6 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp
verifyAgeBotUsername: appConfig.verify_age_bot_username, verifyAgeBotUsername: appConfig.verify_age_bot_username,
verifyAgeCountry: appConfig.verify_age_country, verifyAgeCountry: appConfig.verify_age_country,
verifyAgeMin: appConfig.verify_age_min, verifyAgeMin: appConfig.verify_age_min,
typingDraftTtl: appConfig.message_typing_draft_ttl,
}; };
return { return {

View File

@ -89,7 +89,6 @@ function buildApiChatFieldsFromPeerEntity(
const emojiStatus = userOrChannel?.emojiStatus ? buildApiEmojiStatus(userOrChannel.emojiStatus) : undefined; const emojiStatus = userOrChannel?.emojiStatus ? buildApiEmojiStatus(userOrChannel.emojiStatus) : undefined;
const paidMessagesStars = userOrChannel?.sendPaidMessagesStars; const paidMessagesStars = userOrChannel?.sendPaidMessagesStars;
const isVerified = userOrChannel?.verified; const isVerified = userOrChannel?.verified;
const isForum = channel?.forum || user?.botForumView;
return { return {
isMin, isMin,
@ -114,8 +113,7 @@ function buildApiChatFieldsFromPeerEntity(
profileColor, profileColor,
isJoinToSend: channel?.joinToSend, isJoinToSend: channel?.joinToSend,
isJoinRequest: channel?.joinRequest, isJoinRequest: channel?.joinRequest,
isForum, isForum: channel?.forum,
isBotForum: user?.botForumView,
isMonoforum: channel?.monoforum, isMonoforum: channel?.monoforum,
linkedMonoforumId: channel?.linkedMonoforumId !== undefined linkedMonoforumId: channel?.linkedMonoforumId !== undefined
? buildApiPeerId(channel.linkedMonoforumId, 'channel') : undefined, ? buildApiPeerId(channel.linkedMonoforumId, 'channel') : undefined,

View File

@ -36,7 +36,6 @@ import { ApiMessageEntityTypes, MAIN_THREAD_ID } from '../../types';
import { import {
DELETED_COMMENTS_CHANNEL_ID, DELETED_COMMENTS_CHANNEL_ID,
LOCAL_MESSAGES_LIMIT,
SERVICE_NOTIFICATIONS_USER_ID, SERVICE_NOTIFICATIONS_USER_ID,
SPONSORED_MESSAGE_CACHE_MS, SPONSORED_MESSAGE_CACHE_MS,
SUPPORTED_AUDIO_CONTENT_TYPES, SUPPORTED_AUDIO_CONTENT_TYPES,
@ -77,6 +76,8 @@ import { buildApiRestrictionReasons } from './misc';
import { buildApiPeerColor, buildApiPeerId, getApiChatIdFromMtpPeer } from './peers'; import { buildApiPeerColor, buildApiPeerId, getApiChatIdFromMtpPeer } from './peers';
import { buildMessageReactions } from './reactions'; import { buildMessageReactions } from './reactions';
const LOCAL_MESSAGES_LIMIT = 1e6; // 1M
const LOCAL_MEDIA_UPLOADING_TEMP_ID = 'temp'; const LOCAL_MEDIA_UPLOADING_TEMP_ID = 'temp';
const INPUT_WAVEFORM_LENGTH = 63; const INPUT_WAVEFORM_LENGTH = 63;
const MIN_SCHEDULED_PERIOD = 10; const MIN_SCHEDULED_PERIOD = 10;
@ -86,10 +87,6 @@ function getNextLocalMessageId(lastMessageId = 0) {
return lastMessageId + (++localMessageCounter / LOCAL_MESSAGES_LIMIT); return lastMessageId + (++localMessageCounter / LOCAL_MESSAGES_LIMIT);
} }
export function incrementLocalMessageCounter() {
localMessageCounter++;
}
let currentUserId!: string; let currentUserId!: string;
export function setMessageBuilderCurrentUserId(_currentUserId: string) { export function setMessageBuilderCurrentUserId(_currentUserId: string) {

View File

@ -112,7 +112,7 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined {
const { const {
id, firstName, lastName, fake, scam, support, closeFriend, storiesUnavailable, storiesMaxId, id, firstName, lastName, fake, scam, support, closeFriend, storiesUnavailable, storiesMaxId,
bot, botActiveUsers, botVerificationIcon, botInlinePlaceholder, botAttachMenu, botCanEdit, bot, botActiveUsers, botVerificationIcon, botInlinePlaceholder, botAttachMenu, botCanEdit,
sendPaidMessagesStars, profileColor, botForumView, sendPaidMessagesStars, profileColor,
} = mtpUser; } = mtpUser;
const hasVideoAvatar = mtpUser.photo instanceof GramJs.UserProfilePhoto ? Boolean(mtpUser.photo.hasVideo) : undefined; const hasVideoAvatar = mtpUser.photo instanceof GramJs.UserProfilePhoto ? Boolean(mtpUser.photo.hasVideo) : undefined;
const avatarPhotoId = mtpUser.photo && buildAvatarPhotoId(mtpUser.photo); const avatarPhotoId = mtpUser.photo && buildAvatarPhotoId(mtpUser.photo);
@ -155,7 +155,6 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined {
color: mtpUser.color && buildApiPeerColor(mtpUser.color), color: mtpUser.color && buildApiPeerColor(mtpUser.color),
profileColor: profileColor && buildApiPeerColor(profileColor), profileColor: profileColor && buildApiPeerColor(profileColor),
paidMessagesStars: toJSNumber(sendPaidMessagesStars), paidMessagesStars: toJSNumber(sendPaidMessagesStars),
isBotForum: botForumView,
}; };
} }

View File

@ -16,14 +16,13 @@ import { processAffectedHistory } from '../updates/updateManager';
import { invokeRequest } from './client'; import { invokeRequest } from './client';
export async function createTopic({ export async function createTopic({
chat, title, iconColor, iconEmojiId, sendAs, isTitleMissing, chat, title, iconColor, iconEmojiId, sendAs,
}: { }: {
chat: ApiChat; chat: ApiChat;
title: string; title: string;
iconColor?: number; iconColor?: number;
iconEmojiId?: string; iconEmojiId?: string;
sendAs?: ApiPeer; sendAs?: ApiPeer;
isTitleMissing?: true;
}) { }) {
const { id, accessHash } = chat; const { id, accessHash } = chat;
@ -34,7 +33,6 @@ export async function createTopic({
iconEmojiId: iconEmojiId ? BigInt(iconEmojiId) : undefined, iconEmojiId: iconEmojiId ? BigInt(iconEmojiId) : undefined,
sendAs: sendAs ? buildInputPeer(sendAs.id, sendAs.accessHash) : undefined, sendAs: sendAs ? buildInputPeer(sendAs.id, sendAs.accessHash) : undefined,
randomId: generateRandomBigInt(), randomId: generateRandomBigInt(),
titleMissing: isTitleMissing,
})); }));
if (!(updates instanceof GramJs.Updates) || !updates.updates.length) { if (!(updates instanceof GramJs.Updates) || !updates.updates.length) {
@ -77,10 +75,9 @@ export async function fetchTopics({
if (!result) return undefined; if (!result) return undefined;
const { orderByCreateDate } = result; const { count, orderByCreateDate } = result;
const topics = result.topics.map(buildApiTopic).filter(Boolean); const topics = result.topics.map(buildApiTopic).filter(Boolean);
const count = result.count === 0 ? topics.length : result.count; // Sometimes count is 0 in result, but we have topics
const messages = result.messages.map(buildApiMessage).filter(Boolean); const messages = result.messages.map(buildApiMessage).filter(Boolean);
const draftsById = result.topics.reduce((acc, topic) => { const draftsById = result.topics.reduce((acc, topic) => {
if (topic instanceof GramJs.ForumTopic && topic.draft) { if (topic instanceof GramJs.ForumTopic && topic.draft) {

View File

@ -75,7 +75,6 @@ import {
buildLocalMessage, buildLocalMessage,
buildPreparedInlineMessage, buildPreparedInlineMessage,
buildUploadingMedia, buildUploadingMedia,
incrementLocalMessageCounter,
} from '../apiBuilders/messages'; } from '../apiBuilders/messages';
import { getApiChatIdFromMtpPeer } from '../apiBuilders/peers'; import { getApiChatIdFromMtpPeer } from '../apiBuilders/peers';
import { buildApiUser, buildApiUserStatuses } from '../apiBuilders/users'; import { buildApiUser, buildApiUserStatuses } from '../apiBuilders/users';
@ -1256,21 +1255,23 @@ export async function markMessageListRead({
}) { }) {
const isChannel = getEntityTypeById(chat.id) === 'channel'; const isChannel = getEntityTypeById(chat.id) === 'channel';
// Workaround for local message IDs overflowing some internal `Buffer` range check
const fixedMaxId = Math.min(maxId, MAX_INT_32);
if (isChannel && threadId === MAIN_THREAD_ID) { if (isChannel && threadId === MAIN_THREAD_ID) {
await invokeRequest(new GramJs.channels.ReadHistory({ await invokeRequest(new GramJs.channels.ReadHistory({
channel: buildInputChannel(chat.id, chat.accessHash), channel: buildInputChannel(chat.id, chat.accessHash),
maxId, maxId: fixedMaxId,
})); }));
} else if (threadId !== MAIN_THREAD_ID) { } else if (isChannel) {
await invokeRequest(new GramJs.messages.ReadDiscussion({ await invokeRequest(new GramJs.messages.ReadDiscussion({
peer: buildInputPeer(chat.id, chat.accessHash), peer: buildInputPeer(chat.id, chat.accessHash),
msgId: Number(threadId), msgId: Number(threadId),
readMaxId: maxId, readMaxId: fixedMaxId,
})); }));
} else { } else {
const result = await invokeRequest(new GramJs.messages.ReadHistory({ const result = await invokeRequest(new GramJs.messages.ReadHistory({
peer: buildInputPeer(chat.id, chat.accessHash), peer: buildInputPeer(chat.id, chat.accessHash),
maxId, maxId: fixedMaxId,
})); }));
if (result) { if (result) {
@ -2554,7 +2555,3 @@ export async function fetchPreparedInlineMessage({
return buildPreparedInlineMessage(result); return buildPreparedInlineMessage(result);
} }
export function incrementLocalMessagesCounter() {
incrementLocalMessageCounter();
}

View File

@ -28,7 +28,6 @@ import {
buildChatTypingStatus, buildChatTypingStatus,
} from '../apiBuilders/chats'; } from '../apiBuilders/chats';
import { import {
buildApiFormattedText,
buildApiPhoto, buildApiUsernames, buildPrivacyRules, buildApiPhoto, buildApiUsernames, buildPrivacyRules,
} from '../apiBuilders/common'; } from '../apiBuilders/common';
import { omitVirtualClassFields } from '../apiBuilders/helpers'; import { omitVirtualClassFields } from '../apiBuilders/helpers';
@ -497,9 +496,10 @@ export function updater(update: Update) {
sendApiUpdate({ sendApiUpdate({
'@type': 'updateChatInbox', '@type': 'updateChatInbox',
id: getApiChatIdFromMtpPeer(update.peer), id: getApiChatIdFromMtpPeer(update.peer),
lastReadInboxMessageId: update.maxId, chat: {
unreadCount: update.stillUnreadCount, lastReadInboxMessageId: update.maxId,
threadId: update.topMsgId, unreadCount: update.stillUnreadCount,
},
}); });
} else if (update instanceof GramJs.UpdateReadHistoryOutbox) { } else if (update instanceof GramJs.UpdateReadHistoryOutbox) {
sendApiUpdate({ sendApiUpdate({
@ -648,33 +648,22 @@ export function updater(update: Update) {
update instanceof GramJs.UpdateUserTyping update instanceof GramJs.UpdateUserTyping
|| update instanceof GramJs.UpdateChatUserTyping || update instanceof GramJs.UpdateChatUserTyping
) { ) {
const chatId = update instanceof GramJs.UpdateUserTyping const id = update instanceof GramJs.UpdateUserTyping
? buildApiPeerId(update.userId, 'user') ? buildApiPeerId(update.userId, 'user')
: buildApiPeerId(update.chatId, 'chat'); : buildApiPeerId(update.chatId, 'chat');
const threadId = update instanceof GramJs.UpdateUserTyping ? update.topMsgId : undefined;
if (update.action instanceof GramJs.SendMessageEmojiInteraction) { if (update.action instanceof GramJs.SendMessageEmojiInteraction) {
sendApiUpdate({ sendApiUpdate({
'@type': 'updateStartEmojiInteraction', '@type': 'updateStartEmojiInteraction',
id: chatId, id,
emoji: update.action.emoticon, emoji: update.action.emoticon,
messageId: update.action.msgId, messageId: update.action.msgId,
interaction: buildApiEmojiInteraction(JSON.parse(update.action.interaction.data)), interaction: buildApiEmojiInteraction(JSON.parse(update.action.interaction.data)),
}); });
} else if (update.action instanceof GramJs.SendMessageTextDraftAction) {
sendApiUpdate({
'@type': 'updateChatTypingDraft',
chatId,
id: update.action.randomId.toString(),
threadId,
text: buildApiFormattedText(update.action.text),
});
} else { } else {
sendApiUpdate({ sendApiUpdate({
'@type': 'updateChatTypingStatus', '@type': 'updateChatTypingStatus',
id: chatId, id,
threadId,
typingStatus: buildChatTypingStatus(update), typingStatus: buildChatTypingStatus(update),
}); });
} }

View File

@ -57,7 +57,6 @@ export interface ApiChat {
isForum?: boolean; isForum?: boolean;
isForumAsMessages?: true; isForumAsMessages?: true;
isMonoforum?: boolean; isMonoforum?: boolean;
isBotForum?: boolean;
withForumTabs?: boolean; withForumTabs?: boolean;
linkedMonoforumId?: string; linkedMonoforumId?: string;
areChannelMessagesAllowed?: boolean; areChannelMessagesAllowed?: boolean;

View File

@ -489,7 +489,7 @@ export type ApiMessageEntityDefault = {
type: Exclude< type: Exclude<
`${ApiMessageEntityTypes}`, `${ApiMessageEntityTypes}`,
`${ApiMessageEntityTypes.Pre}` | `${ApiMessageEntityTypes.TextUrl}` | `${ApiMessageEntityTypes.MentionName}` | `${ApiMessageEntityTypes.Pre}` | `${ApiMessageEntityTypes.TextUrl}` | `${ApiMessageEntityTypes.MentionName}` |
`${ApiMessageEntityTypes.Blockquote}` | `${ApiMessageEntityTypes.CustomEmoji}` | `${ApiMessageEntityTypes.Timestamp}` `${ApiMessageEntityTypes.CustomEmoji}` | `${ApiMessageEntityTypes.Blockquote}` | `${ApiMessageEntityTypes.Timestamp}`
>; >;
offset: number; offset: number;
length: number; length: number;
@ -538,8 +538,15 @@ export type ApiMessageEntityTimestamp = {
timestamp: number; timestamp: number;
}; };
export type ApiMessageEntityQuoteFocus = {
type: 'quoteFocus';
offset: number;
length: number;
};
export type ApiMessageEntity = ApiMessageEntityDefault | ApiMessageEntityPre | ApiMessageEntityTextUrl | export type ApiMessageEntity = ApiMessageEntityDefault | ApiMessageEntityPre | ApiMessageEntityTextUrl |
ApiMessageEntityMentionName | ApiMessageEntityCustomEmoji | ApiMessageEntityBlockquote | ApiMessageEntityTimestamp; ApiMessageEntityMentionName | ApiMessageEntityCustomEmoji | ApiMessageEntityBlockquote | ApiMessageEntityTimestamp |
ApiMessageEntityQuoteFocus;
export enum ApiMessageEntityTypes { export enum ApiMessageEntityTypes {
Bold = 'MessageEntityBold', Bold = 'MessageEntityBold',
@ -676,8 +683,6 @@ export interface ApiMessage {
reportDeliveryUntilDate?: number; reportDeliveryUntilDate?: number;
paidMessageStars?: number; paidMessageStars?: number;
restrictionReasons?: ApiRestrictionReason[]; restrictionReasons?: ApiRestrictionReason[];
isTypingDraft?: boolean; // Local field
} }
export interface ApiReactions { export interface ApiReactions {
@ -917,18 +922,13 @@ interface ApiKeyboardButtonCopy {
copyText: string; copyText: string;
} }
export interface KeyboardButtonSuggestedMessage { export interface ApiKeyboardButtonSuggestedMessage {
type: 'suggestedMessage'; type: 'suggestedMessage';
text: string; text: string;
buttonType: 'approve' | 'decline' | 'suggestChanges'; buttonType: 'approve' | 'decline' | 'suggestChanges';
disabled?: boolean; disabled?: boolean;
} }
export interface KeyboardButtonOpenThread {
type: 'openThread';
text: string;
}
export type ApiKeyboardButton = ( export type ApiKeyboardButton = (
ApiKeyboardButtonSimple ApiKeyboardButtonSimple
| ApiKeyboardButtonReceipt | ApiKeyboardButtonReceipt
@ -941,8 +941,7 @@ export type ApiKeyboardButton = (
| ApiKeyboardButtonSimpleWebView | ApiKeyboardButtonSimpleWebView
| ApiKeyboardButtonUrlAuth | ApiKeyboardButtonUrlAuth
| ApiKeyboardButtonCopy | ApiKeyboardButtonCopy
| KeyboardButtonSuggestedMessage | ApiKeyboardButtonSuggestedMessage
| KeyboardButtonOpenThread
); );
export type ApiKeyboardButtons = ApiKeyboardButton[][]; export type ApiKeyboardButtons = ApiKeyboardButton[][];

View File

@ -274,7 +274,6 @@ export interface ApiAppConfig {
verifyAgeBotUsername?: string; verifyAgeBotUsername?: string;
verifyAgeCountry?: string; verifyAgeCountry?: string;
verifyAgeMin?: number; verifyAgeMin?: number;
typingDraftTtl: number;
contactNoteLimit?: number; contactNoteLimit?: number;
} }

View File

@ -132,9 +132,7 @@ export type ApiUpdateChatLeave = {
export type ApiUpdateChatInbox = { export type ApiUpdateChatInbox = {
'@type': 'updateChatInbox'; '@type': 'updateChatInbox';
id: string; id: string;
threadId?: ThreadId; chat: Partial<ApiChat>;
lastReadInboxMessageId: number;
unreadCount: number;
}; };
export type ApiUpdateChatTypingStatus = { export type ApiUpdateChatTypingStatus = {
@ -144,14 +142,6 @@ export type ApiUpdateChatTypingStatus = {
typingStatus: ApiTypingStatus | undefined; typingStatus: ApiTypingStatus | undefined;
}; };
export type ApiUpdateChatTypingDraft = {
'@type': 'updateChatTypingDraft';
chatId: string;
id: string;
threadId?: ThreadId;
text: ApiFormattedText;
};
export type ApiUpdateStartEmojiInteraction = { export type ApiUpdateStartEmojiInteraction = {
'@type': 'updateStartEmojiInteraction'; '@type': 'updateStartEmojiInteraction';
id: string; id: string;
@ -888,7 +878,7 @@ export type ApiUpdate = (
ApiUpdateRecentStickers | ApiUpdateSavedGifs | ApiUpdateNewScheduledMessage | ApiUpdateMoveStickerSetToTop | ApiUpdateRecentStickers | ApiUpdateSavedGifs | ApiUpdateNewScheduledMessage | ApiUpdateMoveStickerSetToTop |
ApiUpdateScheduledMessageSendSucceeded | ApiUpdateScheduledMessage | ApiUpdateStarPaymentStateCompleted | ApiUpdateScheduledMessageSendSucceeded | ApiUpdateScheduledMessage | ApiUpdateStarPaymentStateCompleted |
ApiUpdateDeleteScheduledMessages | ApiUpdateResetMessages | ApiUpdateMessageTranslations | ApiUpdateDeleteScheduledMessages | ApiUpdateResetMessages | ApiUpdateMessageTranslations |
ApiUpdateFailedMessageTranslations | ApiUpdateWebPage | ApiUpdateChatTypingDraft | ApiUpdateFailedMessageTranslations | ApiUpdateWebPage |
ApiUpdateTwoFaError | ApiUpdateTwoFaStateWaitCode | ApiUpdateWebViewResultSent | ApiUpdateTwoFaError | ApiUpdateTwoFaStateWaitCode | ApiUpdateWebViewResultSent |
ApiUpdateDefaultNotifySettings | ApiUpdatePeerNotifySettings | ApiUpdatePeerBlocked | ApiUpdatePrivacy | ApiUpdateDefaultNotifySettings | ApiUpdatePeerNotifySettings | ApiUpdatePeerBlocked | ApiUpdatePrivacy |
ApiUpdateServerTimeOffset | ApiUpdateMessageReactions | ApiUpdateSavedReactionTags | ApiUpdateServerTimeOffset | ApiUpdateMessageReactions | ApiUpdateSavedReactionTags |

View File

@ -47,7 +47,6 @@ export interface ApiUser {
botActiveUsers?: number; botActiveUsers?: number;
botVerificationIconId?: string; botVerificationIconId?: string;
paidMessagesStars?: number; paidMessagesStars?: number;
isBotForum?: boolean;
} }
export interface ApiUserFullInfo { export interface ApiUserFullInfo {

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="43" height="43" viewBox="0 0 43 43"><path d="M31.934 20.178q.856.001 1.678.128.032.005.065.012c5.21.835 9.19 5.348 9.19 10.793 0 6.038-4.895 10.933-10.933 10.933-4.066 0-7.61-2.22-9.495-5.514h-.002l-.06-.106a11 11 0 0 1-.502-1.023l-.06-.147a11 11 0 0 1-.189-.495 11 11 0 0 1-.104-.306l-.053-.17a11 11 0 0 1-.11-.389l-.039-.156a11 11 0 0 1-.076-.331l-.034-.16a11 11 0 0 1-.071-.4l-.02-.13q-.031-.21-.055-.425-.008-.082-.015-.166A11 11 0 0 1 21 31.11c0-5.85 4.593-10.626 10.37-10.92q.28-.013.563-.013M21 .133c5.64 0 10.735 2.118 14.377 5.526 3.507 3.281 5.667 7.76 5.667 12.697q-.002 1.418-.232 2.778a13.3 13.3 0 0 0-8.878-3.38c-7.377 0-13.358 5.981-13.358 13.358 0 1.932.41 3.768 1.149 5.426q-.15-.007-.301-.016l-.047-.003a22 22 0 0 1-3.297-.494c-.298-.068-.636.244-1.2.766-.683.63-1.696 1.566-3.37 2.504-2.137 1.199-5.098 1.095-5.6.885-.48-.201-.11-.603.541-1.308.45-.489 1.036-1.123 1.568-1.938 1.3-1.993.763-4.367.18-4.794C3.636 28.8.955 24.096.955 18.356.956 8.292 9.931.133 21.001.133m10.934 24.3c-.67 0-1.212.543-1.212 1.212V29.9h-4.255a1.212 1.212 0 0 0 0 2.424h4.255v4.255a1.213 1.213 0 0 0 2.424 0v-4.255H37.4a1.213 1.213 0 0 0 0-2.424h-4.254v-4.255c0-.669-.544-1.212-1.212-1.212"/></svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -2301,10 +2301,6 @@
"TitleGiftLocked" = "Gift Locked"; "TitleGiftLocked" = "Gift Locked";
"GiftLockedMessage" = "This gift is currently only available to earlier Telegram users. It will unlock for your account in about **{relativeDate}**."; "GiftLockedMessage" = "This gift is currently only available to earlier Telegram users. It will unlock for your account in about **{relativeDate}**.";
"QuickPreview" = "Quick Preview"; "QuickPreview" = "Quick Preview";
"BotForumContinueThreadButton" = "Continue Last Thread";
"BotForumActionNew" = "New Thread";
"BotForumActionNewDescription" = "Type any message to create a new thread.";
"BotForumTopicTitlePlaceholder" = "New Thread";
"DropOriginalDetailsTransaction" = "Removed Gift Description"; "DropOriginalDetailsTransaction" = "Removed Gift Description";
"StarGiftReasonDropOriginalDetails" = "Removed Description"; "StarGiftReasonDropOriginalDetails" = "Removed Description";
"GiftAnUpgradeButton" = "Gift an Upgrade"; "GiftAnUpgradeButton" = "Gift an Upgrade";
@ -2314,8 +2310,6 @@
"UserNoteTitle" = "Notes"; "UserNoteTitle" = "Notes";
"UserNoteHint" = "only visible to you"; "UserNoteHint" = "only visible to you";
"EditUserNoteHint" = "Notes are only visible to you."; "EditUserNoteHint" = "Notes are only visible to you.";
"BotForumAllTopicTitle" = "All Messages";
"BotForumAllTopicDescription" = "All messages from all topics";
"AriaStoryTogglerOpen" = "Open Story List"; "AriaStoryTogglerOpen" = "Open Story List";
"FileTransferProgress" = "{currentSize} / {totalSize}"; "FileTransferProgress" = "{currentSize} / {totalSize}";
"MediaSizeB_one" = "{size}B"; "MediaSizeB_one" = "{size}B";

View File

@ -1705,7 +1705,7 @@ const Composer: FC<OwnProps & StateProps> = ({
return lang('ComposerPlaceholderAnonymous'); return lang('ComposerPlaceholderAnonymous');
} }
if (chat?.isForum && !chat.isBotForum && chat.isForumAsMessages && threadId === MAIN_THREAD_ID) { if (chat?.isForum && chat?.isForumAsMessages && threadId === MAIN_THREAD_ID) {
return replyToTopic return replyToTopic
? lang('ComposerPlaceholderTopic', { topic: replyToTopic.title }) ? lang('ComposerPlaceholderTopic', { topic: replyToTopic.title })
: lang('ComposerPlaceholderTopicGeneral'); : lang('ComposerPlaceholderTopicGeneral');

View File

@ -1,8 +1,10 @@
import type { FC } from '../../lib/teact/teact';
import type React from '../../lib/teact/teact';
import { memo, useEffect, useMemo } from '../../lib/teact/teact'; import { memo, useEffect, useMemo } from '../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../global'; import { getActions, getGlobal, withGlobal } from '../../global';
import type { import type {
ApiChat, ApiTopic, ApiTypingStatus, ApiUser, ApiChat, ApiThreadInfo, ApiTopic, ApiTypingStatus, ApiUser,
} from '../../api/types'; } from '../../api/types';
import type { IconName } from '../../types/icons'; import type { IconName } from '../../types/icons';
import { MediaViewerOrigin, type StoryViewerOrigin, type ThreadId } from '../../types'; import { MediaViewerOrigin, type StoryViewerOrigin, type ThreadId } from '../../types';
@ -19,6 +21,7 @@ import {
selectChatOnlineCount, selectChatOnlineCount,
selectIsChatRestricted, selectIsChatRestricted,
selectMonoforumChannel, selectMonoforumChannel,
selectThreadInfo,
selectThreadMessagesCount, selectThreadMessagesCount,
selectTopic, selectTopic,
selectUser, selectUser,
@ -65,11 +68,12 @@ type OwnProps = {
isSavedDialog?: boolean; isSavedDialog?: boolean;
withMonoforumStatus?: boolean; withMonoforumStatus?: boolean;
onClick?: VoidFunction; onClick?: VoidFunction;
onEmojiStatusClick?: VoidFunction; onEmojiStatusClick?: NoneToVoidFunction;
}; };
type StateProps = { type StateProps = {
chat?: ApiChat; chat?: ApiChat;
threadInfo?: ApiThreadInfo;
topic?: ApiTopic; topic?: ApiTopic;
onlineCount?: number; onlineCount?: number;
areMessagesLoaded: boolean; areMessagesLoaded: boolean;
@ -78,7 +82,7 @@ type StateProps = {
monoforumChannel?: ApiChat; monoforumChannel?: ApiChat;
}; };
const GroupChatInfo = ({ const GroupChatInfo: FC<OwnProps & StateProps> = ({
typingStatus, typingStatus,
className, className,
statusIcon, statusIcon,
@ -91,6 +95,7 @@ const GroupChatInfo = ({
withFullInfo, withFullInfo,
withUpdatingStatus, withUpdatingStatus,
withChatType, withChatType,
threadInfo,
noRtl, noRtl,
chat: realChat, chat: realChat,
onlineCount, onlineCount,
@ -108,7 +113,7 @@ const GroupChatInfo = ({
monoforumChannel, monoforumChannel,
onClick, onClick,
onEmojiStatusClick, onEmojiStatusClick,
}: OwnProps & StateProps) => { }) => {
const { const {
loadFullChat, loadFullChat,
openMediaViewer, openMediaViewer,
@ -121,7 +126,7 @@ const GroupChatInfo = ({
const lang = useLang(); const lang = useLang();
const isSuperGroup = chat && isChatSuperGroup(chat); const isSuperGroup = chat && isChatSuperGroup(chat);
const isTopic = Boolean(chat?.isForum && topic); const isTopic = Boolean(chat?.isForum && threadInfo && topic);
const { id: chatId, isMin } = chat || {}; const { id: chatId, isMin } = chat || {};
const isRestricted = selectIsChatRestricted(getGlobal(), chatId!); const isRestricted = selectIsChatRestricted(getGlobal(), chatId!);
@ -199,7 +204,7 @@ const GroupChatInfo = ({
activeKey={messagesCount !== undefined ? 1 : 2} activeKey={messagesCount !== undefined ? 1 : 2}
className="message-count-transition" className="message-count-transition"
> >
{messagesCount !== undefined ? oldLang('messages', messagesCount, 'i') : oldLang('lng_forum_no_messages')} {messagesCount !== undefined && oldLang('messages', messagesCount, 'i')}
</Transition> </Transition>
</span> </span>
); );
@ -285,6 +290,7 @@ const GroupChatInfo = ({
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global, { chatId, threadId }): Complete<StateProps> => { (global, { chatId, threadId }): Complete<StateProps> => {
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
const threadInfo = threadId ? selectThreadInfo(global, chatId, threadId) : undefined;
const onlineCount = chat ? selectChatOnlineCount(global, chat) : undefined; const onlineCount = chat ? selectChatOnlineCount(global, chat) : undefined;
const areMessagesLoaded = Boolean(selectChatMessages(global, chatId)); const areMessagesLoaded = Boolean(selectChatMessages(global, chatId));
const topic = threadId ? selectTopic(global, chatId, threadId) : undefined; const topic = threadId ? selectTopic(global, chatId, threadId) : undefined;
@ -294,6 +300,7 @@ export default memo(withGlobal<OwnProps>(
return { return {
chat, chat,
threadInfo,
onlineCount, onlineCount,
topic, topic,
areMessagesLoaded, areMessagesLoaded,

View File

@ -12,12 +12,9 @@ import trimText from '../../util/trimText';
import { insertTextEntity, renderTextWithEntities } from './helpers/renderTextWithEntities'; import { insertTextEntity, renderTextWithEntities } from './helpers/renderTextWithEntities';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback';
import useSyncEffect from '../../hooks/useSyncEffect'; import useSyncEffect from '../../hooks/useSyncEffect';
import useUniqueId from '../../hooks/useUniqueId'; import useUniqueId from '../../hooks/useUniqueId';
import TypingWrapper from './TypingWrapper';
interface OwnProps { interface OwnProps {
messageOrStory: ApiMessage | ApiStory; messageOrStory: ApiMessage | ApiStory;
threadId?: ThreadId; threadId?: ThreadId;
@ -39,7 +36,6 @@ interface OwnProps {
isInSelectMode?: boolean; isInSelectMode?: boolean;
canBeEmpty?: boolean; canBeEmpty?: boolean;
maxTimestamp?: number; maxTimestamp?: number;
shouldAnimateTyping?: boolean;
} }
const MIN_CUSTOM_EMOJIS_FOR_SHARED_CANVAS = 3; const MIN_CUSTOM_EMOJIS_FOR_SHARED_CANVAS = 3;
@ -65,7 +61,6 @@ function MessageText({
canBeEmpty, canBeEmpty,
maxTimestamp, maxTimestamp,
threadId, threadId,
shouldAnimateTyping,
}: OwnProps) { }: OwnProps) {
const sharedCanvasRef = useRef<HTMLCanvasElement>(); const sharedCanvasRef = useRef<HTMLCanvasElement>();
const sharedCanvasHqRef = useRef<HTMLCanvasElement>(); const sharedCanvasHqRef = useRef<HTMLCanvasElement>();
@ -112,48 +107,37 @@ function MessageText({
return customEmojisCount >= MIN_CUSTOM_EMOJIS_FOR_SHARED_CANVAS; return customEmojisCount >= MIN_CUSTOM_EMOJIS_FOR_SHARED_CANVAS;
}, [entitiesWithFocusedQuote]) || 0; }, [entitiesWithFocusedQuote]) || 0;
const renderText = useLastCallback((t: ApiFormattedText) => {
return renderTextWithEntities({
text: t.text,
entities: t.entities,
highlight,
emojiSize,
shouldRenderAsHtml,
containerId,
asPreview,
isProtected,
observeIntersectionForLoading,
observeIntersectionForPlaying,
withTranslucentThumbs,
sharedCanvasRef,
sharedCanvasHqRef,
cacheBuster: textCacheBusterRef.current.toString(),
forcePlayback,
isInSelectMode,
maxTimestamp,
chatId: 'chatId' in messageOrStory ? messageOrStory.chatId : undefined,
messageId: messageOrStory.id,
threadId,
});
});
if (!text && !canBeEmpty) { if (!text && !canBeEmpty) {
return <span className="content-unsupported">{lang('MessageUnsupported')}</span>; return <span className="content-unsupported">{lang('MessageUnsupported')}</span>;
} }
const textToRender: ApiFormattedText = {
text: trimText(text || '', truncateLength),
entities: entitiesWithFocusedQuote,
};
return ( return (
<> <>
{[ {[
withSharedCanvas && <canvas key="shared-canvas" ref={sharedCanvasRef} className="shared-canvas" />, withSharedCanvas && <canvas ref={sharedCanvasRef} className="shared-canvas" />,
withSharedCanvas && <canvas key="shared-canvas-hq" ref={sharedCanvasHqRef} className="shared-canvas" />, withSharedCanvas && <canvas ref={sharedCanvasHqRef} className="shared-canvas" />,
shouldAnimateTyping ? ( renderTextWithEntities({
<TypingWrapper key="typing-wrapper" text={textToRender}>{renderText}</TypingWrapper> text: trimText(text!, truncateLength),
) : renderText(textToRender), entities: entitiesWithFocusedQuote,
highlight,
emojiSize,
shouldRenderAsHtml,
containerId,
asPreview,
isProtected,
observeIntersectionForLoading,
observeIntersectionForPlaying,
withTranslucentThumbs,
sharedCanvasRef,
sharedCanvasHqRef,
cacheBuster: textCacheBusterRef.current.toString(),
forcePlayback,
isInSelectMode,
maxTimestamp,
chatId: 'chatId' in messageOrStory ? messageOrStory.chatId : undefined,
messageId: messageOrStory.id,
threadId,
}),
].flat().filter(Boolean)} ].flat().filter(Boolean)}
</> </>
); );

View File

@ -1,25 +1,19 @@
import type { FC } from '../../lib/teact/teact';
import { memo, useEffect, useMemo } from '../../lib/teact/teact'; import { memo, useEffect, useMemo } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global'; import { getActions, withGlobal } from '../../global';
import type { import type {
ApiChatMember, ApiTopic, ApiTypingStatus, ApiUser, ApiUserStatus, ApiChatMember, ApiTypingStatus, ApiUser, ApiUserStatus,
} from '../../api/types'; } from '../../api/types';
import type { CustomPeer, StoryViewerOrigin, ThreadId } from '../../types'; import type { CustomPeer, StoryViewerOrigin } from '../../types';
import type { IconName } from '../../types/icons'; import type { IconName } from '../../types/icons';
import { MediaViewerOrigin } from '../../types'; import { MediaViewerOrigin } from '../../types';
import { import {
getMainUsername, getUserStatus, isSystemBot, isUserOnline, getMainUsername, getUserStatus, isSystemBot, isUserOnline,
} from '../../global/helpers'; } from '../../global/helpers';
import { import { selectChatMessages, selectUser, selectUserStatus } from '../../global/selectors';
selectChatMessages,
selectThreadMessagesCount,
selectTopic,
selectUser,
selectUserStatus,
} from '../../global/selectors';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import { REM } from './helpers/mediaDimensions';
import renderText from './helpers/renderText'; import renderText from './helpers/renderText';
import useIntervalForceUpdate from '../../hooks/schedulers/useIntervalForceUpdate'; import useIntervalForceUpdate from '../../hooks/schedulers/useIntervalForceUpdate';
@ -28,17 +22,15 @@ import useLastCallback from '../../hooks/useLastCallback';
import useOldLang from '../../hooks/useOldLang'; import useOldLang from '../../hooks/useOldLang';
import RippleEffect from '../ui/RippleEffect'; import RippleEffect from '../ui/RippleEffect';
import Transition from '../ui/Transition';
import Avatar from './Avatar'; import Avatar from './Avatar';
import DotAnimation from './DotAnimation'; import DotAnimation from './DotAnimation';
import FullNameTitle from './FullNameTitle'; import FullNameTitle from './FullNameTitle';
import Icon from './icons/Icon'; import Icon from './icons/Icon';
import TopicIcon from './TopicIcon';
import TypingStatus from './TypingStatus'; import TypingStatus from './TypingStatus';
const TOPIC_ICON_SIZE = 2.5 * REM; type OwnProps = {
userId?: string;
type BaseOwnProps = { customPeer?: CustomPeer;
typingStatus?: ApiTypingStatus; typingStatus?: ApiTypingStatus;
avatarSize?: 'tiny' | 'small' | 'medium' | 'large' | 'jumbo'; avatarSize?: 'tiny' | 'small' | 'medium' | 'large' | 'jumbo';
forceShowSelf?: boolean; forceShowSelf?: boolean;
@ -60,39 +52,25 @@ type BaseOwnProps = {
noRtl?: boolean; noRtl?: boolean;
adminMember?: ApiChatMember; adminMember?: ApiChatMember;
isSavedDialog?: boolean; isSavedDialog?: boolean;
noAvatar?: boolean;
className?: string; className?: string;
onEmojiStatusClick?: NoneToVoidFunction;
iconElement?: React.ReactNode; iconElement?: React.ReactNode;
rightElement?: React.ReactNode; rightElement?: React.ReactNode;
onClick?: VoidFunction;
onEmojiStatusClick?: VoidFunction;
}; };
type OwnProps = BaseOwnProps & ({ type StateProps =
userId: string; {
threadId?: ThreadId; user?: ApiUser;
customPeer?: never; userStatus?: ApiUserStatus;
} | { self?: ApiUser;
userId?: never; isSavedMessages?: boolean;
threadId?: never; areMessagesLoaded: boolean;
customPeer: CustomPeer; isSynced?: boolean;
}); };
type StateProps = {
user?: ApiUser;
userStatus?: ApiUserStatus;
self?: ApiUser;
isSavedMessages?: boolean;
areMessagesLoaded: boolean;
isSynced?: boolean;
topic?: ApiTopic;
messagesCount?: number;
};
const UPDATE_INTERVAL = 1000 * 60; // 1 min const UPDATE_INTERVAL = 1000 * 60; // 1 min
const PrivateChatInfo = ({ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
userId,
customPeer, customPeer,
typingStatus, typingStatus,
avatarSize = 'medium', avatarSize = 'medium',
@ -113,8 +91,6 @@ const PrivateChatInfo = ({
user, user,
userStatus, userStatus,
self, self,
topic,
messagesCount,
isSavedMessages, isSavedMessages,
isSavedDialog, isSavedDialog,
areMessagesLoaded, areMessagesLoaded,
@ -122,13 +98,11 @@ const PrivateChatInfo = ({
ripple, ripple,
className, className,
storyViewerOrigin, storyViewerOrigin,
noAvatar,
isSynced, isSynced,
onEmojiStatusClick,
iconElement, iconElement,
rightElement, rightElement,
onClick, }) => {
onEmojiStatusClick,
}: OwnProps & StateProps) => {
const { const {
loadFullUser, loadFullUser,
openMediaViewer, openMediaViewer,
@ -138,7 +112,8 @@ const PrivateChatInfo = ({
const oldLang = useOldLang(); const oldLang = useOldLang();
const lang = useLang(); const lang = useLang();
const isTopic = Boolean(user?.isBotForum && topic); const { id: userId } = user || {};
const hasAvatarMediaViewer = withMediaViewer && !isSavedMessages; const hasAvatarMediaViewer = withMediaViewer && !isSavedMessages;
useEffect(() => { useEffect(() => {
@ -152,11 +127,11 @@ const PrivateChatInfo = ({
const handleAvatarViewerOpen = useLastCallback( const handleAvatarViewerOpen = useLastCallback(
(e: React.MouseEvent<HTMLDivElement, MouseEvent>, hasMedia: boolean) => { (e: React.MouseEvent<HTMLDivElement, MouseEvent>, hasMedia: boolean) => {
if (hasMedia) { if (user && hasMedia) {
e.stopPropagation(); e.stopPropagation();
openMediaViewer({ openMediaViewer({
isAvatarView: true, isAvatarView: true,
chatId: userId, chatId: user.id,
mediaIndex: 0, mediaIndex: 0,
origin: avatarSize === 'jumbo' ? MediaViewerOrigin.ProfileAvatar : MediaViewerOrigin.MiddleHeaderAvatar, origin: avatarSize === 'jumbo' ? MediaViewerOrigin.ProfileAvatar : MediaViewerOrigin.MiddleHeaderAvatar,
}); });
@ -204,21 +179,6 @@ const PrivateChatInfo = ({
return <TypingStatus typingStatus={typingStatus} />; return <TypingStatus typingStatus={typingStatus} />;
} }
if (isTopic) {
return (
<span className="status" dir="auto">
<Transition
name="fade"
shouldRestoreHeight
activeKey={messagesCount !== undefined ? 1 : 2}
className="message-count-transition"
>
{messagesCount !== undefined ? oldLang('messages', messagesCount, 'i') : oldLang('lng_forum_no_messages')}
</Transition>
</span>
);
}
if (isSystemBot(user.id)) { if (isSystemBot(user.id)) {
return undefined; return undefined;
} }
@ -238,12 +198,6 @@ const PrivateChatInfo = ({
: undefined; : undefined;
function renderNameTitle() { function renderNameTitle() {
if (isTopic) {
return (
<h3 dir="auto" className="fullName">{renderText(topic!.title)}</h3>
);
}
if (customTitle) { if (customTitle) {
return ( return (
<div className="info-name-title"> <div className="info-name-title">
@ -276,11 +230,7 @@ const PrivateChatInfo = ({
} }
return ( return (
<div <div className={buildClassName('ChatInfo', className)} dir={!noRtl && lang.isRtl ? 'rtl' : undefined}>
className={buildClassName('ChatInfo', className)}
dir={!noRtl && lang.isRtl ? 'rtl' : undefined}
onClick={onClick}
>
{isSavedDialog && self && ( {isSavedDialog && self && (
<Avatar <Avatar
key="saved-messages" key="saved-messages"
@ -290,27 +240,18 @@ const PrivateChatInfo = ({
className="saved-dialog-avatar" className="saved-dialog-avatar"
/> />
)} )}
{!noAvatar && !isTopic && ( <Avatar
<Avatar key={user?.id}
key={user?.id} size={avatarSize}
size={avatarSize} peer={customPeer || user}
peer={customPeer || user} className={buildClassName(isSavedDialog && 'overlay-avatar')}
className={buildClassName(isSavedDialog && 'overlay-avatar')} isSavedMessages={isSavedMessages}
isSavedMessages={isSavedMessages} isSavedDialog={isSavedDialog}
isSavedDialog={isSavedDialog} withStory={withStory}
withStory={withStory} storyViewerOrigin={storyViewerOrigin}
storyViewerOrigin={storyViewerOrigin} storyViewerMode="single-peer"
storyViewerMode="single-peer" onClick={hasAvatarMediaViewer ? handleAvatarViewerOpen : undefined}
onClick={hasAvatarMediaViewer ? handleAvatarViewerOpen : undefined} />
/>
)}
{isTopic && (
<TopicIcon
topic={topic!}
className="topic-header-icon"
size={TOPIC_ICON_SIZE}
/>
)}
<div className="info"> <div className="info">
{renderNameTitle()} {renderNameTitle()}
{(status || (!isSavedMessages && !noStatusOrTyping)) && renderStatusOrTyping()} {(status || (!isSavedMessages && !noStatusOrTyping)) && renderStatusOrTyping()}
@ -322,16 +263,13 @@ const PrivateChatInfo = ({
}; };
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global, { userId, threadId, forceShowSelf }): Complete<StateProps> => { (global, { userId, forceShowSelf }): Complete<StateProps> => {
const { isSynced } = global; const { isSynced } = global;
const user = userId ? selectUser(global, userId) : undefined; const user = userId ? selectUser(global, userId) : undefined;
const userStatus = userId ? selectUserStatus(global, userId) : undefined; const userStatus = userId ? selectUserStatus(global, userId) : undefined;
const isSavedMessages = !forceShowSelf && user && user.isSelf; const isSavedMessages = !forceShowSelf && user && user.isSelf;
const self = isSavedMessages ? user : selectUser(global, global.currentUserId!); const self = isSavedMessages ? user : selectUser(global, global.currentUserId!);
const areMessagesLoaded = Boolean(userId ? selectChatMessages(global, userId) : undefined); const areMessagesLoaded = Boolean(userId && selectChatMessages(global, userId));
const topic = threadId ? selectTopic(global, userId, threadId) : undefined;
const messagesCount = topic && userId ? selectThreadMessagesCount(global, userId, threadId!) : undefined;
return { return {
user, user,
@ -340,8 +278,6 @@ export default memo(withGlobal<OwnProps>(
areMessagesLoaded, areMessagesLoaded,
self, self,
isSynced, isSynced,
topic,
messagesCount,
}; };
}, },
)(PrivateChatInfo)); )(PrivateChatInfo));

View File

@ -1,71 +0,0 @@
import {
memo, useEffect, useRef, useSignal, useUnmountCleanup,
} from '../../lib/teact/teact';
import {
type ApiFormattedText,
} from '../../api/types';
import useDerivedState from '../../hooks/useDerivedState';
import useLastCallback from '../../hooks/useLastCallback';
type OwnProps = {
text: ApiFormattedText;
duration?: number;
children: (text: ApiFormattedText) => React.ReactNode;
};
const DEFAULT_HEADWAY_DURATION = 1000;
const MIN_TIMEOUT_DURATION = 1000 / 60; // 60 FPS
const MAX_SYMBOLS_BATCH = 10;
const TypingWrapper = ({
text,
duration = DEFAULT_HEADWAY_DURATION,
children,
}: OwnProps) => {
const [getCurrentTextLength, setCurrentTextLength] = useSignal(text.text.length);
const intervalRef = useRef<number>();
const animate = useLastCallback(() => {
const msPerSymbol = duration / text.text.length;
const timeoutDuration = Math.max(msPerSymbol, MIN_TIMEOUT_DURATION);
const nextSymbolBatchLength = Math.min(Math.ceil(timeoutDuration / msPerSymbol), MAX_SYMBOLS_BATCH);
intervalRef.current = window.setTimeout(() => {
if (getCurrentTextLength() >= text.text.length) {
clearTimeout(intervalRef.current);
return;
}
setCurrentTextLength(getCurrentTextLength() + nextSymbolBatchLength);
}, timeoutDuration);
});
useEffect(() => {
// Text got shorter, skip animation
if (text.text.length < getCurrentTextLength()) {
clearTimeout(intervalRef.current);
setCurrentTextLength(text.text.length);
return;
}
clearTimeout(intervalRef.current);
animate();
}, [getCurrentTextLength, setCurrentTextLength, text.text.length]);
useUnmountCleanup(() => {
clearTimeout(intervalRef.current);
});
const displayedText = useDerivedState(() => {
return {
...text,
text: text.text.slice(0, getCurrentTextLength()),
};
}, [getCurrentTextLength, text]);
return children(displayedText);
};
export default memo(TypingWrapper);

View File

@ -1,4 +1,5 @@
import type { ElementRef } from '../../../lib/teact/teact'; import type { ElementRef } from '../../../lib/teact/teact';
import type React from '../../../lib/teact/teact';
import { getActions } from '../../../global'; import { getActions } from '../../../global';
import type { ApiFormattedText, ApiMessageEntity } from '../../../api/types'; import type { ApiFormattedText, ApiMessageEntity } from '../../../api/types';
@ -9,6 +10,7 @@ import { ApiMessageEntityTypes } from '../../../api/types';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { copyTextToClipboard } from '../../../util/clipboard'; import { copyTextToClipboard } from '../../../util/clipboard';
import { oldTranslate } from '../../../util/oldLangProvider';
import { buildCustomEmojiHtmlFromEntity } from '../../middle/composer/helpers/customEmoji'; import { buildCustomEmojiHtmlFromEntity } from '../../middle/composer/helpers/customEmoji';
import renderText from './renderText'; import renderText from './renderText';
@ -299,12 +301,6 @@ function renderMessagePart({
return renderText(content, filters, params); return renderText(content, filters, params);
} }
export function insertTextEntities(entities: ApiMessageEntity[], newEntities: ApiMessageEntity[]) {
return newEntities.reduce((acc, newEntity) => {
return insertTextEntity(acc, newEntity);
}, entities);
}
export function insertTextEntity(entities: ApiMessageEntity[], newEntity: ApiMessageEntity) { export function insertTextEntity(entities: ApiMessageEntity[], newEntity: ApiMessageEntity) {
const resultEntities: ApiMessageEntity[] = []; const resultEntities: ApiMessageEntity[] = [];
@ -765,9 +761,7 @@ function handleHashtagClick(hashtag?: string, username?: string) {
function handleCodeClick(e: React.MouseEvent<HTMLElement>) { function handleCodeClick(e: React.MouseEvent<HTMLElement>) {
copyTextToClipboard(e.currentTarget.innerText); copyTextToClipboard(e.currentTarget.innerText);
getActions().showNotification({ getActions().showNotification({
message: { message: oldTranslate('TextCopied'),
key: 'TextCopied',
},
}); });
} }

View File

@ -23,7 +23,7 @@ import Button from '../ui/Button';
import DropdownMenu from '../ui/DropdownMenu'; import DropdownMenu from '../ui/DropdownMenu';
import MenuItem from '../ui/MenuItem'; import MenuItem from '../ui/MenuItem';
import ChatList from './main/ChatList'; import ChatList from './main/ChatList';
import ForumPanel from './main/forum/ForumPanel'; import ForumPanel from './main/ForumPanel';
import './ArchivedChats.scss'; import './ArchivedChats.scss';

View File

@ -103,7 +103,7 @@
font-size: 0.875rem !important; font-size: 0.875rem !important;
} }
.selected:not(.onAvatar) { .selected {
.badge:not(.pinned) { .badge:not(.pinned) {
color: var(--color-chat-active); color: var(--color-chat-active);
background: var(--color-white); background: var(--color-white);

View File

@ -1,20 +1,19 @@
import { memo } from '../../../../lib/teact/teact'; import type { FC } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../../global'; import { memo, useCallback } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type { ApiSticker } from '../../../../api/types'; import type { ApiSticker } from '../../../api/types';
import { getHasAdminRight } from '../../../../global/helpers'; import { getHasAdminRight } from '../../../global/helpers';
import { selectAnimatedEmoji, selectChat } from '../../../../global/selectors'; import { selectAnimatedEmoji, selectChat } from '../../../global/selectors';
import buildClassName from '../../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { REM } from '../../../common/helpers/mediaDimensions'; import { REM } from '../../common/helpers/mediaDimensions';
import useAppLayout from '../../../../hooks/useAppLayout'; import useAppLayout from '../../../hooks/useAppLayout';
import useLang from '../../../../hooks/useLang'; import useOldLang from '../../../hooks/useOldLang';
import useLastCallback from '../../../../hooks/useLastCallback';
import useOldLang from '../../../../hooks/useOldLang';
import AnimatedIconFromSticker from '../../../common/AnimatedIconFromSticker'; import AnimatedIconFromSticker from '../../common/AnimatedIconFromSticker';
import Button from '../../../ui/Button'; import Button from '../../ui/Button';
import styles from './EmptyForum.module.scss'; import styles from './EmptyForum.module.scss';
@ -29,27 +28,26 @@ type StateProps = {
const ICON_SIZE = 7 * REM; const ICON_SIZE = 7 * REM;
const EmptyForum = ({ const EmptyForum: FC<OwnProps & StateProps> = ({
chatId, animatedEmoji, canManageTopics, chatId, animatedEmoji, canManageTopics,
}: OwnProps & StateProps) => { }) => {
const { openCreateTopicPanel } = getActions(); const { openCreateTopicPanel } = getActions();
const lang = useLang(); const lang = useOldLang();
const oldLang = useOldLang();
const { isMobile } = useAppLayout(); const { isMobile } = useAppLayout();
const handleCreateTopic = useLastCallback(() => { const handleCreateTopic = useCallback(() => {
openCreateTopicPanel({ chatId }); openCreateTopicPanel({ chatId });
}); }, [chatId, openCreateTopicPanel]);
return ( return (
<div className={styles.root}> <div className={styles.root}>
<div className={styles.sticker}> <div className={styles.sticker}>
{animatedEmoji && <AnimatedIconFromSticker sticker={animatedEmoji} size={ICON_SIZE} />} {animatedEmoji && <AnimatedIconFromSticker sticker={animatedEmoji} size={ICON_SIZE} />}
</div> </div>
<h3 className={styles.title} dir="auto">{oldLang('ChatList.EmptyTopicsTitle')}</h3> <h3 className={styles.title} dir="auto">{lang('ChatList.EmptyTopicsTitle')}</h3>
<p className={buildClassName(styles.description, styles.centered)} dir="auto"> <p className={buildClassName(styles.description, styles.centered)} dir="auto">
{oldLang('ChatList.EmptyTopicsDescription')} {lang('ChatList.EmptyTopicsDescription')}
</p> </p>
{canManageTopics && ( {canManageTopics && (
<Button <Button
@ -59,7 +57,7 @@ const EmptyForum = ({
isRtl={lang.isRtl} isRtl={lang.isRtl}
> >
<div className={styles.buttonText}> <div className={styles.buttonText}>
{oldLang('ChatList.EmptyTopicsCreate')} {lang('ChatList.EmptyTopicsCreate')}
</div> </div>
</Button> </Button>
)} )}

View File

@ -1,18 +1,19 @@
import type { FC } from '../../../lib/teact/teact';
import { import {
beginHeavyAnimation, beginHeavyAnimation,
memo, useEffect, useMemo, useRef, useState, memo, useEffect, useMemo, useRef, useState,
} from '../../../../lib/teact/teact'; } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../../global'; import { getActions, withGlobal } from '../../../global';
import type { ApiChat } from '../../../../api/types'; import type { ApiChat } from '../../../api/types';
import type { TopicsInfo } from '../../../../types'; import type { TopicsInfo } from '../../../types';
import { MAIN_THREAD_ID } from '../../../../api/types'; import { MAIN_THREAD_ID } from '../../../api/types';
import { import {
GENERAL_TOPIC_ID, TOPIC_HEIGHT_PX, TOPIC_LIST_SENSITIVE_AREA, TOPICS_SLICE, GENERAL_TOPIC_ID, TOPIC_HEIGHT_PX, TOPIC_LIST_SENSITIVE_AREA, TOPICS_SLICE,
} from '../../../../config'; } from '../../../config';
import { requestNextMutation } from '../../../../lib/fasterdom/fasterdom'; import { requestNextMutation } from '../../../lib/fasterdom/fasterdom';
import { getOrderedTopics } from '../../../../global/helpers'; import { getOrderedTopics } from '../../../global/helpers';
import { import {
selectCanAnimateInterface, selectCanAnimateInterface,
selectChat, selectChat,
@ -20,32 +21,29 @@ import {
selectIsForumPanelOpen, selectIsForumPanelOpen,
selectTabState, selectTabState,
selectTopicsInfo, selectTopicsInfo,
} from '../../../../global/selectors'; } from '../../../global/selectors';
import { IS_TOUCH_ENV } from '../../../../util/browser/windowEnvironment'; import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment';
import buildClassName from '../../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import captureEscKeyListener from '../../../../util/captureEscKeyListener'; import captureEscKeyListener from '../../../util/captureEscKeyListener';
import { captureEvents, SwipeDirection } from '../../../../util/captureEvents'; import { captureEvents, SwipeDirection } from '../../../util/captureEvents';
import { waitForTransitionEnd } from '../../../../util/cssAnimationEndListeners'; import { waitForTransitionEnd } from '../../../util/cssAnimationEndListeners';
import { isUserId } from '../../../../util/entities/ids';
import useAppLayout from '../../../../hooks/useAppLayout'; import useAppLayout from '../../../hooks/useAppLayout';
import useHistoryBack from '../../../../hooks/useHistoryBack'; import useHistoryBack from '../../../hooks/useHistoryBack';
import useInfiniteScroll from '../../../../hooks/useInfiniteScroll'; import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
import { useIntersectionObserver, useOnIntersect } from '../../../../hooks/useIntersectionObserver'; import { useIntersectionObserver, useOnIntersect } from '../../../hooks/useIntersectionObserver';
import useLang from '../../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback';
import useLastCallback from '../../../../hooks/useLastCallback'; import useOldLang from '../../../hooks/useOldLang';
import usePreviousDeprecated from '../../../../hooks/usePreviousDeprecated'; import usePreviousDeprecated from '../../../hooks/usePreviousDeprecated';
import useOrderDiff from '../hooks/useOrderDiff'; import useOrderDiff from './hooks/useOrderDiff';
import GroupCallTopPane from '../../../calls/group/GroupCallTopPane'; import GroupCallTopPane from '../../calls/group/GroupCallTopPane';
import GroupChatInfo from '../../../common/GroupChatInfo'; import GroupChatInfo from '../../common/GroupChatInfo';
import Icon from '../../../common/icons/Icon'; import Icon from '../../common/icons/Icon';
import PrivateChatInfo from '../../../common/PrivateChatInfo'; import HeaderActions from '../../middle/HeaderActions';
import HeaderActions from '../../../middle/HeaderActions'; import Button from '../../ui/Button';
import Button from '../../../ui/Button'; import InfiniteScroll from '../../ui/InfiniteScroll';
import InfiniteScroll from '../../../ui/InfiniteScroll'; import Loading from '../../ui/Loading';
import Loading from '../../../ui/Loading';
import AllMessagesTopic from './AllMessagesTopic';
import EmptyForum from './EmptyForum'; import EmptyForum from './EmptyForum';
import Topic from './Topic'; import Topic from './Topic';
@ -68,17 +66,17 @@ type StateProps = {
const INTERSECTION_THROTTLE = 200; const INTERSECTION_THROTTLE = 200;
const ForumPanel = ({ const ForumPanel: FC<OwnProps & StateProps> = ({
chat, chat,
currentTopicId, currentTopicId,
isOpen, isOpen,
isHidden, isHidden,
topicsInfo, topicsInfo,
withInterfaceAnimations,
onTopicSearch, onTopicSearch,
onCloseAnimationEnd, onCloseAnimationEnd,
onOpenAnimationStart, onOpenAnimationStart,
}: OwnProps & StateProps) => { withInterfaceAnimations,
}) => {
const { const {
closeForumPanel, openChatWithInfo, loadTopics, closeForumPanel, openChatWithInfo, loadTopics,
} = getActions(); } = getActions();
@ -97,7 +95,7 @@ const ForumPanel = ({
}, [topicsInfo, chatId]); }, [topicsInfo, chatId]);
const [isScrolled, setIsScrolled] = useState(false); const [isScrolled, setIsScrolled] = useState(false);
const lang = useLang(); const lang = useOldLang();
const handleClose = useLastCallback(() => { const handleClose = useLastCallback(() => {
closeForumPanel(); closeForumPanel();
@ -124,17 +122,13 @@ const ForumPanel = ({
}); });
const orderedIds = useMemo(() => { const orderedIds = useMemo(() => {
const ids = topicsInfo return topicsInfo
? getOrderedTopics( ? getOrderedTopics(
Object.values(topicsInfo.topicsById), Object.values(topicsInfo.topicsById),
topicsInfo.orderedPinnedTopicIds, topicsInfo.orderedPinnedTopicIds,
).map(({ id }) => id) ).map(({ id }) => id)
: []; : [];
}, [topicsInfo]);
if (!chat?.isBotForum) return ids;
return [MAIN_THREAD_ID, ...ids];
}, [chat?.isBotForum, topicsInfo]);
const { orderDiffById, getAnimationType, onReorderAnimationEnd } = useOrderDiff(orderedIds, chat?.id); const { orderDiffById, getAnimationType, onReorderAnimationEnd } = useOrderDiff(orderedIds, chat?.id);
@ -203,37 +197,23 @@ const ForumPanel = ({
function renderTopics() { function renderTopics() {
const viewportOffset = orderedIds.indexOf(viewportIds![0]); const viewportOffset = orderedIds.indexOf(viewportIds![0]);
return viewportIds?.map((id, i) => { return viewportIds?.map((id, i) => (
if (id === MAIN_THREAD_ID) { <Topic
return ( key={id}
<AllMessagesTopic chatId={chat!.id}
key={id} topic={topicsInfo!.topicsById[id]}
chatId={chat!.id} style={`top: ${(viewportOffset + i) * TOPIC_HEIGHT_PX}px;`}
isSelected={currentTopicId === id} isSelected={currentTopicId === id}
/> observeIntersection={observe}
); animationType={getAnimationType(id)}
} orderDiff={orderDiffById[id]}
onReorderAnimationEnd={onReorderAnimationEnd}
return ( />
<Topic ));
key={id}
chatId={chat!.id}
topic={topicsInfo!.topicsById[id]}
style={`top: ${(viewportOffset + i) * TOPIC_HEIGHT_PX}px;`}
isSelected={currentTopicId === id}
observeIntersection={observe}
animationType={getAnimationType(id)}
orderDiff={orderDiffById[id]}
onReorderAnimationEnd={onReorderAnimationEnd}
/>
);
});
} }
const isLoading = topicsInfo === undefined; const isLoading = topicsInfo === undefined;
if (!chat) return undefined;
return ( return (
<div <div
ref={ref} ref={ref}
@ -256,14 +236,7 @@ const ForumPanel = ({
<Icon name="close" /> <Icon name="close" />
</Button> </Button>
{isUserId(chat.id) ? ( {chat && (
<PrivateChatInfo
noAvatar
className={styles.info}
userId={chat.id}
onClick={handleToggleChatInfo}
/>
) : (
<GroupChatInfo <GroupChatInfo
noAvatar noAvatar
className={styles.info} className={styles.info}
@ -272,18 +245,21 @@ const ForumPanel = ({
/> />
)} )}
<HeaderActions {chat
chatId={chat.id} && (
threadId={MAIN_THREAD_ID} <HeaderActions
messageListType="thread" chatId={chat.id}
canExpandActions={false} threadId={MAIN_THREAD_ID}
isForForum messageListType="thread"
isMobile={isMobile} canExpandActions={false}
onTopicSearch={onTopicSearch} isForForum
/> isMobile={isMobile}
onTopicSearch={onTopicSearch}
/>
)}
</div> </div>
{!isUserId(chat.id) && <GroupCallTopPane chatId={chat.id} />} {chat && <GroupCallTopPane chatId={chat.id} />}
<div className={styles.notch} /> <div className={styles.notch} />

View File

@ -25,7 +25,7 @@ import NewChatButton from '../NewChatButton';
import LeftSearch from '../search/LeftSearch.async'; import LeftSearch from '../search/LeftSearch.async';
import ChatFolders from './ChatFolders'; import ChatFolders from './ChatFolders';
import ContactList from './ContactList.async'; import ContactList from './ContactList.async';
import ForumPanel from './forum/ForumPanel'; import ForumPanel from './ForumPanel';
import LeftMainHeader from './LeftMainHeader'; import LeftMainHeader from './LeftMainHeader';
import './LeftMain.scss'; import './LeftMain.scss';

View File

@ -1,17 +1,17 @@
import type { FC } from '../../../../lib/teact/teact'; import type { FC } from '../../../lib/teact/teact';
import { memo } from '../../../../lib/teact/teact'; import { memo } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../../global'; import { getActions, withGlobal } from '../../../global';
import type { import type {
ApiChat, ApiDraft, ApiMessage, ApiMessageOutgoingStatus, ApiChat, ApiDraft, ApiMessage, ApiMessageOutgoingStatus,
ApiPeer, ApiTopic, ApiTypeStory, ApiTypingStatus, ApiPeer, ApiTopic, ApiTypeStory, ApiTypingStatus,
} from '../../../../api/types'; } from '../../../api/types';
import type { ObserveFn } from '../../../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import type { ChatAnimationTypes } from '../hooks'; import type { ChatAnimationTypes } from './hooks';
import { UNMUTE_TIMESTAMP } from '../../../../config'; import { UNMUTE_TIMESTAMP } from '../../../config';
import { groupStatefulContent } from '../../../../global/helpers'; import { groupStatefulContent } from '../../../global/helpers';
import { getIsChatMuted } from '../../../../global/helpers/notifications'; import { getIsChatMuted } from '../../../global/helpers/notifications';
import { import {
selectCanAnimateInterface, selectCanAnimateInterface,
selectCanDeleteTopic, selectCanDeleteTopic,
@ -27,25 +27,25 @@ import {
selectThreadInfo, selectThreadInfo,
selectThreadParam, selectThreadParam,
selectTopics, selectTopics,
} from '../../../../global/selectors'; } from '../../../global/selectors';
import { IS_OPEN_IN_NEW_TAB_SUPPORTED } from '../../../../util/browser/windowEnvironment'; import { IS_OPEN_IN_NEW_TAB_SUPPORTED } from '../../../util/browser/windowEnvironment';
import buildClassName from '../../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { createLocationHash } from '../../../../util/routing'; import { createLocationHash } from '../../../util/routing';
import renderText from '../../../common/helpers/renderText'; import renderText from '../../common/helpers/renderText';
import useFlag from '../../../../hooks/useFlag'; import useFlag from '../../../hooks/useFlag';
import useLastCallback from '../../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../../hooks/useOldLang'; import useOldLang from '../../../hooks/useOldLang';
import useChatListEntry from '../hooks/useChatListEntry'; import useChatListEntry from './hooks/useChatListEntry';
import useTopicContextActions from '../hooks/useTopicContextActions'; import useTopicContextActions from './hooks/useTopicContextActions';
import Icon from '../../../common/icons/Icon'; import Icon from '../../common/icons/Icon';
import LastMessageMeta from '../../../common/LastMessageMeta'; import LastMessageMeta from '../../common/LastMessageMeta';
import TopicIcon from '../../../common/TopicIcon'; import TopicIcon from '../../common/TopicIcon';
import ConfirmDialog from '../../../ui/ConfirmDialog'; import ConfirmDialog from '../../ui/ConfirmDialog';
import ListItem from '../../../ui/ListItem'; import ListItem from '../../ui/ListItem';
import MuteChatModal from '../../MuteChatModal.async'; import MuteChatModal from '../MuteChatModal.async';
import ChatBadge from '../ChatBadge'; import ChatBadge from './ChatBadge';
import styles from './Topic.module.scss'; import styles from './Topic.module.scss';
@ -165,7 +165,7 @@ const Topic: FC<OwnProps & StateProps> = ({
} }
openThread({ chatId, threadId: topic.id, shouldReplaceHistory: true }); openThread({ chatId, threadId: topic.id, shouldReplaceHistory: true });
if (!chat.isBotForum && !chat.isMonoforum) setViewForumAsMessages({ chatId, isEnabled: false }); setViewForumAsMessages({ chatId, isEnabled: false });
if (canScrollDown) { if (canScrollDown) {
scrollMessageListToBottom(); scrollMessageListToBottom();
@ -263,7 +263,7 @@ export default memo(withGlobal<OwnProps>(
const typingStatus = selectThreadParam(global, chatId, topic.id, 'typingStatus'); const typingStatus = selectThreadParam(global, chatId, topic.id, 'typingStatus');
const draft = selectDraft(global, chatId, topic.id); const draft = selectDraft(global, chatId, topic.id);
const threadInfo = selectThreadInfo(global, chatId, topic.id); const threadInfo = selectThreadInfo(global, chatId, topic.id);
const wasTopicOpened = chat?.isBotForum || Boolean(threadInfo?.lastReadInboxMessageId); const wasTopicOpened = Boolean(threadInfo?.lastReadInboxMessageId);
const topics = selectTopics(global, chatId); const topics = selectTopics(global, chatId);
const { chatId: currentChatId, threadId: currentThreadId } = selectCurrentMessageList(global) || {}; const { chatId: currentChatId, threadId: currentThreadId } = selectCurrentMessageList(global) || {};

View File

@ -1,87 +0,0 @@
import { memo } from '@teact';
import { getActions, withGlobal } from '../../../../global';
import { type ApiMessage, MAIN_THREAD_ID } from '../../../../api/types';
import { selectChatLastMessage } from '../../../../global/selectors';
import { IS_OPEN_IN_NEW_TAB_SUPPORTED } from '../../../../util/browser/windowEnvironment';
import buildClassName from '../../../../util/buildClassName';
import { createLocationHash } from '../../../../util/routing';
import useLang from '../../../../hooks/useLang';
import useLastCallback from '../../../../hooks/useLastCallback';
import LastMessageMeta from '../../../common/LastMessageMeta';
import ListItem from '../../../ui/ListItem';
import styles from './Topic.module.scss';
type OwnProps = {
chatId: string;
isSelected: boolean;
style?: string;
};
type StateProps = {
lastMessage?: ApiMessage;
};
const AllMessagesTopic = ({
chatId, isSelected, style, lastMessage,
}: OwnProps & StateProps) => {
const { openThread, openQuickPreview } = getActions();
const lang = useLang();
const handleOpenTopic = useLastCallback((e: React.MouseEvent) => {
if (e.altKey) {
e.preventDefault();
openQuickPreview({ id: chatId });
return;
}
openThread({ chatId, threadId: MAIN_THREAD_ID, shouldReplaceHistory: true });
});
return (
<ListItem
className={buildClassName(
styles.root,
'Chat',
isSelected && 'selected',
'chat-item-clickable',
)}
onClick={handleOpenTopic}
style={style}
href={IS_OPEN_IN_NEW_TAB_SUPPORTED ? `#${createLocationHash(chatId, 'thread', MAIN_THREAD_ID)}` : undefined}
>
<div className="info">
<div className="info-row">
<div className={buildClassName('title')}>
<h3 dir="auto" className="fullName">{lang('BotForumAllTopicTitle')}</h3>
</div>
<div className="separator" />
{lastMessage && (
<LastMessageMeta
message={lastMessage}
/>
)}
</div>
<div className="subtitle">
<span className="last-message">
{lang('BotForumAllTopicDescription')}
</span>
</div>
</div>
</ListItem>
);
};
export default memo(withGlobal<OwnProps>(
(global, { chatId }): Complete<StateProps> => {
const lastMessage = selectChatLastMessage(global, chatId, 'all');
return {
lastMessage,
};
},
)(AllMessagesTopic));

View File

@ -5,7 +5,6 @@ import type { ApiChat, ApiTopic } from '../../../../api/types';
import type { MenuItemContextAction } from '../../../ui/ListItem'; import type { MenuItemContextAction } from '../../../ui/ListItem';
import { getCanManageTopic, getHasAdminRight } from '../../../../global/helpers'; import { getCanManageTopic, getHasAdminRight } from '../../../../global/helpers';
import { IS_TAURI } from '../../../../util/browser/globalEnvironment';
import { IS_OPEN_IN_NEW_TAB_SUPPORTED } from '../../../../util/browser/windowEnvironment'; import { IS_OPEN_IN_NEW_TAB_SUPPORTED } from '../../../../util/browser/windowEnvironment';
import { compact } from '../../../../util/iteratees'; import { compact } from '../../../../util/iteratees';
@ -49,11 +48,11 @@ export default function useTopicContextActions({
openQuickPreview, openQuickPreview,
} = getActions(); } = getActions();
const canToggleClosed = getCanManageTopic(chat, topic) && !chat.isBotForum; const canToggleClosed = getCanManageTopic(chat, topic);
const canTogglePinned = chat.isCreator || getHasAdminRight(chat, 'manageTopics'); const canTogglePinned = chat.isCreator || getHasAdminRight(chat, 'manageTopics');
const actionOpenInNewTab = IS_OPEN_IN_NEW_TAB_SUPPORTED && { const actionOpenInNewTab = IS_OPEN_IN_NEW_TAB_SUPPORTED && {
title: IS_TAURI ? lang('ChatListOpenInNewWindow') : lang('ChatListOpenInNewTab'), title: 'Open in new tab',
icon: 'open-in-new-tab', icon: 'open-in-new-tab',
handler: () => { handler: () => {
openChatInNewTab({ chatId: chat.id, threadId: topicId }); openChatInNewTab({ chatId: chat.id, threadId: topicId });

View File

@ -863,8 +863,7 @@ export default memo(withGlobal<OwnProps>(
const canGift = selectCanGift(global, chatId); const canGift = selectCanGift(global, chatId);
const topic = selectTopic(global, chatId, threadId); const topic = selectTopic(global, chatId, threadId);
// Disable manual creation for bot forums const canCreateTopic = chat.isForum && (
const canCreateTopic = chat.isForum && !chat.isBotForum && (
chat.isCreator || !isUserRightBanned(chat, 'manageTopics') || getHasAdminRight(chat, 'manageTopics') chat.isCreator || !isUserRightBanned(chat, 'manageTopics') || getHasAdminRight(chat, 'manageTopics')
); );
const canEditTopic = topic && getCanManageTopic(chat, topic); const canEditTopic = topic && getCanManageTopic(chat, topic);

View File

@ -1,8 +1,6 @@
.MessageList { .MessageList {
--action-message-bg: var(--pattern-color); --action-message-bg: var(--pattern-color);
scroll-snap-type: y proximity;
overflow-x: hidden; overflow-x: hidden;
overflow-y: scroll; overflow-y: scroll;
flex: 1; flex: 1;
@ -38,10 +36,6 @@
display: none; display: none;
} }
&.no-bottom-snap {
scroll-snap-type: none;
}
.messages-container { .messages-container {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -57,10 +51,6 @@
margin-top: 100vh !important; margin-top: 100vh !important;
} }
.fab-trigger {
scroll-snap-align: end;
}
@media (max-width: 600px) { @media (max-width: 600px) {
width: 100vw; width: 100vw;
// Patch for an issue on Android when rotating device // Patch for an issue on Android when rotating device

View File

@ -14,7 +14,7 @@ import {
MESSAGE_LIST_SLICE, MESSAGE_LIST_SLICE,
SERVICE_NOTIFICATIONS_USER_ID, SERVICE_NOTIFICATIONS_USER_ID,
} from '../../config'; } from '../../config';
import { forceMeasure, requestForcedReflow, requestMeasure, requestMutation } from '../../lib/fasterdom/fasterdom'; import { forceMeasure, requestForcedReflow, requestMeasure } from '../../lib/fasterdom/fasterdom';
import { import {
getIsSavedDialog, getIsSavedDialog,
getMessageHtmlId, getMessageHtmlId,
@ -54,7 +54,6 @@ import {
import { selectIsChatRestricted } from '../../global/selectors/chats'; import { selectIsChatRestricted } from '../../global/selectors/chats';
import { selectActiveRestrictionReasons, selectCurrentMessageList } from '../../global/selectors/messages'; import { selectActiveRestrictionReasons, selectCurrentMessageList } from '../../global/selectors/messages';
import animateScroll, { isAnimatingScroll, restartCurrentScrollAnimation } from '../../util/animateScroll'; import animateScroll, { isAnimatingScroll, restartCurrentScrollAnimation } from '../../util/animateScroll';
import { IS_FIREFOX } from '../../util/browser/windowEnvironment';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import { isUserId } from '../../util/entities/ids'; import { isUserId } from '../../util/entities/ids';
import { orderBy } from '../../util/iteratees'; import { orderBy } from '../../util/iteratees';
@ -146,7 +145,6 @@ type StateProps = {
translationLanguage?: string; translationLanguage?: string;
shouldAutoTranslate?: boolean; shouldAutoTranslate?: boolean;
isActive?: boolean; isActive?: boolean;
isBotForum?: boolean;
shouldScrollToBottom?: boolean; shouldScrollToBottom?: boolean;
}; };
@ -168,19 +166,13 @@ const MESSAGE_REACTIONS_POLLING_INTERVAL = 20 * 1000;
const MESSAGE_COMMENTS_POLLING_INTERVAL = 20 * 1000; const MESSAGE_COMMENTS_POLLING_INTERVAL = 20 * 1000;
const MESSAGE_FACT_CHECK_UPDATE_INTERVAL = 5 * 1000; const MESSAGE_FACT_CHECK_UPDATE_INTERVAL = 5 * 1000;
const MESSAGE_STORY_POLLING_INTERVAL = 5 * 60 * 1000; const MESSAGE_STORY_POLLING_INTERVAL = 5 * 60 * 1000;
const BOTTOM_THRESHOLD = 50; const BOTTOM_THRESHOLD = 50;
const BOTTOM_SNAP_THRESHOLD = 10;
const UNREAD_DIVIDER_TOP = 10; const UNREAD_DIVIDER_TOP = 10;
const SCROLL_DEBOUNCE = 200; const SCROLL_DEBOUNCE = 200;
const MESSAGE_ANIMATION_DURATION = 500; const MESSAGE_ANIMATION_DURATION = 500;
const BOTTOM_FOCUS_MARGIN = 0.5 * REM; const BOTTOM_FOCUS_MARGIN = 0.5 * REM;
const SELECT_MODE_ANIMATION_DURATION = 200; const SELECT_MODE_ANIMATION_DURATION = 200;
const UNREAD_DIVIDER_CLASS = 'unread-divider'; const UNREAD_DIVIDER_CLASS = 'unread-divider';
const FORCE_MESSAGES_SCROLL_CLASS = 'force-messages-scroll';
const NO_BOTTOM_SNAP_CLASS = 'no-bottom-snap';
const runDebouncedForScroll = debounce((cb) => cb(), SCROLL_DEBOUNCE, false); const runDebouncedForScroll = debounce((cb) => cb(), SCROLL_DEBOUNCE, false);
@ -196,7 +188,6 @@ const MessageList: FC<OwnProps & StateProps> = ({
canPost, canPost,
isSynced, isSynced,
isActive, isActive,
isBotForum,
shouldScrollToBottom, shouldScrollToBottom,
// eslint-disable-next-line @typescript-eslint/no-shadow // eslint-disable-next-line @typescript-eslint/no-shadow
isChatMonoforum, isChatMonoforum,
@ -267,7 +258,6 @@ const MessageList: FC<OwnProps & StateProps> = ({
const memoFocusingIdRef = useRef<number>(); const memoFocusingIdRef = useRef<number>();
const isScrollTopJustUpdatedRef = useRef(false); const isScrollTopJustUpdatedRef = useRef(false);
const shouldAnimateAppearanceRef = useRef(Boolean(lastMessage)); const shouldAnimateAppearanceRef = useRef(Boolean(lastMessage));
const scrollSnapDisabledTimerRef = useRef<number>();
const isSavedDialog = getIsSavedDialog(chatId, threadId, currentUserId); const isSavedDialog = getIsSavedDialog(chatId, threadId, currentUserId);
const hasOpenChatButton = isSavedDialog && threadId !== ANONYMOUS_USER_ID; const hasOpenChatButton = isSavedDialog && threadId !== ANONYMOUS_USER_ID;
@ -508,33 +498,6 @@ const MessageList: FC<OwnProps & StateProps> = ({
const [getContainerHeight, prevContainerHeightRef] = useContainerHeight(containerRef, canPost && !isSelectModeActive); const [getContainerHeight, prevContainerHeightRef] = useContainerHeight(containerRef, canPost && !isSelectModeActive);
const handleWheel = useLastCallback((e: React.WheelEvent<HTMLDivElement>) => {
// Firefox is finicky about bottom scroll snapping, so we enable it only when nearing the bottom
// https://bugzilla.mozilla.org/show_bug.cgi?id=1753188
if (!IS_FIREFOX) return;
const container = containerRef.current;
if (!container) return;
const scrollTop = container.scrollTop;
const scrollHeight = container.scrollHeight;
const offsetHeight = container.offsetHeight;
const isNearBottomForSnap = scrollTop >= scrollHeight - offsetHeight - BOTTOM_SNAP_THRESHOLD;
if (!isNearBottomForSnap) return;
if (e.deltaY < 0) {
clearTimeout(scrollSnapDisabledTimerRef.current);
requestMutation(() => {
addExtraClass(container, NO_BOTTOM_SNAP_CLASS);
container.scrollBy(0, -BOTTOM_SNAP_THRESHOLD); // Manually scroll to prevent ignoring first event
});
} else {
requestMutation(() => {
removeExtraClass(container, NO_BOTTOM_SNAP_CLASS);
});
}
});
// Initial message loading // Initial message loading
useEffect(() => { useEffect(() => {
if (!loadMoreAround || !isChatLoaded || isRestricted || focusingId) { if (!loadMoreAround || !isChatLoaded || isRestricted || focusingId) {
@ -628,30 +591,21 @@ const MessageList: FC<OwnProps & StateProps> = ({
isViewportNewest isViewportNewest
&& wasMessageAdded && wasMessageAdded
&& (messageIds && messageIds.length < MESSAGE_LIST_SLICE / 2) && (messageIds && messageIds.length < MESSAGE_LIST_SLICE / 2)
&& !container.parentElement!.classList.contains(FORCE_MESSAGES_SCROLL_CLASS) && !container.parentElement!.classList.contains('force-messages-scroll')
&& forceMeasure(() => ( && forceMeasure(() => (
(container.firstElementChild as HTMLDivElement).clientHeight <= container.offsetHeight * 2 (container.firstElementChild as HTMLDivElement).clientHeight <= container.offsetHeight * 2
)) ))
) { ) {
addExtraClass(container.parentElement!, FORCE_MESSAGES_SCROLL_CLASS); addExtraClass(container.parentElement!, 'force-messages-scroll');
container.parentElement!.classList.add('force-messages-scroll');
setTimeout(() => { setTimeout(() => {
if (container.parentElement) { if (container.parentElement) {
removeExtraClass(container.parentElement, FORCE_MESSAGES_SCROLL_CLASS); removeExtraClass(container.parentElement, 'force-messages-scroll');
} }
}, MESSAGE_ANIMATION_DURATION); }, MESSAGE_ANIMATION_DURATION);
} }
if (wasMessageAdded) {
clearTimeout(scrollSnapDisabledTimerRef.current);
addExtraClass(container, NO_BOTTOM_SNAP_CLASS);
scrollSnapDisabledTimerRef.current = window.setTimeout(() => {
removeExtraClass(container, NO_BOTTOM_SNAP_CLASS);
}, MESSAGE_ANIMATION_DURATION);
}
requestForcedReflow(() => { requestForcedReflow(() => {
const { scrollTop, scrollHeight, offsetHeight } = container; const { scrollTop, scrollHeight, offsetHeight } = container;
const scrollOffset = scrollOffsetRef.current; const scrollOffset = scrollOffsetRef.current;
@ -768,7 +722,6 @@ const MessageList: FC<OwnProps & StateProps> = ({
!isReady && 'is-animating', !isReady && 'is-animating',
hasOpenChatButton && 'saved-dialog', hasOpenChatButton && 'saved-dialog',
isChatProtected && 'hide-on-print', isChatProtected && 'hide-on-print',
IS_FIREFOX && NO_BOTTOM_SNAP_CLASS,
); );
const hasMessages = Boolean((messageIds && messageGroups) || lastMessage); const hasMessages = Boolean((messageIds && messageGroups) || lastMessage);
@ -851,7 +804,6 @@ const MessageList: FC<OwnProps & StateProps> = ({
noAppearanceAnimation={!messageGroups || !shouldAnimateAppearanceRef.current} noAppearanceAnimation={!messageGroups || !shouldAnimateAppearanceRef.current}
isQuickPreview={isQuickPreview} isQuickPreview={isQuickPreview}
canPost={canPost} canPost={canPost}
isBotForum={isBotForum}
shouldScrollToBottom={shouldScrollToBottom} shouldScrollToBottom={shouldScrollToBottom}
onScrollDownToggle={onScrollDownToggle} onScrollDownToggle={onScrollDownToggle}
onNotchToggle={onNotchToggle} onNotchToggle={onNotchToggle}
@ -870,7 +822,6 @@ const MessageList: FC<OwnProps & StateProps> = ({
activeKey={activeKey} activeKey={activeKey}
shouldCleanup shouldCleanup
onScroll={handleScroll} onScroll={handleScroll}
onWheel={handleWheel}
onMouseDown={preventMessageInputBlur} onMouseDown={preventMessageInputBlur}
> >
{renderContent()} {renderContent()}
@ -986,7 +937,6 @@ export default memo(withGlobal<OwnProps>(
canTranslate, canTranslate,
translationLanguage, translationLanguage,
shouldAutoTranslate, shouldAutoTranslate,
isBotForum: chat.isBotForum,
shouldScrollToBottom, shouldScrollToBottom,
}; };
}, },

View File

@ -37,7 +37,6 @@ import usePreviousDeprecated from '../../hooks/usePreviousDeprecated';
import useMessageObservers from './hooks/useMessageObservers'; import useMessageObservers from './hooks/useMessageObservers';
import useScrollHooks from './hooks/useScrollHooks'; import useScrollHooks from './hooks/useScrollHooks';
import Icon from '../common/icons/Icon';
import MiniTable, { type TableEntry } from '../common/MiniTable'; import MiniTable, { type TableEntry } from '../common/MiniTable';
import ActionMessage from './message/ActionMessage'; import ActionMessage from './message/ActionMessage';
import Message from './message/Message'; import Message from './message/Message';
@ -60,7 +59,6 @@ interface OwnProps {
withUsers: boolean; withUsers: boolean;
isChannelChat: boolean | undefined; isChannelChat: boolean | undefined;
isChatMonoforum?: boolean; isChatMonoforum?: boolean;
isBotForum?: boolean;
isEmptyThread?: boolean; isEmptyThread?: boolean;
isComments?: boolean; isComments?: boolean;
noAvatars: boolean; noAvatars: boolean;
@ -101,7 +99,6 @@ const MessageListContent = ({
withUsers, withUsers,
isChannelChat, isChannelChat,
isChatMonoforum, isChatMonoforum,
isBotForum,
noAvatars, noAvatars,
containerRef, containerRef,
anchorIdRef, anchorIdRef,
@ -246,20 +243,6 @@ const MessageListContent = ({
return undefined; return undefined;
}; };
const renderBotForumTopicAction = () => {
if (!isBotForum || threadId !== MAIN_THREAD_ID) return undefined;
return (
<div className={buildClassName('local-action-message', actionMessageStyles.root)} key="botforum-new-topic">
<div className={actionMessageStyles.contentBox}>
<Icon className={actionMessageStyles.botForumTopicIcon} name="topic-new" />
<h3 className={actionMessageStyles.botForumTopicTitle}>{lang('BotForumActionNew')}</h3>
<span className={actionMessageStyles.botForumTopicDescription}>{lang('BotForumActionNewDescription')}</span>
<Icon className={actionMessageStyles.botForumTopicArrow} name="down" />
</div>
</div>
);
};
const messageCountToAnimate = noAppearanceAnimation ? 0 : messageGroups.reduce((acc, messageGroup) => { const messageCountToAnimate = noAppearanceAnimation ? 0 : messageGroups.reduce((acc, messageGroup) => {
return acc + messageGroup.senderGroups.flat().length; return acc + messageGroup.senderGroups.flat().length;
}, 0); }, 0);
@ -465,7 +448,6 @@ const MessageListContent = ({
{shouldRenderAccountInfo {shouldRenderAccountInfo
&& <MessageListAccountInfo key={`account_info_${chatId}`} chatId={chatId} hasMessages />} && <MessageListAccountInfo key={`account_info_${chatId}`} chatId={chatId} hasMessages />}
{dateGroups.flat()} {dateGroups.flat()}
{isViewportNewest && renderBotForumTopicAction()}
{withHistoryTriggers && ( {withHistoryTriggers && (
<div <div
ref={forwardsTriggerRef} ref={forwardsTriggerRef}

View File

@ -279,12 +279,11 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
<PrivateChatInfo <PrivateChatInfo
key={displayChatId} key={displayChatId}
userId={displayChatId} userId={displayChatId}
threadId={!isSavedDialog ? threadId : undefined}
typingStatus={typingStatus} typingStatus={typingStatus}
status={connectionStatusText || savedMessagesStatus} status={connectionStatusText || savedMessagesStatus}
withDots={Boolean(connectionStatusText)} withDots={Boolean(connectionStatusText)}
withFullInfo={threadId === MAIN_THREAD_ID} withFullInfo
withMediaViewer={threadId === MAIN_THREAD_ID} withMediaViewer
withStory={!isChatWithSelf} withStory={!isChatWithSelf}
withUpdatingStatus withUpdatingStatus
isSavedDialog={isSavedDialog} isSavedDialog={isSavedDialog}

View File

@ -9,8 +9,8 @@ import { selectChatMessage, selectCurrentMessageList } from '../../../global/sel
import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment'; import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment';
import renderKeyboardButtonText from './helpers/renderKeyboardButtonText'; import renderKeyboardButtonText from './helpers/renderKeyboardButtonText';
import useLang from '../../../hooks/useLang';
import useMouseInside from '../../../hooks/useMouseInside'; import useMouseInside from '../../../hooks/useMouseInside';
import useOldLang from '../../../hooks/useOldLang';
import Button from '../../ui/Button'; import Button from '../../ui/Button';
import Menu from '../../ui/Menu'; import Menu from '../../ui/Menu';
@ -33,7 +33,7 @@ const BotKeyboardMenu: FC<OwnProps & StateProps> = ({
}) => { }) => {
const { clickBotInlineButton } = getActions(); const { clickBotInlineButton } = getActions();
const lang = useLang(); const lang = useOldLang();
const [handleMouseEnter, handleMouseLeave] = useMouseInside(isOpen, onClose); const [handleMouseEnter, handleMouseLeave] = useMouseInside(isOpen, onClose);
const { isKeyboardSingleUse } = message || {}; const { isKeyboardSingleUse } = message || {};

View File

@ -7,9 +7,11 @@ import { STARS_ICON_PLACEHOLDER } from '../../../../config';
import { replaceWithTeact } from '../../../../util/replaceWithTeact'; import { replaceWithTeact } from '../../../../util/replaceWithTeact';
import renderText from '../../../common/helpers/renderText'; import renderText from '../../../common/helpers/renderText';
import { type OldLangFn } from '../../../../hooks/useOldLang';
import Icon from '../../../common/icons/Icon'; import Icon from '../../../common/icons/Icon';
export default function renderKeyboardButtonText(lang: LangFn, button: ApiKeyboardButton): TeactNode { export default function renderKeyboardButtonText(lang: OldLangFn | LangFn, button: ApiKeyboardButton): TeactNode {
if (button.type === 'receipt') { if (button.type === 'receipt') {
return lang('PaymentReceipt'); return lang('PaymentReceipt');
} }

View File

@ -83,7 +83,7 @@ export default function useMessageObservers(
}); });
if (!isQuickPreview) { if (!isQuickPreview) {
if (memoFirstUnreadIdRef.current && maxId && maxId >= memoFirstUnreadIdRef.current) { if (memoFirstUnreadIdRef.current && maxId >= memoFirstUnreadIdRef.current) {
markMessageListRead({ maxId }); markMessageListRead({ maxId });
} }

View File

@ -283,23 +283,3 @@
font-size: 1rem; font-size: 1rem;
vertical-align: middle; vertical-align: middle;
} }
.botForumTopicIcon {
padding: 1.25rem;
border-radius: 50%;
font-size: 2.5rem;
background-color: var(--action-message-bg);
}
.botForumTopicTitle {
margin-block: 0.5rem 0;
}
.botForumTopicDescription {
font-weight: var(--font-weight-normal);
}
.botForumTopicArrow {
font-size: 1.5rem;
opacity: 0.5;
}

View File

@ -40,6 +40,7 @@ import useEnsureMessage from '../../../hooks/useEnsureMessage';
import useFlag from '../../../hooks/useFlag'; import useFlag from '../../../hooks/useFlag';
import { type ObserveFn, useOnIntersect } from '../../../hooks/useIntersectionObserver'; import { type ObserveFn, useOnIntersect } from '../../../hooks/useIntersectionObserver';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
import useMessageResizeObserver from '../../../hooks/useResizeMessageObserver';
import useShowTransition from '../../../hooks/useShowTransition'; import useShowTransition from '../../../hooks/useShowTransition';
import { type OnIntersectPinnedMessage } from '../hooks/usePinnedMessage'; import { type OnIntersectPinnedMessage } from '../hooks/usePinnedMessage';
import useFluidBackgroundFilter from './hooks/useFluidBackgroundFilter'; import useFluidBackgroundFilter from './hooks/useFluidBackgroundFilter';
@ -124,11 +125,11 @@ const ActionMessage = ({
hasUnreadReaction, hasUnreadReaction,
isResizingContainer, isResizingContainer,
scrollTargetPosition, scrollTargetPosition,
isAccountFrozen,
onIntersectPinnedMessage, onIntersectPinnedMessage,
observeIntersectionForBottom, observeIntersectionForBottom,
observeIntersectionForLoading, observeIntersectionForLoading,
observeIntersectionForPlaying, observeIntersectionForPlaying,
isAccountFrozen,
}: OwnProps & StateProps) => { }: OwnProps & StateProps) => {
const { const {
requestConfetti, requestConfetti,
@ -167,6 +168,8 @@ const ActionMessage = ({
useOnIntersect(ref, !shouldSkipRender ? observeIntersectionForBottom : undefined); useOnIntersect(ref, !shouldSkipRender ? observeIntersectionForBottom : undefined);
useMessageResizeObserver(ref, !shouldSkipRender && isLastInList && action.type !== 'channelJoined');
useEnsureMessage( useEnsureMessage(
replyToPeerId || chatId, replyToPeerId || chatId,
replyToMsgId, replyToMsgId,

View File

@ -40,7 +40,6 @@ import {
getPinnedMediaValue, getPinnedMediaValue,
renderMessageLink, renderMessageLink,
renderPeerLink, renderPeerLink,
renderTopicLink,
translateWithYou, translateWithYou,
} from './helpers/messageActions'; } from './helpers/messageActions';
@ -82,6 +81,7 @@ const ActionMessageText = ({
asPreview, asPreview,
}: OwnProps & StateProps) => { }: OwnProps & StateProps) => {
const { const {
openThread,
openTelegramLink, openTelegramLink,
openUrl, openUrl,
} = getActions(); } = getActions();
@ -231,15 +231,18 @@ const ActionMessageText = ({
const topicId = selectThreadIdFromMessage(global, message); const topicId = selectThreadIdFromMessage(global, message);
const topicLinkContent = ( const topicLink = (
<> <Link
className={styles.topicLink}
onClick={() => openThread({ chatId, threadId: topicId })}
>
{iconEmojiId ? <CustomEmoji documentId={iconEmojiId} isSelectable /> {iconEmojiId ? <CustomEmoji documentId={iconEmojiId} isSelectable />
: <TopicDefaultIcon topicId={topicId} title={title} iconColor={iconColor} />} : <TopicDefaultIcon topicId={topicId} title={title} iconColor={iconColor} />}
{NBSP} {NBSP}
{renderText(title)} {renderText(title)}
</> </Link>
); );
const topicLink = renderTopicLink(chatId, Number(topicId), topicLinkContent, asPreview);
return lang('ActionTopicCreated', { topic: topicLink }, { withNodes: true }); return lang('ActionTopicCreated', { topic: topicLink }, { withNodes: true });
} }
@ -250,8 +253,12 @@ const ActionMessageText = ({
const topicId = selectThreadIdFromMessage(global, message); const topicId = selectThreadIdFromMessage(global, message);
const currentTopic = selectTopic(global, chatId, topicId); const currentTopic = selectTopic(global, chatId, topicId);
const topicLinkContent = ( const topicLink = (
<> <Link
className={styles.topicLink}
onClick={() => openThread({ chatId, threadId: topicId })}
>
{iconEmojiId && iconEmojiId !== DEFAULT_TOPIC_ICON_ID {iconEmojiId && iconEmojiId !== DEFAULT_TOPIC_ICON_ID
? <CustomEmoji documentId={iconEmojiId} isSelectable /> ? <CustomEmoji documentId={iconEmojiId} isSelectable />
: ( : (
@ -263,12 +270,17 @@ const ActionMessageText = ({
)} )}
{topicId !== GENERAL_TOPIC_ID && NBSP} {topicId !== GENERAL_TOPIC_ID && NBSP}
{renderText(title || currentTopic?.title || lang('ActionTopicPlaceholder'))} {renderText(title || currentTopic?.title || lang('ActionTopicPlaceholder'))}
</> </Link>
); );
const topicLink = renderTopicLink(chatId, Number(topicId), topicLinkContent, asPreview);
const topicPlaceholderLink = renderTopicLink( const topicPlaceholderLink = (
chatId, Number(topicId), lang('ActionTopicPlaceholder'), asPreview, <Link
className={styles.topicLink}
onClick={() => openThread({ chatId, threadId: topicId })}
>
{lang('ActionTopicPlaceholder')}
</Link>
); );
if (isClosed !== undefined) { if (isClosed !== undefined) {

View File

@ -1,12 +1,13 @@
import type { TeactNode } from '../../../lib/teact/teact'; import type { FC, TeactNode } from '../../../lib/teact/teact';
import { memo, useMemo } from '../../../lib/teact/teact'; import { memo, useMemo } from '../../../lib/teact/teact';
import type { ApiKeyboardButton } from '../../../api/types'; import type { ApiKeyboardButton, ApiMessage } from '../../../api/types';
import type { ActionPayloads } from '../../../global/types';
import { RE_TME_LINK, TME_LINK_PREFIX } from '../../../config'; import { RE_TME_LINK, TME_LINK_PREFIX } from '../../../config';
import renderKeyboardButtonText from '../composer/helpers/renderKeyboardButtonText'; import renderKeyboardButtonText from '../composer/helpers/renderKeyboardButtonText';
import useLang from '../../../hooks/useLang'; import useOldLang from '../../../hooks/useOldLang';
import Icon from '../../common/icons/Icon'; import Icon from '../../common/icons/Icon';
import Button from '../../ui/Button'; import Button from '../../ui/Button';
@ -14,12 +15,12 @@ import Button from '../../ui/Button';
import './InlineButtons.scss'; import './InlineButtons.scss';
type OwnProps = { type OwnProps = {
inlineButtons: ApiKeyboardButton[][]; message: ApiMessage;
onClick: (payload: ApiKeyboardButton) => void; onClick: (payload: ActionPayloads['clickBotInlineButton']) => void;
}; };
const InlineButtons = ({ inlineButtons, onClick }: OwnProps) => { const InlineButtons: FC<OwnProps> = ({ message, onClick }) => {
const lang = useLang(); const lang = useOldLang();
const renderIcon = (button: ApiKeyboardButton) => { const renderIcon = (button: ApiKeyboardButton) => {
const { type } = button; const { type } = button;
@ -65,15 +66,15 @@ const InlineButtons = ({ inlineButtons, onClick }: OwnProps) => {
const buttonTexts = useMemo(() => { const buttonTexts = useMemo(() => {
const texts: TeactNode[][] = []; const texts: TeactNode[][] = [];
inlineButtons.forEach((row) => { message.inlineButtons!.forEach((row) => {
texts.push(row.map((button) => renderKeyboardButtonText(lang, button))); texts.push(row.map((button) => renderKeyboardButtonText(lang, button)));
}); });
return texts; return texts;
}, [lang, inlineButtons]); }, [lang, message.inlineButtons]);
return ( return (
<div className="InlineButtons"> <div className="InlineButtons">
{inlineButtons.map((row, i) => ( {message.inlineButtons!.map((row, i) => (
<div className="row"> <div className="row">
{row.map((button, j) => ( {row.map((button, j) => (
<Button <Button
@ -81,7 +82,7 @@ const InlineButtons = ({ inlineButtons, onClick }: OwnProps) => {
ripple ripple
disabled={button.type === 'unsupported' || (button.type === 'suggestedMessage' && button.disabled)} disabled={button.type === 'unsupported' || (button.type === 'suggestedMessage' && button.disabled)}
onClick={() => onClick(button)} onClick={() => onClick({ chatId: message.chatId, messageId: message.id, button })}
> >
{renderIcon(button)} {renderIcon(button)}
<span className="inline-button-text"> <span className="inline-button-text">

View File

@ -14,7 +14,6 @@ import type {
ApiAvailableReaction, ApiAvailableReaction,
ApiChat, ApiChat,
ApiChatMember, ApiChatMember,
ApiKeyboardButton,
ApiMessage, ApiMessage,
ApiMessageOutgoingStatus, ApiMessageOutgoingStatus,
ApiPeer, ApiPeer,
@ -28,6 +27,7 @@ import type {
ApiUser, ApiUser,
ApiWebPage, ApiWebPage,
} from '../../../api/types'; } from '../../../api/types';
import type { ActionPayloads } from '../../../global/types';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import type { import type {
ActiveEmojiInteraction, ActiveEmojiInteraction,
@ -150,6 +150,7 @@ import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang'; import useOldLang from '../../../hooks/useOldLang';
import usePeerColor from '../../../hooks/usePeerColor'; import usePeerColor from '../../../hooks/usePeerColor';
import usePreviousDeprecated from '../../../hooks/usePreviousDeprecated'; import usePreviousDeprecated from '../../../hooks/usePreviousDeprecated';
import useMessageResizeObserver from '../../../hooks/useResizeMessageObserver';
import useShowTransition from '../../../hooks/useShowTransition'; import useShowTransition from '../../../hooks/useShowTransition';
import useTextLanguage from '../../../hooks/useTextLanguage'; import useTextLanguage from '../../../hooks/useTextLanguage';
import useDetectChatLanguage from './hooks/useDetectChatLanguage'; import useDetectChatLanguage from './hooks/useDetectChatLanguage';
@ -214,25 +215,27 @@ type MessagePositionProperties = {
isLastInList: boolean; isLastInList: boolean;
}; };
type OwnProps = { type OwnProps =
message: ApiMessage; {
album?: IAlbum; message: ApiMessage;
noAvatars?: boolean; album?: IAlbum;
withAvatar?: boolean; noAvatars?: boolean;
withSenderName?: boolean; withAvatar?: boolean;
threadId: ThreadId; withSenderName?: boolean;
messageListType: MessageListType; threadId: ThreadId;
noComments: boolean; messageListType: MessageListType;
noReplies: boolean; noComments: boolean;
appearanceOrder: number; noReplies: boolean;
isJustAdded: boolean; appearanceOrder: number;
memoFirstUnreadIdRef?: { current: number | undefined }; isJustAdded: boolean;
getIsMessageListReady?: Signal<boolean>; memoFirstUnreadIdRef?: { current: number | undefined };
observeIntersectionForBottom?: ObserveFn; getIsMessageListReady?: Signal<boolean>;
observeIntersectionForLoading?: ObserveFn; observeIntersectionForBottom?: ObserveFn;
observeIntersectionForPlaying?: ObserveFn; observeIntersectionForLoading?: ObserveFn;
onIntersectPinnedMessage?: OnIntersectPinnedMessage; observeIntersectionForPlaying?: ObserveFn;
} & MessagePositionProperties; onIntersectPinnedMessage?: OnIntersectPinnedMessage;
}
& MessagePositionProperties;
type StateProps = { type StateProps = {
theme: ThemeKey; theme: ThemeKey;
@ -264,7 +267,6 @@ type StateProps = {
isResizingContainer?: boolean; isResizingContainer?: boolean;
isForwarding?: boolean; isForwarding?: boolean;
isChatWithSelf?: boolean; isChatWithSelf?: boolean;
isBotForum?: boolean;
isRepliesChat?: boolean; isRepliesChat?: boolean;
isAnonymousForwards?: boolean; isAnonymousForwards?: boolean;
isChannel?: boolean; isChannel?: boolean;
@ -393,7 +395,6 @@ const Message = ({
isResizingContainer, isResizingContainer,
isForwarding, isForwarding,
isChatWithSelf, isChatWithSelf,
isBotForum,
isRepliesChat, isRepliesChat,
isAnonymousForwards, isAnonymousForwards,
isChannel, isChannel,
@ -463,7 +464,6 @@ const Message = ({
animateUnreadReaction, animateUnreadReaction,
focusMessage, focusMessage,
markMentionsRead, markMentionsRead,
openThread,
} = getActions(); } = getActions();
const ref = useRef<HTMLDivElement>(); const ref = useRef<HTMLDivElement>();
@ -524,7 +524,6 @@ const Message = ({
const { const {
id: messageId, chatId, forwardInfo, viaBotId, isTranscriptionError, factCheck, id: messageId, chatId, forwardInfo, viaBotId, isTranscriptionError, factCheck,
isTypingDraft,
} = message; } = message;
useUnmountCleanup(() => { useUnmountCleanup(() => {
@ -910,6 +909,8 @@ const Message = ({
|| ((asForwarded || isChatWithSelf) && forwardInfo?.postAuthorTitle) || ((asForwarded || isChatWithSelf) && forwardInfo?.postAuthorTitle)
|| undefined; || undefined;
useMessageResizeObserver(ref, isLastInList);
useEffect(() => { useEffect(() => {
const bottomMarker = bottomMarkerRef.current; const bottomMarker = bottomMarkerRef.current;
if (!bottomMarker || !isElementInViewport(bottomMarker)) return; if (!bottomMarker || !isElementInViewport(bottomMarker)) return;
@ -1026,7 +1027,6 @@ const Message = ({
canBeEmpty={hasFactCheck} canBeEmpty={hasFactCheck}
maxTimestamp={maxTimestamp} maxTimestamp={maxTimestamp}
threadId={threadId} threadId={threadId}
shouldAnimateTyping={isTypingDraft}
/> />
); );
} }
@ -1540,45 +1540,25 @@ const Message = ({
); );
} }
const handleInlineButtonClick = useLastCallback((button: ApiKeyboardButton) => { const handleSuggestedMessageButton = useLastCallback((payload: ActionPayloads['clickBotInlineButton']) => {
clickBotInlineButton({ if (payload.button.type !== 'suggestedMessage') return;
chatId, if (payload.button.buttonType === 'approve') {
messageId: message.id, openSuggestedPostApprovalModal({
threadId,
button,
});
});
const handleLocalInlineButtonClick = useLastCallback((button: ApiKeyboardButton) => {
if (button.type === 'openThread') {
openThread({
chatId,
threadId: messageTopic!.id,
});
return;
}
if (button.type === 'suggestedMessage') {
if (button.buttonType === 'approve') {
openSuggestedPostApprovalModal({
chatId,
messageId: message.id,
});
return;
}
if (button.buttonType === 'decline') {
openDeclineDialog();
return;
}
clickSuggestedMessageButton({
chatId, chatId,
messageId: message.id, messageId: message.id,
button,
}); });
return; return;
} }
if (payload.button.buttonType === 'decline') {
openDeclineDialog();
return;
}
clickSuggestedMessageButton({
...payload,
button: payload.button,
});
}); });
const handleDeclineReasonChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => { const handleDeclineReasonChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
@ -1692,52 +1672,11 @@ const Message = ({
const shouldRenderSuggestedPostButtons = message.suggestedPostInfo const shouldRenderSuggestedPostButtons = message.suggestedPostInfo
&& !message.isOutgoing && !message.suggestedPostInfo.isAccepted && !message.suggestedPostInfo.isRejected; && !message.isOutgoing && !message.suggestedPostInfo.isAccepted && !message.suggestedPostInfo.isRejected;
const isSuggestedPostExpired = (() => { const isSuggestedPostExpired = useMemo(() => {
if (!message.suggestedPostInfo?.scheduleDate || !minFutureTime) return false; if (!message.suggestedPostInfo?.scheduleDate || !minFutureTime) return false;
const now = getServerTime(); const now = getServerTime();
return message.suggestedPostInfo.scheduleDate <= now + minFutureTime; return message.suggestedPostInfo.scheduleDate <= now + minFutureTime;
})(); }, [message.suggestedPostInfo, minFutureTime]);
const suggestedPostButtons: ApiKeyboardButton[][] | undefined = useMemo(() => {
if (!shouldRenderSuggestedPostButtons) return undefined;
return [
[
{
type: 'suggestedMessage',
buttonType: 'decline',
text: lang('SuggestedPostDecline'),
},
{
type: 'suggestedMessage',
buttonType: 'approve',
text: lang('SuggestedPostApprove'),
disabled: isSuggestedPostExpired,
},
],
[
{
type: 'suggestedMessage',
buttonType: 'suggestChanges',
text: lang('SuggestedPostSuggestChanges'),
},
],
];
}, [isSuggestedPostExpired, lang, shouldRenderSuggestedPostButtons]);
const openThreadButtons: ApiKeyboardButton[][] | undefined = useMemo(() => {
if (!isBotForum || message.inlineButtons || !messageTopic || !isLastInList ||
threadId !== MAIN_THREAD_ID
) return undefined;
return [
[{
type: 'openThread',
text: lang('BotForumContinueThreadButton'),
}],
];
}, [isBotForum, lang, message.inlineButtons, messageTopic, isLastInList, threadId]);
const additionalInlineButtons = suggestedPostButtons || openThreadButtons;
return ( return (
<div <div
@ -1852,12 +1791,36 @@ const Message = ({
{withQuickReactionButton && quickReactionPosition === 'in-content' && renderQuickReactionButton()} {withQuickReactionButton && quickReactionPosition === 'in-content' && renderQuickReactionButton()}
</div> </div>
{message.inlineButtons && ( {message.inlineButtons && (
<InlineButtons inlineButtons={message.inlineButtons} onClick={handleInlineButtonClick} /> <InlineButtons message={message} onClick={clickBotInlineButton} />
)} )}
{additionalInlineButtons && ( {shouldRenderSuggestedPostButtons && (
<InlineButtons <InlineButtons
inlineButtons={additionalInlineButtons} message={{
onClick={handleLocalInlineButtonClick} ...message,
inlineButtons: [
[
{
type: 'suggestedMessage',
buttonType: 'decline',
text: lang('SuggestedPostDecline'),
},
{
type: 'suggestedMessage',
buttonType: 'approve',
text: lang('SuggestedPostApprove'),
disabled: isSuggestedPostExpired,
},
],
[
{
type: 'suggestedMessage',
buttonType: 'suggestChanges',
text: lang('SuggestedPostSuggestChanges'),
},
],
],
}}
onClick={handleSuggestedMessageButton}
/> />
)} )}
{reactionsPosition === 'outside' && !isStoryMention && ( {reactionsPosition === 'outside' && !isStoryMention && (
@ -2022,8 +1985,8 @@ export default memo(withGlobal<OwnProps>(
const hasUnreadReaction = chat?.unreadReactions?.includes(message.id); const hasUnreadReaction = chat?.unreadReactions?.includes(message.id);
const hasTopicChip = threadId === MAIN_THREAD_ID && chat?.isForum && !chat.isBotForum && isFirstInGroup; const hasTopicChip = threadId === MAIN_THREAD_ID && chat?.isForum && isFirstInGroup;
const messageTopic = selectTopicFromMessage(global, message); const messageTopic = hasTopicChip ? selectTopicFromMessage(global, message) : undefined;
const chatTranslations = selectChatTranslations(global, chatId); const chatTranslations = selectChatTranslations(global, chatId);
@ -2084,7 +2047,6 @@ export default memo(withGlobal<OwnProps>(
isForwarding, isForwarding,
reactionMessage, reactionMessage,
isChatWithSelf, isChatWithSelf,
isBotForum: chat?.isBotForum,
isRepliesChat: isSystemBotChat, isRepliesChat: isSystemBotChat,
isAnonymousForwards, isAnonymousForwards,
isChannel, isChannel,

View File

@ -96,7 +96,6 @@ export function renderPeerLink(peerId: string | undefined, text: string, asPrevi
getActions().openChat({ id: peerId }); getActions().openChat({ id: peerId });
}} }}
// box-decoration-break: clone; is broken when child has `dir` attribute // box-decoration-break: clone; is broken when child has `dir` attribute
// https://bugs.webkit.org/show_bug.cgi?id=296990
withMultilineFix={IS_SAFARI} withMultilineFix={IS_SAFARI}
> >
{renderText(text)} {renderText(text)}
@ -130,23 +129,6 @@ export function renderMessageLink(
); );
} }
export function renderTopicLink(chatId: string, topicId: number, content: TeactNode, asPreview?: boolean) {
if (asPreview) return content;
return (
<Link
className={styles.topicLink}
onClick={(e) => {
e.stopPropagation();
getActions().openThread({ chatId, threadId: topicId });
}}
withMultilineFix={IS_SAFARI}
>
{content}
</Link>
);
}
export function getCallMessageKey(action: ApiMessageActionPhoneCall, isOutgoing: boolean): RegularLangKey { export function getCallMessageKey(action: ApiMessageActionPhoneCall, isOutgoing: boolean): RegularLangKey {
const isMissed = action.reason === 'missed'; const isMissed = action.reason === 'missed';
const isCancelled = action.reason === 'busy' || action.duration === undefined; const isCancelled = action.reason === 'busy' || action.duration === undefined;

View File

@ -42,15 +42,14 @@ export type TransitionProps = {
slideClassName?: string; slideClassName?: string;
withSwipeControl?: boolean; withSwipeControl?: boolean;
isBlockingAnimation?: boolean; isBlockingAnimation?: boolean;
onStart?: NoneToVoidFunction;
onStop?: NoneToVoidFunction;
onScroll?: NoneToVoidFunction;
onMouseDown?: (e: React.MouseEvent<HTMLDivElement>) => void;
children: React.ReactNode | ChildrenFn; children: React.ReactNode | ChildrenFn;
'data-tauri-drag-region'?: true; 'data-tauri-drag-region'?: true;
contentSelector?: string; contentSelector?: string;
restoreHeightKey?: number; restoreHeightKey?: number;
onStart?: NoneToVoidFunction;
onStop?: NoneToVoidFunction;
onScroll?: NoneToVoidFunction;
onWheel?: (e: React.WheelEvent<HTMLDivElement>) => void;
onMouseDown?: (e: React.MouseEvent<HTMLDivElement>) => void;
}; };
const FALLBACK_ANIMATION_END = 1000; const FALLBACK_ANIMATION_END = 1000;
@ -88,15 +87,14 @@ function Transition({
slideClassName, slideClassName,
withSwipeControl, withSwipeControl,
isBlockingAnimation, isBlockingAnimation,
children,
'data-tauri-drag-region': dataTauriDragRegion,
contentSelector,
restoreHeightKey,
onStart, onStart,
onStop, onStop,
onScroll, onScroll,
onMouseDown, onMouseDown,
onWheel, children,
'data-tauri-drag-region': dataTauriDragRegion,
contentSelector,
restoreHeightKey,
}: TransitionProps) { }: TransitionProps) {
const currentKeyRef = useRef<number>(); const currentKeyRef = useRef<number>();
// No need for a container to update on change // No need for a container to update on change
@ -413,7 +411,6 @@ function Transition({
data-tauri-drag-region={dataTauriDragRegion} data-tauri-drag-region={dataTauriDragRegion}
onScroll={onScroll} onScroll={onScroll}
onMouseDown={onMouseDown} onMouseDown={onMouseDown}
onWheel={onWheel}
> >
{contents} {contents}
</div> </div>

View File

@ -338,7 +338,6 @@ export const REPLIES_USER_ID = '1271266957'; // TODO For Test connection ID must
export const VERIFICATION_CODES_USER_ID = '489000'; export const VERIFICATION_CODES_USER_ID = '489000';
export const ANONYMOUS_USER_ID = '2666000'; export const ANONYMOUS_USER_ID = '2666000';
export const RESTRICTED_EMOJI_SET_ID = '7173162320003080'; export const RESTRICTED_EMOJI_SET_ID = '7173162320003080';
export const LOCAL_MESSAGES_LIMIT = 1e6; // 1M
export const CHANNEL_ID_BASE = 10n ** 12n; export const CHANNEL_ID_BASE = 10n ** 12n;
export const DEFAULT_GIF_SEARCH_BOT_USERNAME = 'gif'; export const DEFAULT_GIF_SEARCH_BOT_USERNAME = 'gif';
export const ALL_FOLDER_ID = 0; export const ALL_FOLDER_ID = 0;

View File

@ -2386,6 +2386,9 @@ addActionHandler('processAttachBotParameters', async (global, actions, payload):
addActionHandler('loadTopics', async (global, actions, payload): Promise<void> => { addActionHandler('loadTopics', async (global, actions, payload): Promise<void> => {
if (selectIsCurrentUserFrozen(global)) return; if (selectIsCurrentUserFrozen(global)) return;
const { chatId, force } = payload; const { chatId, force } = payload;
if (selectIsCurrentUserFrozen(global)) {
return;
}
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
if (!chat) return; if (!chat) return;

View File

@ -148,7 +148,6 @@ import {
selectTabState, selectTabState,
selectThreadIdFromMessage, selectThreadIdFromMessage,
selectThreadInfo, selectThreadInfo,
selectThreadParam,
selectTopic, selectTopic,
selectTranslationLanguage, selectTranslationLanguage,
selectUser, selectUser,
@ -424,16 +423,6 @@ addActionHandler('sendMessage', async (global, actions, payload): Promise<void>
suggestedMedia = suggestedMessage.content; suggestedMedia = suggestedMessage.content;
} }
if (chat.isBotForum && threadId === MAIN_THREAD_ID && replyInfo?.type === 'message') {
const replyMessage = selectChatMessage(global, chatId!, replyInfo.replyToMsgId);
const replyThreadId = replyMessage && selectThreadIdFromMessage(global, replyMessage);
actions.openThread({
chatId: chatId!,
threadId: replyThreadId || replyInfo?.replyToTopId || replyInfo?.replyToMsgId,
tabId,
});
}
const params: SendMessageParams = { const params: SendMessageParams = {
...payload, ...payload,
chat, chat,
@ -453,24 +442,6 @@ addActionHandler('sendMessage', async (global, actions, payload): Promise<void>
actions.clearWebPagePreview({ tabId }); actions.clearWebPagePreview({ tabId });
} }
// Create new bot forum topic
if (chat.isBotForum && threadId === MAIN_THREAD_ID && replyInfo?.type !== 'message') {
const baseTitle = params.text ?? getTranslationFn()('BotForumTopicTitlePlaceholder');
const title = baseTitle.length > 12 ? `${baseTitle.slice(0, 12)}...` : baseTitle;
const topic = await callApi('createTopic', {
chat,
title,
isTitleMissing: true,
sendAs: params.sendAs,
});
if (topic) {
params.replyInfo = params.replyInfo?.type === 'message'
? { ...params.replyInfo, replyToTopId: topic }
: { type: 'message', replyToMsgId: topic, replyToTopId: topic };
getActions().openThread({ chatId: chat.id, threadId: topic });
}
}
const isSingle = (!payload.attachments || payload.attachments.length <= 1) && !isForwarding; const isSingle = (!payload.attachments || payload.attachments.length <= 1) && !isForwarding;
const isGrouped = !isSingle && payload.shouldGroupMessages; const isGrouped = !isSingle && payload.shouldGroupMessages;
const localMessages: SendMessageParams[] = []; const localMessages: SendMessageParams[] = [];
@ -1262,7 +1233,6 @@ addActionHandler('markMessageListRead', (global, actions, payload): ActionReturn
const viewportIds = selectViewportIds(global, chatId, threadId, tabId); const viewportIds = selectViewportIds(global, chatId, threadId, tabId);
const minId = selectFirstUnreadId(global, chatId, threadId); const minId = selectFirstUnreadId(global, chatId, threadId);
const topic = selectTopic(global, chatId, threadId);
if (threadId !== MAIN_THREAD_ID && !chat.isForum) { if (threadId !== MAIN_THREAD_ID && !chat.isForum) {
global = updateThreadInfo(global, chatId, threadId, { global = updateThreadInfo(global, chatId, threadId, {
@ -1271,7 +1241,7 @@ addActionHandler('markMessageListRead', (global, actions, payload): ActionReturn
return global; return global;
} }
if (!viewportIds || !minId || (!chat.unreadCount && !topic?.unreadCount)) { if (!viewportIds || !minId || !chat.unreadCount) {
return global; return global;
} }
@ -1280,17 +1250,17 @@ addActionHandler('markMessageListRead', (global, actions, payload): ActionReturn
return global; return global;
} }
const topic = selectTopic(global, chatId, threadId);
if (chat.isForum && topic) { if (chat.isForum && topic) {
global = updateThreadInfo(global, chatId, threadId, { global = updateThreadInfo(global, chatId, threadId, {
lastReadInboxMessageId: maxId, lastReadInboxMessageId: maxId,
}); });
const newTopicUnreadCount = Math.max(0, topic.unreadCount - readCount); const newTopicUnreadCount = Math.max(0, topic.unreadCount - readCount);
if (newTopicUnreadCount === 0 && !chat.isBotForum && chat.unreadCount) { if (newTopicUnreadCount === 0) {
global = updateChat(global, chatId, { global = updateChat(global, chatId, {
unreadCount: Math.max(0, chat.unreadCount - 1), unreadCount: Math.max(0, chat.unreadCount - 1),
}); });
} }
return updateTopic(global, chatId, Number(threadId), { return updateTopic(global, chatId, Number(threadId), {
unreadCount: newTopicUnreadCount, unreadCount: newTopicUnreadCount,
}); });
@ -1298,7 +1268,7 @@ addActionHandler('markMessageListRead', (global, actions, payload): ActionReturn
return updateChat(global, chatId, { return updateChat(global, chatId, {
lastReadInboxMessageId: maxId, lastReadInboxMessageId: maxId,
unreadCount: Math.max(0, (chat.unreadCount || 0) - readCount), unreadCount: Math.max(0, chat.unreadCount - readCount),
}); });
}); });
@ -1772,13 +1742,9 @@ async function loadViewportMessages<T extends GlobalState>(
global = getGlobal(); global = getGlobal();
const localTypingDrafts = selectThreadParam(global, chatId, threadId, 'typingDraftIdByRandomId');
const typingDraftMessages = localTypingDrafts ? Object.values(localTypingDrafts)
.map((id) => selectChatMessage(global, chatId, id))
.filter(Boolean) : [];
const localMessages = chatId === SERVICE_NOTIFICATIONS_USER_ID const localMessages = chatId === SERVICE_NOTIFICATIONS_USER_ID
? global.serviceNotifications.filter(({ isDeleted }) => !isDeleted).map(({ message }) => message) ? global.serviceNotifications.filter(({ isDeleted }) => !isDeleted).map(({ message }) => message)
: typingDraftMessages; : [];
const allMessages = ([] as ApiMessage[]).concat(messages, localMessages); const allMessages = ([] as ApiMessage[]).concat(messages, localMessages);
const byId = buildCollectionByKey(allMessages, 'id'); const byId = buildCollectionByKey(allMessages, 'id');
const ids = Object.keys(byId).map(Number); const ids = Object.keys(byId).map(Number);

View File

@ -156,8 +156,7 @@ async function loadAndReplaceMessages<T extends GlobalState>(global: T, actions:
currentChatId, currentChatId,
activeThreadId, activeThreadId,
), ),
activeThreadId !== MAIN_THREAD_ID && !currentChat.isForum activeThreadId !== MAIN_THREAD_ID && !getIsSavedDialog(currentChat.id, activeThreadId, global.currentUserId)
&& !getIsSavedDialog(currentChat.id, activeThreadId, global.currentUserId)
? callApi('fetchDiscussionMessage', { ? callApi('fetchDiscussionMessage', {
chat: currentChat, chat: currentChat,
messageId: Number(activeThreadId), messageId: Number(activeThreadId),

View File

@ -24,7 +24,6 @@ import {
updateChatFullInfo, updateChatFullInfo,
updateChatListType, updateChatListType,
updatePeerStoriesHidden, updatePeerStoriesHidden,
updateThreadInfo,
updateTopic, updateTopic,
} from '../../reducers'; } from '../../reducers';
import { updateUnreadReactions } from '../../reducers/reactions'; import { updateUnreadReactions } from '../../reducers/reactions';
@ -149,21 +148,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
} }
case 'updateChatInbox': { case 'updateChatInbox': {
const { id, threadId, lastReadInboxMessageId, unreadCount } = update; return updateChat(global, update.id, update.chat);
const chat = selectChat(global, id);
if (chat?.isBotForum && threadId) {
global = updateTopic(global, id, Number(threadId), {
unreadCount,
});
return updateThreadInfo(global, id, threadId, {
lastReadInboxMessageId,
});
} else {
return updateChat(global, id, {
lastReadInboxMessageId,
unreadCount,
});
}
} }
case 'updateChatTypingStatus': { case 'updateChatTypingStatus': {
@ -215,8 +200,6 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
unreadCount: topic.unreadCount ? topic.unreadCount + 1 : 1, unreadCount: topic.unreadCount ? topic.unreadCount + 1 : 1,
}); });
} }
// TODO Replace draft with new message
} }
setGlobal(global); setGlobal(global);

View File

@ -20,15 +20,9 @@ import { getMessageKey, isLocalMessageId } from '../../../util/keys/messageKey';
import { notifyAboutMessage } from '../../../util/notifications'; import { notifyAboutMessage } from '../../../util/notifications';
import { onTickEnd } from '../../../util/schedulers'; import { onTickEnd } from '../../../util/schedulers';
import { getServerTime } from '../../../util/serverTime'; import { getServerTime } from '../../../util/serverTime';
import { callApi } from '../../../api/gramjs';
import { import {
addPaidReaction, addPaidReaction,
checkIfHasUnreadReactions, checkIfHasUnreadReactions, getIsSavedDialog, getMessageContent, getMessageText, isActionMessage,
createApiMessageFromTypingDraft,
getIsSavedDialog,
getMessageContent,
getMessageText,
isActionMessage,
isMessageLocal, isMessageLocal,
} from '../../helpers'; } from '../../helpers';
import { getMessageReplyInfo, getStoryReplyInfo } from '../../helpers/replies'; import { getMessageReplyInfo, getStoryReplyInfo } from '../../helpers/replies';
@ -91,11 +85,9 @@ import {
selectScheduledIds, selectScheduledIds,
selectScheduledMessage, selectScheduledMessage,
selectTabState, selectTabState,
selectThread,
selectThreadByMessage, selectThreadByMessage,
selectThreadIdFromMessage, selectThreadIdFromMessage,
selectThreadInfo, selectThreadInfo,
selectThreadParam,
selectTopic, selectTopic,
selectTopicFromMessage, selectTopicFromMessage,
selectViewportIds, selectViewportIds,
@ -186,14 +178,6 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
actions.reportMessageDelivery({ chatId, messageId: id }); actions.reportMessageDelivery({ chatId, messageId: id });
} }
if (chat?.isBotForum && !newMessage.isOutgoing && !isLocal) {
const threadId = selectThreadIdFromMessage(global, newMessage);
const typingDraftStore = selectThreadParam(global, chatId, threadId, 'typingDraftIdByRandomId');
const localDraftIds = Object.values(typingDraftStore || {});
global = deleteChatMessages(global, chatId, localDraftIds);
global = replaceThreadParam(global, chatId, threadId, 'typingDraftIdByRandomId', undefined);
}
setGlobal(global); setGlobal(global);
// Reload dialogs if chat is not present in the list // Reload dialogs if chat is not present in the list
@ -929,78 +913,6 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
global = updateMessageTranslations(global, chatId, messageIds, toLanguageCode, []); global = updateMessageTranslations(global, chatId, messageIds, toLanguageCode, []);
setGlobal(global); setGlobal(global);
break;
}
case 'updateChatTypingDraft': {
const { id, chatId, threadId = MAIN_THREAD_ID, text } = update;
const thread = selectThread(global, chatId, threadId);
if (!thread) return undefined;
let typingDraftStore = selectThreadParam(global, chatId, threadId, 'typingDraftIdByRandomId');
const messageId = typingDraftStore?.[id];
const isUpdatingDraft = Boolean(messageId);
const updatingMessage = isUpdatingDraft ? selectChatMessage(global, chatId, messageId) : undefined;
const rescheduleDraftRemoval = () => {
// Clear typing draft after timeout
setTimeout(() => {
global = getGlobal();
const currentTypingDraftStore = selectThreadParam(global, chatId, threadId, 'typingDraftIdByRandomId');
if (currentTypingDraftStore?.[id]) {
const currentMessageId = currentTypingDraftStore[id];
const currentMessage = selectChatMessage(global, chatId, currentMessageId);
// Already deleted or replaced with a new message
if (!currentMessage || getServerTime() - currentMessage.editDate! < global.appConfig.typingDraftTtl) return;
const newTypingDraftIds = omit(currentTypingDraftStore, [id]);
global = replaceThreadParam(global, chatId, threadId, 'typingDraftIdByRandomId', newTypingDraftIds);
global = deleteChatMessages(global, chatId, [currentMessageId]);
setGlobal(global);
}
}, global.appConfig.typingDraftTtl * 1000);
};
if (isUpdatingDraft && updatingMessage) {
global = updateChatMessage(global, chatId, messageId, {
content: {
text,
},
editDate: getServerTime(),
});
rescheduleDraftRemoval();
return global;
}
// Let worker know that we have new local message
callApi('incrementLocalMessagesCounter');
const lastMessageId = selectChatLastMessageId(global, chatId);
const newMessage = createApiMessageFromTypingDraft({
lastMessageId: lastMessageId || 0,
chatId,
threadId,
text,
});
actions.apiUpdate({
'@type': 'newMessage',
chatId,
id: newMessage.id,
message: newMessage,
});
typingDraftStore = {
...typingDraftStore,
[id]: newMessage.id,
};
global = replaceThreadParam(global, chatId, threadId, 'typingDraftIdByRandomId', typingDraftStore);
rescheduleDraftRemoval();
return global;
} }
} }
}); });

View File

@ -500,11 +500,6 @@ addActionHandler('scrollMessageListToBottom', (global, actions, payload): Action
cancelScrollBlockingAnimation(); cancelScrollBlockingAnimation();
} }
const isViewportNewest = selectIsViewportNewest(global, chatId, threadId, tabId);
if (isViewportNewest) {
return;
}
actions.loadViewportMessages({ actions.loadViewportMessages({
chatId, chatId,
threadId, threadId,

View File

@ -665,7 +665,7 @@ function reduceMessages<T extends GlobalState>(global: T): GlobalState['messages
}, {} as GlobalState['messages']['byChatId'][string]['threadsById']); }, {} as GlobalState['messages']['byChatId'][string]['threadsById']);
const cleanedById = Object.values(byId).reduce((acc, message) => { const cleanedById = Object.values(byId).reduce((acc, message) => {
if (!message || message.isTypingDraft) return acc; if (!message) return acc;
let cleanedMessage = omitLocalMedia(message); let cleanedMessage = omitLocalMedia(message);
cleanedMessage = omitLocalPaidReactions(cleanedMessage); cleanedMessage = omitLocalPaidReactions(cleanedMessage);

View File

@ -9,8 +9,7 @@ import type {
ApiTypeStory, ApiTypeStory,
} from '../../api/types'; } from '../../api/types';
import type { import type {
ApiFormattedText, ApiPoll, ApiWebPage, MediaContainer, StatefulMediaContent,
ApiPoll, ApiReplyInfo, ApiWebPage, MediaContainer, StatefulMediaContent,
} from '../../api/types/messages'; } from '../../api/types/messages';
import type { ThreadId } from '../../types'; import type { ThreadId } from '../../types';
import type { LangFn } from '../../util/localization'; import type { LangFn } from '../../util/localization';
@ -18,7 +17,6 @@ import type { GlobalState } from '../types';
import { ApiMessageEntityTypes, MAIN_THREAD_ID } from '../../api/types'; import { ApiMessageEntityTypes, MAIN_THREAD_ID } from '../../api/types';
import { import {
LOCAL_MESSAGES_LIMIT,
LOTTIE_STICKER_MIME_TYPE, LOTTIE_STICKER_MIME_TYPE,
RE_LINK_TEMPLATE, RE_LINK_TEMPLATE,
SERVICE_NOTIFICATIONS_USER_ID, SERVICE_NOTIFICATIONS_USER_ID,
@ -40,11 +38,6 @@ import { getMainUsername } from './users';
const RE_LINK = new RegExp(RE_LINK_TEMPLATE, 'i'); const RE_LINK = new RegExp(RE_LINK_TEMPLATE, 'i');
let uiLocalMessageCounter = 0;
function getNextLocalMessageId(lastMessageId = 0) {
return lastMessageId + (++uiLocalMessageCounter / LOCAL_MESSAGES_LIMIT);
}
export function getMessageHtmlId(messageId: number, index?: number) { export function getMessageHtmlId(messageId: number, index?: number) {
const parts = ['message', messageId.toString().replace('.', '-'), index].filter(Boolean); const parts = ['message', messageId.toString().replace('.', '-'), index].filter(Boolean);
return parts.join('-'); return parts.join('-');
@ -511,38 +504,3 @@ export function getSuggestedChangesActionText(
withMarkdown: true, withMarkdown: true,
}); });
} }
export function createApiMessageFromTypingDraft({
lastMessageId,
chatId,
threadId,
text,
}: {
lastMessageId: number;
chatId: string;
threadId: ThreadId;
text: ApiFormattedText;
}): ApiMessage {
const localId = getNextLocalMessageId(lastMessageId);
const replyInfo: ApiReplyInfo | undefined = typeof threadId === 'number' && threadId !== MAIN_THREAD_ID ? {
type: 'message',
replyToMsgId: threadId,
replyToTopId: threadId,
isForumTopic: true,
} : undefined;
return {
id: localId,
chatId,
replyInfo,
isOutgoing: false,
date: getServerTime(),
content: {
text,
},
isSilent: true,
isTypingDraft: true,
editDate: getServerTime(),
};
}

View File

@ -1,5 +1,4 @@
import type { import type {
ApiFormattedText,
ApiMessage, ApiPoll, ApiPollResult, ApiQuickReply, ApiSponsoredMessage, ApiThreadInfo, ApiMessage, ApiPoll, ApiPollResult, ApiQuickReply, ApiSponsoredMessage, ApiThreadInfo,
ApiWebPage, ApiWebPage,
ApiWebPageFull, ApiWebPageFull,
@ -47,7 +46,6 @@ import {
selectTabState, selectTabState,
selectThreadIdFromMessage, selectThreadIdFromMessage,
selectThreadInfo, selectThreadInfo,
selectThreadParam,
selectViewportIds, selectViewportIds,
selectWebPage, selectWebPage,
} from '../selectors'; } from '../selectors';
@ -1066,24 +1064,3 @@ export function updatePollVote<T extends GlobalState>(
}, },
}); });
} }
export function updateTypingDraft<T extends GlobalState>(
global: T,
chatId: string,
threadId: ThreadId | undefined = MAIN_THREAD_ID,
randomId: string,
text: ApiFormattedText,
) {
const typingDraftStore = selectThreadParam(global, chatId, threadId, 'typingDraftIdByRandomId');
const messageId = typingDraftStore?.[randomId];
if (!messageId) {
return global;
}
global = updateChatMessage(global, chatId, messageId, {
content: {
text,
},
});
return global;
}

View File

@ -127,7 +127,7 @@ export function updatePeerPhotos<T extends GlobalState>(
}); });
} }
const hasFallbackPhoto = fallbackPhoto && currentPhotos.photos.at(-1)?.id === fallbackPhoto.id; const hasFallbackPhoto = currentPhotos.photos.at(-1).id === fallbackPhoto?.id;
const currentPhotoArray = hasFallbackPhoto ? currentPhotos.photos.slice(0, -1) : currentPhotos.photos; const currentPhotoArray = hasFallbackPhoto ? currentPhotos.photos.slice(0, -1) : currentPhotos.photos;
const photos = uniqueByField([...currentPhotoArray, ...newPhotos, fallbackPhoto].filter(Boolean), 'id'); const photos = uniqueByField([...currentPhotoArray, ...newPhotos, fallbackPhoto].filter(Boolean), 'id');

View File

@ -551,8 +551,7 @@ export function selectCanDeleteTopic<T extends GlobalState>(global: T, chatId: s
if (topicId === GENERAL_TOPIC_ID) return false; if (topicId === GENERAL_TOPIC_ID) return false;
return chat.isBotForum return chat.isCreator
|| chat.isCreator
|| getHasAdminRight(chat, 'deleteMessages') || getHasAdminRight(chat, 'deleteMessages')
|| (chat.isForum || (chat.isForum
&& selectCanDeleteOwnerTopic(global, chat.id, topicId)); && selectCanDeleteOwnerTopic(global, chat.id, topicId));
@ -1560,7 +1559,7 @@ export function selectTopicLink<T extends GlobalState>(
global: T, chatId: string, topicId?: ThreadId, global: T, chatId: string, topicId?: ThreadId,
) { ) {
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
if (!chat || !chat.isForum || chat.isBotForum) { if (!chat || !chat?.isForum) {
return undefined; return undefined;
} }

View File

@ -22,6 +22,7 @@ import type {
ApiInputSavedStarGift, ApiInputSavedStarGift,
ApiInputSuggestedPostInfo, ApiInputSuggestedPostInfo,
ApiKeyboardButton, ApiKeyboardButton,
ApiKeyboardButtonSuggestedMessage,
ApiLimitTypeWithModal, ApiLimitTypeWithModal,
ApiMessage, ApiMessage,
ApiMessageEntity, ApiMessageEntity,
@ -59,7 +60,6 @@ import type {
ApiUser, ApiUser,
ApiVideo, ApiVideo,
BotsPrivacyType, BotsPrivacyType,
KeyboardButtonSuggestedMessage,
LinkContext, LinkContext,
PrivacyVisibility, PrivacyVisibility,
} from '../../api/types'; } from '../../api/types';
@ -2084,7 +2084,7 @@ export interface ActionPayloads {
clickSuggestedMessageButton: { clickSuggestedMessageButton: {
chatId: string; chatId: string;
messageId: number; messageId: number;
button: KeyboardButtonSuggestedMessage; button: ApiKeyboardButtonSuggestedMessage;
} & WithTabId; } & WithTabId;
switchBotInline: { switchBotInline: {

View File

@ -0,0 +1,53 @@
import type { ElementRef } from '../lib/teact/teact';
import { beginHeavyAnimation, useRef } from '../lib/teact/teact';
import { getActions } from '../global';
import { isAnimatingScroll } from '../util/animateScroll';
import useLastCallback from './useLastCallback';
import useResizeObserver from './useResizeObserver';
import useThrottledCallback from './useThrottledCallback';
const BOTTOM_FOCUS_SCROLL_THRESHOLD = 5;
const THROTTLE_MS = 300;
const RESIZE_ANIMATION_DURATION = 400;
function useMessageResizeObserver(
ref: ElementRef<HTMLElement> | undefined,
shouldFocusOnResize = false,
) {
const {
scrollMessageListToBottom,
} = getActions();
const messageHeightRef = useRef(0);
const handleResize = useLastCallback(
(entry) => {
const lastHeight = messageHeightRef.current;
const newHeight = entry.contentRect.height;
messageHeightRef.current = newHeight;
if (isAnimatingScroll() || !lastHeight || newHeight <= lastHeight) return;
const container = entry.target.closest('.MessageList');
if (!container) return;
beginHeavyAnimation(RESIZE_ANIMATION_DURATION);
const resizeDiff = newHeight - lastHeight;
const { offsetHeight, scrollHeight, scrollTop } = container;
const currentScrollBottom = Math.round(scrollHeight - scrollTop - offsetHeight);
const previousScrollBottom = currentScrollBottom - resizeDiff;
if (previousScrollBottom <= BOTTOM_FOCUS_SCROLL_THRESHOLD) {
scrollMessageListToBottom();
}
},
);
const throttledResize = useThrottledCallback(handleResize, [handleResize], THROTTLE_MS, false);
useResizeObserver(ref, throttledResize, !shouldFocusOnResize);
}
export default useMessageResizeObserver;

View File

@ -157,5 +157,4 @@ export const DEFAULT_APP_CONFIG: ApiAppConfig = {
'fragment.com', 'fragment.com',
'translations.telegram.org', 'translations.telegram.org',
], ],
typingDraftTtl: 10,
}; };

View File

@ -3,8 +3,8 @@
font-weight: normal; font-weight: normal;
font-style: normal; font-style: normal;
font-display: block; font-display: block;
src: url("./icons.woff2?33f6294c2f4a2ffb1e77473fb35bc539") format("woff2"), src: url("./icons.woff2?cba4fd3111b01885aeeafb1a9ec56b6b") format("woff2"),
url("./icons.woff?33f6294c2f4a2ffb1e77473fb35bc539") format("woff"); url("./icons.woff?cba4fd3111b01885aeeafb1a9ec56b6b") format("woff");
} }
.icon-char::before { .icon-char::before {
@ -810,105 +810,102 @@ url("./icons.woff?33f6294c2f4a2ffb1e77473fb35bc539") format("woff");
.icon-toncoin::before { .icon-toncoin::before {
content: "\f207"; content: "\f207";
} }
.icon-topic-new::before { .icon-trade::before {
content: "\f208"; content: "\f208";
} }
.icon-trade::before { .icon-transcribe::before {
content: "\f209"; content: "\f209";
} }
.icon-transcribe::before { .icon-truck::before {
content: "\f20a"; content: "\f20a";
} }
.icon-truck::before { .icon-unarchive::before {
content: "\f20b"; content: "\f20b";
} }
.icon-unarchive::before { .icon-underlined::before {
content: "\f20c"; content: "\f20c";
} }
.icon-underlined::before { .icon-understood::before {
content: "\f20d"; content: "\f20d";
} }
.icon-understood::before { .icon-unique-profile::before {
content: "\f20e"; content: "\f20e";
} }
.icon-unique-profile::before { .icon-unlist-outline::before {
content: "\f20f"; content: "\f20f";
} }
.icon-unlist-outline::before { .icon-unlist::before {
content: "\f210"; content: "\f210";
} }
.icon-unlist::before { .icon-unlock-badge::before {
content: "\f211"; content: "\f211";
} }
.icon-unlock-badge::before { .icon-unlock::before {
content: "\f212"; content: "\f212";
} }
.icon-unlock::before { .icon-unmute::before {
content: "\f213"; content: "\f213";
} }
.icon-unmute::before { .icon-unpin::before {
content: "\f214"; content: "\f214";
} }
.icon-unpin::before { .icon-unread::before {
content: "\f215"; content: "\f215";
} }
.icon-unread::before { .icon-up::before {
content: "\f216"; content: "\f216";
} }
.icon-up::before { .icon-user-filled::before {
content: "\f217"; content: "\f217";
} }
.icon-user-filled::before { .icon-user-online::before {
content: "\f218"; content: "\f218";
} }
.icon-user-online::before { .icon-user-stars::before {
content: "\f219"; content: "\f219";
} }
.icon-user-stars::before { .icon-user::before {
content: "\f21a"; content: "\f21a";
} }
.icon-user::before { .icon-video-outlined::before {
content: "\f21b"; content: "\f21b";
} }
.icon-video-outlined::before { .icon-video-stop::before {
content: "\f21c"; content: "\f21c";
} }
.icon-video-stop::before { .icon-video::before {
content: "\f21d"; content: "\f21d";
} }
.icon-video::before { .icon-view-once::before {
content: "\f21e"; content: "\f21e";
} }
.icon-view-once::before { .icon-voice-chat::before {
content: "\f21f"; content: "\f21f";
} }
.icon-voice-chat::before { .icon-volume-1::before {
content: "\f220"; content: "\f220";
} }
.icon-volume-1::before { .icon-volume-2::before {
content: "\f221"; content: "\f221";
} }
.icon-volume-2::before { .icon-volume-3::before {
content: "\f222"; content: "\f222";
} }
.icon-volume-3::before { .icon-warning::before {
content: "\f223"; content: "\f223";
} }
.icon-warning::before { .icon-web::before {
content: "\f224"; content: "\f224";
} }
.icon-web::before { .icon-webapp::before {
content: "\f225"; content: "\f225";
} }
.icon-webapp::before { .icon-word-wrap::before {
content: "\f226"; content: "\f226";
} }
.icon-word-wrap::before { .icon-zoom-in::before {
content: "\f227"; content: "\f227";
} }
.icon-zoom-in::before { .icon-zoom-out::before {
content: "\f228"; content: "\f228";
} }
.icon-zoom-out::before {
content: "\f229";
}

View File

@ -279,38 +279,37 @@ $icons-map: (
"tag": "\f205", "tag": "\f205",
"timer": "\f206", "timer": "\f206",
"toncoin": "\f207", "toncoin": "\f207",
"topic-new": "\f208", "trade": "\f208",
"trade": "\f209", "transcribe": "\f209",
"transcribe": "\f20a", "truck": "\f20a",
"truck": "\f20b", "unarchive": "\f20b",
"unarchive": "\f20c", "underlined": "\f20c",
"underlined": "\f20d", "understood": "\f20d",
"understood": "\f20e", "unique-profile": "\f20e",
"unique-profile": "\f20f", "unlist-outline": "\f20f",
"unlist-outline": "\f210", "unlist": "\f210",
"unlist": "\f211", "unlock-badge": "\f211",
"unlock-badge": "\f212", "unlock": "\f212",
"unlock": "\f213", "unmute": "\f213",
"unmute": "\f214", "unpin": "\f214",
"unpin": "\f215", "unread": "\f215",
"unread": "\f216", "up": "\f216",
"up": "\f217", "user-filled": "\f217",
"user-filled": "\f218", "user-online": "\f218",
"user-online": "\f219", "user-stars": "\f219",
"user-stars": "\f21a", "user": "\f21a",
"user": "\f21b", "video-outlined": "\f21b",
"video-outlined": "\f21c", "video-stop": "\f21c",
"video-stop": "\f21d", "video": "\f21d",
"video": "\f21e", "view-once": "\f21e",
"view-once": "\f21f", "voice-chat": "\f21f",
"voice-chat": "\f220", "volume-1": "\f220",
"volume-1": "\f221", "volume-2": "\f221",
"volume-2": "\f222", "volume-3": "\f222",
"volume-3": "\f223", "warning": "\f223",
"warning": "\f224", "web": "\f224",
"web": "\f225", "webapp": "\f225",
"webapp": "\f226", "word-wrap": "\f226",
"word-wrap": "\f227", "zoom-in": "\f227",
"zoom-in": "\f228", "zoom-out": "\f228",
"zoom-out": "\f229",
); );

Binary file not shown.

Binary file not shown.

View File

@ -262,7 +262,6 @@ export type FontIconName =
| 'tag' | 'tag'
| 'timer' | 'timer'
| 'toncoin' | 'toncoin'
| 'topic-new'
| 'trade' | 'trade'
| 'transcribe' | 'transcribe'
| 'truck' | 'truck'

View File

@ -588,13 +588,6 @@ export type StarGiftInfo = {
shouldUpgrade?: boolean; shouldUpgrade?: boolean;
}; };
export type TypingDraft = {
senderId: string;
id: string;
date: number;
text: ApiFormattedText;
};
export interface TabThread { export interface TabThread {
scrollOffset?: number; scrollOffset?: number;
replyStack?: number[]; replyStack?: number[];
@ -617,7 +610,6 @@ export interface Thread {
threadInfo?: ApiThreadInfo; threadInfo?: ApiThreadInfo;
firstMessageId?: number; firstMessageId?: number;
typingStatus?: ApiTypingStatus; typingStatus?: ApiTypingStatus;
typingDraftIdByRandomId?: Record<string, number>;
} }
export interface ServiceNotification { export interface ServiceNotification {

View File

@ -1720,10 +1720,6 @@ export interface LangPair {
'ConfirmBuyGiftForTonDescription': undefined; 'ConfirmBuyGiftForTonDescription': undefined;
'TitleGiftLocked': undefined; 'TitleGiftLocked': undefined;
'QuickPreview': undefined; 'QuickPreview': undefined;
'BotForumContinueThreadButton': undefined;
'BotForumActionNew': undefined;
'BotForumActionNewDescription': undefined;
'BotForumTopicTitlePlaceholder': undefined;
'DropOriginalDetailsTransaction': undefined; 'DropOriginalDetailsTransaction': undefined;
'StarGiftReasonDropOriginalDetails': undefined; 'StarGiftReasonDropOriginalDetails': undefined;
'GiftAnUpgradeButton': undefined; 'GiftAnUpgradeButton': undefined;
@ -1732,8 +1728,6 @@ export interface LangPair {
'UserNoteTitle': undefined; 'UserNoteTitle': undefined;
'UserNoteHint': undefined; 'UserNoteHint': undefined;
'EditUserNoteHint': undefined; 'EditUserNoteHint': undefined;
'BotForumAllTopicTitle': undefined;
'BotForumAllTopicDescription': undefined;
'AriaStoryTogglerOpen': undefined; 'AriaStoryTogglerOpen': undefined;
'InviteBlockedTitle': undefined; 'InviteBlockedTitle': undefined;
'InviteBlockedOneMessage': undefined; 'InviteBlockedOneMessage': undefined;