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> {
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> {
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_country?: string;
verify_age_min?: number;
message_typing_draft_ttl?: 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,
verifyAgeCountry: appConfig.verify_age_country,
verifyAgeMin: appConfig.verify_age_min,
typingDraftTtl: appConfig.message_typing_draft_ttl,
};
return {

View File

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

View File

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

View File

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

View File

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

View File

@ -75,7 +75,6 @@ import {
buildLocalMessage,
buildPreparedInlineMessage,
buildUploadingMedia,
incrementLocalMessageCounter,
} from '../apiBuilders/messages';
import { getApiChatIdFromMtpPeer } from '../apiBuilders/peers';
import { buildApiUser, buildApiUserStatuses } from '../apiBuilders/users';
@ -1256,21 +1255,23 @@ export async function markMessageListRead({
}) {
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) {
await invokeRequest(new GramJs.channels.ReadHistory({
channel: buildInputChannel(chat.id, chat.accessHash),
maxId,
maxId: fixedMaxId,
}));
} else if (threadId !== MAIN_THREAD_ID) {
} else if (isChannel) {
await invokeRequest(new GramJs.messages.ReadDiscussion({
peer: buildInputPeer(chat.id, chat.accessHash),
msgId: Number(threadId),
readMaxId: maxId,
readMaxId: fixedMaxId,
}));
} else {
const result = await invokeRequest(new GramJs.messages.ReadHistory({
peer: buildInputPeer(chat.id, chat.accessHash),
maxId,
maxId: fixedMaxId,
}));
if (result) {
@ -2554,7 +2555,3 @@ export async function fetchPreparedInlineMessage({
return buildPreparedInlineMessage(result);
}
export function incrementLocalMessagesCounter() {
incrementLocalMessageCounter();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -47,7 +47,6 @@ export interface ApiUser {
botActiveUsers?: number;
botVerificationIconId?: string;
paidMessagesStars?: number;
isBotForum?: boolean;
}
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";
"GiftLockedMessage" = "This gift is currently only available to earlier Telegram users. It will unlock for your account in about **{relativeDate}**.";
"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";
"StarGiftReasonDropOriginalDetails" = "Removed Description";
"GiftAnUpgradeButton" = "Gift an Upgrade";
@ -2314,8 +2310,6 @@
"UserNoteTitle" = "Notes";
"UserNoteHint" = "only visible to you";
"EditUserNoteHint" = "Notes are only visible to you.";
"BotForumAllTopicTitle" = "All Messages";
"BotForumAllTopicDescription" = "All messages from all topics";
"AriaStoryTogglerOpen" = "Open Story List";
"FileTransferProgress" = "{currentSize} / {totalSize}";
"MediaSizeB_one" = "{size}B";

View File

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

View File

@ -12,12 +12,9 @@ import trimText from '../../util/trimText';
import { insertTextEntity, renderTextWithEntities } from './helpers/renderTextWithEntities';
import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback';
import useSyncEffect from '../../hooks/useSyncEffect';
import useUniqueId from '../../hooks/useUniqueId';
import TypingWrapper from './TypingWrapper';
interface OwnProps {
messageOrStory: ApiMessage | ApiStory;
threadId?: ThreadId;
@ -39,7 +36,6 @@ interface OwnProps {
isInSelectMode?: boolean;
canBeEmpty?: boolean;
maxTimestamp?: number;
shouldAnimateTyping?: boolean;
}
const MIN_CUSTOM_EMOJIS_FOR_SHARED_CANVAS = 3;
@ -65,7 +61,6 @@ function MessageText({
canBeEmpty,
maxTimestamp,
threadId,
shouldAnimateTyping,
}: OwnProps) {
const sharedCanvasRef = useRef<HTMLCanvasElement>();
const sharedCanvasHqRef = useRef<HTMLCanvasElement>();
@ -112,48 +107,37 @@ function MessageText({
return customEmojisCount >= MIN_CUSTOM_EMOJIS_FOR_SHARED_CANVAS;
}, [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) {
return <span className="content-unsupported">{lang('MessageUnsupported')}</span>;
}
const textToRender: ApiFormattedText = {
text: trimText(text || '', truncateLength),
entities: entitiesWithFocusedQuote,
};
return (
<>
{[
withSharedCanvas && <canvas key="shared-canvas" ref={sharedCanvasRef} className="shared-canvas" />,
withSharedCanvas && <canvas key="shared-canvas-hq" ref={sharedCanvasHqRef} className="shared-canvas" />,
shouldAnimateTyping ? (
<TypingWrapper key="typing-wrapper" text={textToRender}>{renderText}</TypingWrapper>
) : renderText(textToRender),
withSharedCanvas && <canvas ref={sharedCanvasRef} className="shared-canvas" />,
withSharedCanvas && <canvas ref={sharedCanvasHqRef} className="shared-canvas" />,
renderTextWithEntities({
text: trimText(text!, truncateLength),
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)}
</>
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,17 +1,17 @@
import type { FC } from '../../../../lib/teact/teact';
import { memo } from '../../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../../global';
import type { FC } from '../../../lib/teact/teact';
import { memo } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type {
ApiChat, ApiDraft, ApiMessage, ApiMessageOutgoingStatus,
ApiPeer, ApiTopic, ApiTypeStory, ApiTypingStatus,
} from '../../../../api/types';
import type { ObserveFn } from '../../../../hooks/useIntersectionObserver';
import type { ChatAnimationTypes } from '../hooks';
} from '../../../api/types';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import type { ChatAnimationTypes } from './hooks';
import { UNMUTE_TIMESTAMP } from '../../../../config';
import { groupStatefulContent } from '../../../../global/helpers';
import { getIsChatMuted } from '../../../../global/helpers/notifications';
import { UNMUTE_TIMESTAMP } from '../../../config';
import { groupStatefulContent } from '../../../global/helpers';
import { getIsChatMuted } from '../../../global/helpers/notifications';
import {
selectCanAnimateInterface,
selectCanDeleteTopic,
@ -27,25 +27,25 @@ import {
selectThreadInfo,
selectThreadParam,
selectTopics,
} 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 renderText from '../../../common/helpers/renderText';
} 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 renderText from '../../common/helpers/renderText';
import useFlag from '../../../../hooks/useFlag';
import useLastCallback from '../../../../hooks/useLastCallback';
import useOldLang from '../../../../hooks/useOldLang';
import useChatListEntry from '../hooks/useChatListEntry';
import useTopicContextActions from '../hooks/useTopicContextActions';
import useFlag from '../../../hooks/useFlag';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import useChatListEntry from './hooks/useChatListEntry';
import useTopicContextActions from './hooks/useTopicContextActions';
import Icon from '../../../common/icons/Icon';
import LastMessageMeta from '../../../common/LastMessageMeta';
import TopicIcon from '../../../common/TopicIcon';
import ConfirmDialog from '../../../ui/ConfirmDialog';
import ListItem from '../../../ui/ListItem';
import MuteChatModal from '../../MuteChatModal.async';
import ChatBadge from '../ChatBadge';
import Icon from '../../common/icons/Icon';
import LastMessageMeta from '../../common/LastMessageMeta';
import TopicIcon from '../../common/TopicIcon';
import ConfirmDialog from '../../ui/ConfirmDialog';
import ListItem from '../../ui/ListItem';
import MuteChatModal from '../MuteChatModal.async';
import ChatBadge from './ChatBadge';
import styles from './Topic.module.scss';
@ -165,7 +165,7 @@ const Topic: FC<OwnProps & StateProps> = ({
}
openThread({ chatId, threadId: topic.id, shouldReplaceHistory: true });
if (!chat.isBotForum && !chat.isMonoforum) setViewForumAsMessages({ chatId, isEnabled: false });
setViewForumAsMessages({ chatId, isEnabled: false });
if (canScrollDown) {
scrollMessageListToBottom();
@ -263,7 +263,7 @@ export default memo(withGlobal<OwnProps>(
const typingStatus = selectThreadParam(global, chatId, topic.id, 'typingStatus');
const draft = selectDraft(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 { 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 { getCanManageTopic, getHasAdminRight } from '../../../../global/helpers';
import { IS_TAURI } from '../../../../util/browser/globalEnvironment';
import { IS_OPEN_IN_NEW_TAB_SUPPORTED } from '../../../../util/browser/windowEnvironment';
import { compact } from '../../../../util/iteratees';
@ -49,11 +48,11 @@ export default function useTopicContextActions({
openQuickPreview,
} = getActions();
const canToggleClosed = getCanManageTopic(chat, topic) && !chat.isBotForum;
const canToggleClosed = getCanManageTopic(chat, topic);
const canTogglePinned = chat.isCreator || getHasAdminRight(chat, 'manageTopics');
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',
handler: () => {
openChatInNewTab({ chatId: chat.id, threadId: topicId });

View File

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

View File

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

View File

@ -14,7 +14,7 @@ import {
MESSAGE_LIST_SLICE,
SERVICE_NOTIFICATIONS_USER_ID,
} from '../../config';
import { forceMeasure, requestForcedReflow, requestMeasure, requestMutation } from '../../lib/fasterdom/fasterdom';
import { forceMeasure, requestForcedReflow, requestMeasure } from '../../lib/fasterdom/fasterdom';
import {
getIsSavedDialog,
getMessageHtmlId,
@ -54,7 +54,6 @@ import {
import { selectIsChatRestricted } from '../../global/selectors/chats';
import { selectActiveRestrictionReasons, selectCurrentMessageList } from '../../global/selectors/messages';
import animateScroll, { isAnimatingScroll, restartCurrentScrollAnimation } from '../../util/animateScroll';
import { IS_FIREFOX } from '../../util/browser/windowEnvironment';
import buildClassName from '../../util/buildClassName';
import { isUserId } from '../../util/entities/ids';
import { orderBy } from '../../util/iteratees';
@ -146,7 +145,6 @@ type StateProps = {
translationLanguage?: string;
shouldAutoTranslate?: boolean;
isActive?: boolean;
isBotForum?: boolean;
shouldScrollToBottom?: boolean;
};
@ -168,19 +166,13 @@ const MESSAGE_REACTIONS_POLLING_INTERVAL = 20 * 1000;
const MESSAGE_COMMENTS_POLLING_INTERVAL = 20 * 1000;
const MESSAGE_FACT_CHECK_UPDATE_INTERVAL = 5 * 1000;
const MESSAGE_STORY_POLLING_INTERVAL = 5 * 60 * 1000;
const BOTTOM_THRESHOLD = 50;
const BOTTOM_SNAP_THRESHOLD = 10;
const UNREAD_DIVIDER_TOP = 10;
const SCROLL_DEBOUNCE = 200;
const MESSAGE_ANIMATION_DURATION = 500;
const BOTTOM_FOCUS_MARGIN = 0.5 * REM;
const SELECT_MODE_ANIMATION_DURATION = 200;
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);
@ -196,7 +188,6 @@ const MessageList: FC<OwnProps & StateProps> = ({
canPost,
isSynced,
isActive,
isBotForum,
shouldScrollToBottom,
// eslint-disable-next-line @typescript-eslint/no-shadow
isChatMonoforum,
@ -267,7 +258,6 @@ const MessageList: FC<OwnProps & StateProps> = ({
const memoFocusingIdRef = useRef<number>();
const isScrollTopJustUpdatedRef = useRef(false);
const shouldAnimateAppearanceRef = useRef(Boolean(lastMessage));
const scrollSnapDisabledTimerRef = useRef<number>();
const isSavedDialog = getIsSavedDialog(chatId, threadId, currentUserId);
const hasOpenChatButton = isSavedDialog && threadId !== ANONYMOUS_USER_ID;
@ -508,33 +498,6 @@ const MessageList: FC<OwnProps & StateProps> = ({
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
useEffect(() => {
if (!loadMoreAround || !isChatLoaded || isRestricted || focusingId) {
@ -628,30 +591,21 @@ const MessageList: FC<OwnProps & StateProps> = ({
isViewportNewest
&& wasMessageAdded
&& (messageIds && messageIds.length < MESSAGE_LIST_SLICE / 2)
&& !container.parentElement!.classList.contains(FORCE_MESSAGES_SCROLL_CLASS)
&& !container.parentElement!.classList.contains('force-messages-scroll')
&& forceMeasure(() => (
(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(() => {
if (container.parentElement) {
removeExtraClass(container.parentElement, FORCE_MESSAGES_SCROLL_CLASS);
removeExtraClass(container.parentElement, 'force-messages-scroll');
}
}, 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(() => {
const { scrollTop, scrollHeight, offsetHeight } = container;
const scrollOffset = scrollOffsetRef.current;
@ -768,7 +722,6 @@ const MessageList: FC<OwnProps & StateProps> = ({
!isReady && 'is-animating',
hasOpenChatButton && 'saved-dialog',
isChatProtected && 'hide-on-print',
IS_FIREFOX && NO_BOTTOM_SNAP_CLASS,
);
const hasMessages = Boolean((messageIds && messageGroups) || lastMessage);
@ -851,7 +804,6 @@ const MessageList: FC<OwnProps & StateProps> = ({
noAppearanceAnimation={!messageGroups || !shouldAnimateAppearanceRef.current}
isQuickPreview={isQuickPreview}
canPost={canPost}
isBotForum={isBotForum}
shouldScrollToBottom={shouldScrollToBottom}
onScrollDownToggle={onScrollDownToggle}
onNotchToggle={onNotchToggle}
@ -870,7 +822,6 @@ const MessageList: FC<OwnProps & StateProps> = ({
activeKey={activeKey}
shouldCleanup
onScroll={handleScroll}
onWheel={handleWheel}
onMouseDown={preventMessageInputBlur}
>
{renderContent()}
@ -986,7 +937,6 @@ export default memo(withGlobal<OwnProps>(
canTranslate,
translationLanguage,
shouldAutoTranslate,
isBotForum: chat.isBotForum,
shouldScrollToBottom,
};
},

View File

@ -37,7 +37,6 @@ import usePreviousDeprecated from '../../hooks/usePreviousDeprecated';
import useMessageObservers from './hooks/useMessageObservers';
import useScrollHooks from './hooks/useScrollHooks';
import Icon from '../common/icons/Icon';
import MiniTable, { type TableEntry } from '../common/MiniTable';
import ActionMessage from './message/ActionMessage';
import Message from './message/Message';
@ -60,7 +59,6 @@ interface OwnProps {
withUsers: boolean;
isChannelChat: boolean | undefined;
isChatMonoforum?: boolean;
isBotForum?: boolean;
isEmptyThread?: boolean;
isComments?: boolean;
noAvatars: boolean;
@ -101,7 +99,6 @@ const MessageListContent = ({
withUsers,
isChannelChat,
isChatMonoforum,
isBotForum,
noAvatars,
containerRef,
anchorIdRef,
@ -246,20 +243,6 @@ const MessageListContent = ({
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) => {
return acc + messageGroup.senderGroups.flat().length;
}, 0);
@ -465,7 +448,6 @@ const MessageListContent = ({
{shouldRenderAccountInfo
&& <MessageListAccountInfo key={`account_info_${chatId}`} chatId={chatId} hasMessages />}
{dateGroups.flat()}
{isViewportNewest && renderBotForumTopicAction()}
{withHistoryTriggers && (
<div
ref={forwardsTriggerRef}

View File

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

View File

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

View File

@ -7,9 +7,11 @@ import { STARS_ICON_PLACEHOLDER } from '../../../../config';
import { replaceWithTeact } from '../../../../util/replaceWithTeact';
import renderText from '../../../common/helpers/renderText';
import { type OldLangFn } from '../../../../hooks/useOldLang';
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') {
return lang('PaymentReceipt');
}

View File

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

View File

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

View File

@ -40,7 +40,6 @@ import {
getPinnedMediaValue,
renderMessageLink,
renderPeerLink,
renderTopicLink,
translateWithYou,
} from './helpers/messageActions';
@ -82,6 +81,7 @@ const ActionMessageText = ({
asPreview,
}: OwnProps & StateProps) => {
const {
openThread,
openTelegramLink,
openUrl,
} = getActions();
@ -231,15 +231,18 @@ const ActionMessageText = ({
const topicId = selectThreadIdFromMessage(global, message);
const topicLinkContent = (
<>
const topicLink = (
<Link
className={styles.topicLink}
onClick={() => openThread({ chatId, threadId: topicId })}
>
{iconEmojiId ? <CustomEmoji documentId={iconEmojiId} isSelectable />
: <TopicDefaultIcon topicId={topicId} title={title} iconColor={iconColor} />}
{NBSP}
{renderText(title)}
</>
</Link>
);
const topicLink = renderTopicLink(chatId, Number(topicId), topicLinkContent, asPreview);
return lang('ActionTopicCreated', { topic: topicLink }, { withNodes: true });
}
@ -250,8 +253,12 @@ const ActionMessageText = ({
const topicId = selectThreadIdFromMessage(global, message);
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
? <CustomEmoji documentId={iconEmojiId} isSelectable />
: (
@ -263,12 +270,17 @@ const ActionMessageText = ({
)}
{topicId !== GENERAL_TOPIC_ID && NBSP}
{renderText(title || currentTopic?.title || lang('ActionTopicPlaceholder'))}
</>
</Link>
);
const topicLink = renderTopicLink(chatId, Number(topicId), topicLinkContent, asPreview);
const topicPlaceholderLink = renderTopicLink(
chatId, Number(topicId), lang('ActionTopicPlaceholder'), asPreview,
const topicPlaceholderLink = (
<Link
className={styles.topicLink}
onClick={() => openThread({ chatId, threadId: topicId })}
>
{lang('ActionTopicPlaceholder')}
</Link>
);
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 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 renderKeyboardButtonText from '../composer/helpers/renderKeyboardButtonText';
import useLang from '../../../hooks/useLang';
import useOldLang from '../../../hooks/useOldLang';
import Icon from '../../common/icons/Icon';
import Button from '../../ui/Button';
@ -14,12 +15,12 @@ import Button from '../../ui/Button';
import './InlineButtons.scss';
type OwnProps = {
inlineButtons: ApiKeyboardButton[][];
onClick: (payload: ApiKeyboardButton) => void;
message: ApiMessage;
onClick: (payload: ActionPayloads['clickBotInlineButton']) => void;
};
const InlineButtons = ({ inlineButtons, onClick }: OwnProps) => {
const lang = useLang();
const InlineButtons: FC<OwnProps> = ({ message, onClick }) => {
const lang = useOldLang();
const renderIcon = (button: ApiKeyboardButton) => {
const { type } = button;
@ -65,15 +66,15 @@ const InlineButtons = ({ inlineButtons, onClick }: OwnProps) => {
const buttonTexts = useMemo(() => {
const texts: TeactNode[][] = [];
inlineButtons.forEach((row) => {
message.inlineButtons!.forEach((row) => {
texts.push(row.map((button) => renderKeyboardButtonText(lang, button)));
});
return texts;
}, [lang, inlineButtons]);
}, [lang, message.inlineButtons]);
return (
<div className="InlineButtons">
{inlineButtons.map((row, i) => (
{message.inlineButtons!.map((row, i) => (
<div className="row">
{row.map((button, j) => (
<Button
@ -81,7 +82,7 @@ const InlineButtons = ({ inlineButtons, onClick }: OwnProps) => {
ripple
disabled={button.type === 'unsupported' || (button.type === 'suggestedMessage' && button.disabled)}
onClick={() => onClick(button)}
onClick={() => onClick({ chatId: message.chatId, messageId: message.id, button })}
>
{renderIcon(button)}
<span className="inline-button-text">

View File

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

View File

@ -96,7 +96,6 @@ export function renderPeerLink(peerId: string | undefined, text: string, asPrevi
getActions().openChat({ id: peerId });
}}
// box-decoration-break: clone; is broken when child has `dir` attribute
// https://bugs.webkit.org/show_bug.cgi?id=296990
withMultilineFix={IS_SAFARI}
>
{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 {
const isMissed = action.reason === 'missed';
const isCancelled = action.reason === 'busy' || action.duration === undefined;

View File

@ -42,15 +42,14 @@ export type TransitionProps = {
slideClassName?: string;
withSwipeControl?: boolean;
isBlockingAnimation?: boolean;
onStart?: NoneToVoidFunction;
onStop?: NoneToVoidFunction;
onScroll?: NoneToVoidFunction;
onMouseDown?: (e: React.MouseEvent<HTMLDivElement>) => void;
children: React.ReactNode | ChildrenFn;
'data-tauri-drag-region'?: true;
contentSelector?: string;
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;
@ -88,15 +87,14 @@ function Transition({
slideClassName,
withSwipeControl,
isBlockingAnimation,
children,
'data-tauri-drag-region': dataTauriDragRegion,
contentSelector,
restoreHeightKey,
onStart,
onStop,
onScroll,
onMouseDown,
onWheel,
children,
'data-tauri-drag-region': dataTauriDragRegion,
contentSelector,
restoreHeightKey,
}: TransitionProps) {
const currentKeyRef = useRef<number>();
// No need for a container to update on change
@ -413,7 +411,6 @@ function Transition({
data-tauri-drag-region={dataTauriDragRegion}
onScroll={onScroll}
onMouseDown={onMouseDown}
onWheel={onWheel}
>
{contents}
</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 ANONYMOUS_USER_ID = '2666000';
export const RESTRICTED_EMOJI_SET_ID = '7173162320003080';
export const LOCAL_MESSAGES_LIMIT = 1e6; // 1M
export const CHANNEL_ID_BASE = 10n ** 12n;
export const DEFAULT_GIF_SEARCH_BOT_USERNAME = 'gif';
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> => {
if (selectIsCurrentUserFrozen(global)) return;
const { chatId, force } = payload;
if (selectIsCurrentUserFrozen(global)) {
return;
}
const chat = selectChat(global, chatId);
if (!chat) return;

View File

@ -148,7 +148,6 @@ import {
selectTabState,
selectThreadIdFromMessage,
selectThreadInfo,
selectThreadParam,
selectTopic,
selectTranslationLanguage,
selectUser,
@ -424,16 +423,6 @@ addActionHandler('sendMessage', async (global, actions, payload): Promise<void>
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 = {
...payload,
chat,
@ -453,24 +442,6 @@ addActionHandler('sendMessage', async (global, actions, payload): Promise<void>
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 isGrouped = !isSingle && payload.shouldGroupMessages;
const localMessages: SendMessageParams[] = [];
@ -1262,7 +1233,6 @@ addActionHandler('markMessageListRead', (global, actions, payload): ActionReturn
const viewportIds = selectViewportIds(global, chatId, threadId, tabId);
const minId = selectFirstUnreadId(global, chatId, threadId);
const topic = selectTopic(global, chatId, threadId);
if (threadId !== MAIN_THREAD_ID && !chat.isForum) {
global = updateThreadInfo(global, chatId, threadId, {
@ -1271,7 +1241,7 @@ addActionHandler('markMessageListRead', (global, actions, payload): ActionReturn
return global;
}
if (!viewportIds || !minId || (!chat.unreadCount && !topic?.unreadCount)) {
if (!viewportIds || !minId || !chat.unreadCount) {
return global;
}
@ -1280,17 +1250,17 @@ addActionHandler('markMessageListRead', (global, actions, payload): ActionReturn
return global;
}
const topic = selectTopic(global, chatId, threadId);
if (chat.isForum && topic) {
global = updateThreadInfo(global, chatId, threadId, {
lastReadInboxMessageId: maxId,
});
const newTopicUnreadCount = Math.max(0, topic.unreadCount - readCount);
if (newTopicUnreadCount === 0 && !chat.isBotForum && chat.unreadCount) {
if (newTopicUnreadCount === 0) {
global = updateChat(global, chatId, {
unreadCount: Math.max(0, chat.unreadCount - 1),
});
}
return updateTopic(global, chatId, Number(threadId), {
unreadCount: newTopicUnreadCount,
});
@ -1298,7 +1268,7 @@ addActionHandler('markMessageListRead', (global, actions, payload): ActionReturn
return updateChat(global, chatId, {
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();
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
? global.serviceNotifications.filter(({ isDeleted }) => !isDeleted).map(({ message }) => message)
: typingDraftMessages;
: [];
const allMessages = ([] as ApiMessage[]).concat(messages, localMessages);
const byId = buildCollectionByKey(allMessages, 'id');
const ids = Object.keys(byId).map(Number);

View File

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

View File

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

View File

@ -20,15 +20,9 @@ import { getMessageKey, isLocalMessageId } from '../../../util/keys/messageKey';
import { notifyAboutMessage } from '../../../util/notifications';
import { onTickEnd } from '../../../util/schedulers';
import { getServerTime } from '../../../util/serverTime';
import { callApi } from '../../../api/gramjs';
import {
addPaidReaction,
checkIfHasUnreadReactions,
createApiMessageFromTypingDraft,
getIsSavedDialog,
getMessageContent,
getMessageText,
isActionMessage,
checkIfHasUnreadReactions, getIsSavedDialog, getMessageContent, getMessageText, isActionMessage,
isMessageLocal,
} from '../../helpers';
import { getMessageReplyInfo, getStoryReplyInfo } from '../../helpers/replies';
@ -91,11 +85,9 @@ import {
selectScheduledIds,
selectScheduledMessage,
selectTabState,
selectThread,
selectThreadByMessage,
selectThreadIdFromMessage,
selectThreadInfo,
selectThreadParam,
selectTopic,
selectTopicFromMessage,
selectViewportIds,
@ -186,14 +178,6 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
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);
// 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, []);
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();
}
const isViewportNewest = selectIsViewportNewest(global, chatId, threadId, tabId);
if (isViewportNewest) {
return;
}
actions.loadViewportMessages({
chatId,
threadId,

View File

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

View File

@ -9,8 +9,7 @@ import type {
ApiTypeStory,
} from '../../api/types';
import type {
ApiFormattedText,
ApiPoll, ApiReplyInfo, ApiWebPage, MediaContainer, StatefulMediaContent,
ApiPoll, ApiWebPage, MediaContainer, StatefulMediaContent,
} from '../../api/types/messages';
import type { ThreadId } from '../../types';
import type { LangFn } from '../../util/localization';
@ -18,7 +17,6 @@ import type { GlobalState } from '../types';
import { ApiMessageEntityTypes, MAIN_THREAD_ID } from '../../api/types';
import {
LOCAL_MESSAGES_LIMIT,
LOTTIE_STICKER_MIME_TYPE,
RE_LINK_TEMPLATE,
SERVICE_NOTIFICATIONS_USER_ID,
@ -40,11 +38,6 @@ import { getMainUsername } from './users';
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) {
const parts = ['message', messageId.toString().replace('.', '-'), index].filter(Boolean);
return parts.join('-');
@ -511,38 +504,3 @@ export function getSuggestedChangesActionText(
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 {
ApiFormattedText,
ApiMessage, ApiPoll, ApiPollResult, ApiQuickReply, ApiSponsoredMessage, ApiThreadInfo,
ApiWebPage,
ApiWebPageFull,
@ -47,7 +46,6 @@ import {
selectTabState,
selectThreadIdFromMessage,
selectThreadInfo,
selectThreadParam,
selectViewportIds,
selectWebPage,
} 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 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;
return chat.isBotForum
|| chat.isCreator
return chat.isCreator
|| getHasAdminRight(chat, 'deleteMessages')
|| (chat.isForum
&& selectCanDeleteOwnerTopic(global, chat.id, topicId));
@ -1560,7 +1559,7 @@ export function selectTopicLink<T extends GlobalState>(
global: T, chatId: string, topicId?: ThreadId,
) {
const chat = selectChat(global, chatId);
if (!chat || !chat.isForum || chat.isBotForum) {
if (!chat || !chat?.isForum) {
return undefined;
}

View File

@ -22,6 +22,7 @@ import type {
ApiInputSavedStarGift,
ApiInputSuggestedPostInfo,
ApiKeyboardButton,
ApiKeyboardButtonSuggestedMessage,
ApiLimitTypeWithModal,
ApiMessage,
ApiMessageEntity,
@ -59,7 +60,6 @@ import type {
ApiUser,
ApiVideo,
BotsPrivacyType,
KeyboardButtonSuggestedMessage,
LinkContext,
PrivacyVisibility,
} from '../../api/types';
@ -2084,7 +2084,7 @@ export interface ActionPayloads {
clickSuggestedMessageButton: {
chatId: string;
messageId: number;
button: KeyboardButtonSuggestedMessage;
button: ApiKeyboardButtonSuggestedMessage;
} & WithTabId;
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',
'translations.telegram.org',
],
typingDraftTtl: 10,
};

View File

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

View File

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

Binary file not shown.

Binary file not shown.

View File

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

View File

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

View File

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