Revert "Revert "Support Bot Forums (#6407)""
This reverts commit fe69e6b50d046f855d3335bc940aebedda840403.
This commit is contained in:
parent
75343a0e3f
commit
77173b21d8
1
src/@types/global.d.ts
vendored
1
src/@types/global.d.ts
vendored
@ -175,7 +175,6 @@ 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>[];
|
||||
|
||||
@ -118,6 +118,7 @@ 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;
|
||||
}
|
||||
|
||||
@ -241,6 +242,7 @@ 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 {
|
||||
|
||||
@ -89,6 +89,7 @@ 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,
|
||||
@ -113,7 +114,8 @@ function buildApiChatFieldsFromPeerEntity(
|
||||
profileColor,
|
||||
isJoinToSend: channel?.joinToSend,
|
||||
isJoinRequest: channel?.joinRequest,
|
||||
isForum: channel?.forum,
|
||||
isForum,
|
||||
isBotForum: user?.botForumView,
|
||||
isMonoforum: channel?.monoforum,
|
||||
linkedMonoforumId: channel?.linkedMonoforumId !== undefined
|
||||
? buildApiPeerId(channel.linkedMonoforumId, 'channel') : undefined,
|
||||
|
||||
@ -36,6 +36,7 @@ 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,
|
||||
@ -76,8 +77,6 @@ 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;
|
||||
@ -87,6 +86,10 @@ function getNextLocalMessageId(lastMessageId = 0) {
|
||||
return lastMessageId + (++localMessageCounter / LOCAL_MESSAGES_LIMIT);
|
||||
}
|
||||
|
||||
export function incrementLocalMessageCounter() {
|
||||
localMessageCounter++;
|
||||
}
|
||||
|
||||
let currentUserId!: string;
|
||||
|
||||
export function setMessageBuilderCurrentUserId(_currentUserId: string) {
|
||||
|
||||
@ -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,
|
||||
sendPaidMessagesStars, profileColor, botForumView,
|
||||
} = mtpUser;
|
||||
const hasVideoAvatar = mtpUser.photo instanceof GramJs.UserProfilePhoto ? Boolean(mtpUser.photo.hasVideo) : undefined;
|
||||
const avatarPhotoId = mtpUser.photo && buildAvatarPhotoId(mtpUser.photo);
|
||||
@ -155,6 +155,7 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined {
|
||||
color: mtpUser.color && buildApiPeerColor(mtpUser.color),
|
||||
profileColor: profileColor && buildApiPeerColor(profileColor),
|
||||
paidMessagesStars: toJSNumber(sendPaidMessagesStars),
|
||||
isBotForum: botForumView,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -16,13 +16,14 @@ import { processAffectedHistory } from '../updates/updateManager';
|
||||
import { invokeRequest } from './client';
|
||||
|
||||
export async function createTopic({
|
||||
chat, title, iconColor, iconEmojiId, sendAs,
|
||||
chat, title, iconColor, iconEmojiId, sendAs, isTitleMissing,
|
||||
}: {
|
||||
chat: ApiChat;
|
||||
title: string;
|
||||
iconColor?: number;
|
||||
iconEmojiId?: string;
|
||||
sendAs?: ApiPeer;
|
||||
isTitleMissing?: true;
|
||||
}) {
|
||||
const { id, accessHash } = chat;
|
||||
|
||||
@ -33,6 +34,7 @@ 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) {
|
||||
@ -75,9 +77,10 @@ export async function fetchTopics({
|
||||
|
||||
if (!result) return undefined;
|
||||
|
||||
const { count, orderByCreateDate } = result;
|
||||
const { 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) {
|
||||
|
||||
@ -75,6 +75,7 @@ import {
|
||||
buildLocalMessage,
|
||||
buildPreparedInlineMessage,
|
||||
buildUploadingMedia,
|
||||
incrementLocalMessageCounter,
|
||||
} from '../apiBuilders/messages';
|
||||
import { getApiChatIdFromMtpPeer } from '../apiBuilders/peers';
|
||||
import { buildApiUser, buildApiUserStatuses } from '../apiBuilders/users';
|
||||
@ -1255,23 +1256,21 @@ 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: fixedMaxId,
|
||||
maxId,
|
||||
}));
|
||||
} else if (isChannel) {
|
||||
} else if (threadId !== MAIN_THREAD_ID) {
|
||||
await invokeRequest(new GramJs.messages.ReadDiscussion({
|
||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||
msgId: Number(threadId),
|
||||
readMaxId: fixedMaxId,
|
||||
readMaxId: maxId,
|
||||
}));
|
||||
} else {
|
||||
const result = await invokeRequest(new GramJs.messages.ReadHistory({
|
||||
peer: buildInputPeer(chat.id, chat.accessHash),
|
||||
maxId: fixedMaxId,
|
||||
maxId,
|
||||
}));
|
||||
|
||||
if (result) {
|
||||
@ -2555,3 +2554,7 @@ export async function fetchPreparedInlineMessage({
|
||||
|
||||
return buildPreparedInlineMessage(result);
|
||||
}
|
||||
|
||||
export function incrementLocalMessagesCounter() {
|
||||
incrementLocalMessageCounter();
|
||||
}
|
||||
|
||||
@ -28,6 +28,7 @@ import {
|
||||
buildChatTypingStatus,
|
||||
} from '../apiBuilders/chats';
|
||||
import {
|
||||
buildApiFormattedText,
|
||||
buildApiPhoto, buildApiUsernames, buildPrivacyRules,
|
||||
} from '../apiBuilders/common';
|
||||
import { omitVirtualClassFields } from '../apiBuilders/helpers';
|
||||
@ -496,10 +497,9 @@ export function updater(update: Update) {
|
||||
sendApiUpdate({
|
||||
'@type': 'updateChatInbox',
|
||||
id: getApiChatIdFromMtpPeer(update.peer),
|
||||
chat: {
|
||||
lastReadInboxMessageId: update.maxId,
|
||||
unreadCount: update.stillUnreadCount,
|
||||
},
|
||||
lastReadInboxMessageId: update.maxId,
|
||||
unreadCount: update.stillUnreadCount,
|
||||
threadId: update.topMsgId,
|
||||
});
|
||||
} else if (update instanceof GramJs.UpdateReadHistoryOutbox) {
|
||||
sendApiUpdate({
|
||||
@ -648,22 +648,33 @@ export function updater(update: Update) {
|
||||
update instanceof GramJs.UpdateUserTyping
|
||||
|| update instanceof GramJs.UpdateChatUserTyping
|
||||
) {
|
||||
const id = update instanceof GramJs.UpdateUserTyping
|
||||
const chatId = 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,
|
||||
id: chatId,
|
||||
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,
|
||||
id: chatId,
|
||||
threadId,
|
||||
typingStatus: buildChatTypingStatus(update),
|
||||
});
|
||||
}
|
||||
|
||||
@ -57,6 +57,7 @@ export interface ApiChat {
|
||||
isForum?: boolean;
|
||||
isForumAsMessages?: true;
|
||||
isMonoforum?: boolean;
|
||||
isBotForum?: boolean;
|
||||
withForumTabs?: boolean;
|
||||
linkedMonoforumId?: string;
|
||||
areChannelMessagesAllowed?: boolean;
|
||||
|
||||
@ -489,7 +489,7 @@ export type ApiMessageEntityDefault = {
|
||||
type: Exclude<
|
||||
`${ApiMessageEntityTypes}`,
|
||||
`${ApiMessageEntityTypes.Pre}` | `${ApiMessageEntityTypes.TextUrl}` | `${ApiMessageEntityTypes.MentionName}` |
|
||||
`${ApiMessageEntityTypes.CustomEmoji}` | `${ApiMessageEntityTypes.Blockquote}` | `${ApiMessageEntityTypes.Timestamp}`
|
||||
`${ApiMessageEntityTypes.Blockquote}` | `${ApiMessageEntityTypes.CustomEmoji}` | `${ApiMessageEntityTypes.Timestamp}`
|
||||
>;
|
||||
offset: number;
|
||||
length: number;
|
||||
@ -538,15 +538,8 @@ export type ApiMessageEntityTimestamp = {
|
||||
timestamp: number;
|
||||
};
|
||||
|
||||
export type ApiMessageEntityQuoteFocus = {
|
||||
type: 'quoteFocus';
|
||||
offset: number;
|
||||
length: number;
|
||||
};
|
||||
|
||||
export type ApiMessageEntity = ApiMessageEntityDefault | ApiMessageEntityPre | ApiMessageEntityTextUrl |
|
||||
ApiMessageEntityMentionName | ApiMessageEntityCustomEmoji | ApiMessageEntityBlockquote | ApiMessageEntityTimestamp |
|
||||
ApiMessageEntityQuoteFocus;
|
||||
ApiMessageEntityMentionName | ApiMessageEntityCustomEmoji | ApiMessageEntityBlockquote | ApiMessageEntityTimestamp;
|
||||
|
||||
export enum ApiMessageEntityTypes {
|
||||
Bold = 'MessageEntityBold',
|
||||
@ -683,6 +676,8 @@ export interface ApiMessage {
|
||||
reportDeliveryUntilDate?: number;
|
||||
paidMessageStars?: number;
|
||||
restrictionReasons?: ApiRestrictionReason[];
|
||||
|
||||
isTypingDraft?: boolean; // Local field
|
||||
}
|
||||
|
||||
export interface ApiReactions {
|
||||
@ -922,13 +917,18 @@ interface ApiKeyboardButtonCopy {
|
||||
copyText: string;
|
||||
}
|
||||
|
||||
export interface ApiKeyboardButtonSuggestedMessage {
|
||||
export interface KeyboardButtonSuggestedMessage {
|
||||
type: 'suggestedMessage';
|
||||
text: string;
|
||||
buttonType: 'approve' | 'decline' | 'suggestChanges';
|
||||
disabled?: boolean;
|
||||
}
|
||||
|
||||
export interface KeyboardButtonOpenThread {
|
||||
type: 'openThread';
|
||||
text: string;
|
||||
}
|
||||
|
||||
export type ApiKeyboardButton = (
|
||||
ApiKeyboardButtonSimple
|
||||
| ApiKeyboardButtonReceipt
|
||||
@ -941,7 +941,8 @@ export type ApiKeyboardButton = (
|
||||
| ApiKeyboardButtonSimpleWebView
|
||||
| ApiKeyboardButtonUrlAuth
|
||||
| ApiKeyboardButtonCopy
|
||||
| ApiKeyboardButtonSuggestedMessage
|
||||
| KeyboardButtonSuggestedMessage
|
||||
| KeyboardButtonOpenThread
|
||||
);
|
||||
|
||||
export type ApiKeyboardButtons = ApiKeyboardButton[][];
|
||||
|
||||
@ -274,6 +274,7 @@ export interface ApiAppConfig {
|
||||
verifyAgeBotUsername?: string;
|
||||
verifyAgeCountry?: string;
|
||||
verifyAgeMin?: number;
|
||||
typingDraftTtl: number;
|
||||
contactNoteLimit?: number;
|
||||
}
|
||||
|
||||
|
||||
@ -132,7 +132,9 @@ export type ApiUpdateChatLeave = {
|
||||
export type ApiUpdateChatInbox = {
|
||||
'@type': 'updateChatInbox';
|
||||
id: string;
|
||||
chat: Partial<ApiChat>;
|
||||
threadId?: ThreadId;
|
||||
lastReadInboxMessageId: number;
|
||||
unreadCount: number;
|
||||
};
|
||||
|
||||
export type ApiUpdateChatTypingStatus = {
|
||||
@ -142,6 +144,14 @@ 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;
|
||||
@ -878,7 +888,7 @@ export type ApiUpdate = (
|
||||
ApiUpdateRecentStickers | ApiUpdateSavedGifs | ApiUpdateNewScheduledMessage | ApiUpdateMoveStickerSetToTop |
|
||||
ApiUpdateScheduledMessageSendSucceeded | ApiUpdateScheduledMessage | ApiUpdateStarPaymentStateCompleted |
|
||||
ApiUpdateDeleteScheduledMessages | ApiUpdateResetMessages | ApiUpdateMessageTranslations |
|
||||
ApiUpdateFailedMessageTranslations | ApiUpdateWebPage |
|
||||
ApiUpdateFailedMessageTranslations | ApiUpdateWebPage | ApiUpdateChatTypingDraft |
|
||||
ApiUpdateTwoFaError | ApiUpdateTwoFaStateWaitCode | ApiUpdateWebViewResultSent |
|
||||
ApiUpdateDefaultNotifySettings | ApiUpdatePeerNotifySettings | ApiUpdatePeerBlocked | ApiUpdatePrivacy |
|
||||
ApiUpdateServerTimeOffset | ApiUpdateMessageReactions | ApiUpdateSavedReactionTags |
|
||||
|
||||
@ -47,6 +47,7 @@ export interface ApiUser {
|
||||
botActiveUsers?: number;
|
||||
botVerificationIconId?: string;
|
||||
paidMessagesStars?: number;
|
||||
isBotForum?: boolean;
|
||||
}
|
||||
|
||||
export interface ApiUserFullInfo {
|
||||
|
||||
1
src/assets/font-icons/topic-new.svg
Normal file
1
src/assets/font-icons/topic-new.svg
Normal file
@ -0,0 +1 @@
|
||||
<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>
|
||||
|
After Width: | Height: | Size: 1.2 KiB |
@ -2301,6 +2301,10 @@
|
||||
"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";
|
||||
@ -2310,6 +2314,8 @@
|
||||
"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";
|
||||
|
||||
@ -1705,7 +1705,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
return lang('ComposerPlaceholderAnonymous');
|
||||
}
|
||||
|
||||
if (chat?.isForum && chat?.isForumAsMessages && threadId === MAIN_THREAD_ID) {
|
||||
if (chat?.isForum && !chat.isBotForum && chat.isForumAsMessages && threadId === MAIN_THREAD_ID) {
|
||||
return replyToTopic
|
||||
? lang('ComposerPlaceholderTopic', { topic: replyToTopic.title })
|
||||
: lang('ComposerPlaceholderTopicGeneral');
|
||||
|
||||
@ -1,10 +1,8 @@
|
||||
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, ApiThreadInfo, ApiTopic, ApiTypingStatus, ApiUser,
|
||||
ApiChat, ApiTopic, ApiTypingStatus, ApiUser,
|
||||
} from '../../api/types';
|
||||
import type { IconName } from '../../types/icons';
|
||||
import { MediaViewerOrigin, type StoryViewerOrigin, type ThreadId } from '../../types';
|
||||
@ -21,7 +19,6 @@ import {
|
||||
selectChatOnlineCount,
|
||||
selectIsChatRestricted,
|
||||
selectMonoforumChannel,
|
||||
selectThreadInfo,
|
||||
selectThreadMessagesCount,
|
||||
selectTopic,
|
||||
selectUser,
|
||||
@ -68,12 +65,11 @@ type OwnProps = {
|
||||
isSavedDialog?: boolean;
|
||||
withMonoforumStatus?: boolean;
|
||||
onClick?: VoidFunction;
|
||||
onEmojiStatusClick?: NoneToVoidFunction;
|
||||
onEmojiStatusClick?: VoidFunction;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
chat?: ApiChat;
|
||||
threadInfo?: ApiThreadInfo;
|
||||
topic?: ApiTopic;
|
||||
onlineCount?: number;
|
||||
areMessagesLoaded: boolean;
|
||||
@ -82,7 +78,7 @@ type StateProps = {
|
||||
monoforumChannel?: ApiChat;
|
||||
};
|
||||
|
||||
const GroupChatInfo: FC<OwnProps & StateProps> = ({
|
||||
const GroupChatInfo = ({
|
||||
typingStatus,
|
||||
className,
|
||||
statusIcon,
|
||||
@ -95,7 +91,6 @@ const GroupChatInfo: FC<OwnProps & StateProps> = ({
|
||||
withFullInfo,
|
||||
withUpdatingStatus,
|
||||
withChatType,
|
||||
threadInfo,
|
||||
noRtl,
|
||||
chat: realChat,
|
||||
onlineCount,
|
||||
@ -113,7 +108,7 @@ const GroupChatInfo: FC<OwnProps & StateProps> = ({
|
||||
monoforumChannel,
|
||||
onClick,
|
||||
onEmojiStatusClick,
|
||||
}) => {
|
||||
}: OwnProps & StateProps) => {
|
||||
const {
|
||||
loadFullChat,
|
||||
openMediaViewer,
|
||||
@ -126,7 +121,7 @@ const GroupChatInfo: FC<OwnProps & StateProps> = ({
|
||||
const lang = useLang();
|
||||
|
||||
const isSuperGroup = chat && isChatSuperGroup(chat);
|
||||
const isTopic = Boolean(chat?.isForum && threadInfo && topic);
|
||||
const isTopic = Boolean(chat?.isForum && topic);
|
||||
const { id: chatId, isMin } = chat || {};
|
||||
const isRestricted = selectIsChatRestricted(getGlobal(), chatId!);
|
||||
|
||||
@ -204,7 +199,7 @@ const GroupChatInfo: FC<OwnProps & StateProps> = ({
|
||||
activeKey={messagesCount !== undefined ? 1 : 2}
|
||||
className="message-count-transition"
|
||||
>
|
||||
{messagesCount !== undefined && oldLang('messages', messagesCount, 'i')}
|
||||
{messagesCount !== undefined ? oldLang('messages', messagesCount, 'i') : oldLang('lng_forum_no_messages')}
|
||||
</Transition>
|
||||
</span>
|
||||
);
|
||||
@ -290,7 +285,6 @@ const GroupChatInfo: FC<OwnProps & StateProps> = ({
|
||||
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;
|
||||
@ -300,7 +294,6 @@ export default memo(withGlobal<OwnProps>(
|
||||
|
||||
return {
|
||||
chat,
|
||||
threadInfo,
|
||||
onlineCount,
|
||||
topic,
|
||||
areMessagesLoaded,
|
||||
|
||||
@ -12,9 +12,12 @@ 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;
|
||||
@ -36,6 +39,7 @@ interface OwnProps {
|
||||
isInSelectMode?: boolean;
|
||||
canBeEmpty?: boolean;
|
||||
maxTimestamp?: number;
|
||||
shouldAnimateTyping?: boolean;
|
||||
}
|
||||
|
||||
const MIN_CUSTOM_EMOJIS_FOR_SHARED_CANVAS = 3;
|
||||
@ -61,6 +65,7 @@ function MessageText({
|
||||
canBeEmpty,
|
||||
maxTimestamp,
|
||||
threadId,
|
||||
shouldAnimateTyping,
|
||||
}: OwnProps) {
|
||||
const sharedCanvasRef = useRef<HTMLCanvasElement>();
|
||||
const sharedCanvasHqRef = useRef<HTMLCanvasElement>();
|
||||
@ -107,37 +112,48 @@ 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 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,
|
||||
}),
|
||||
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),
|
||||
].flat().filter(Boolean)}
|
||||
</>
|
||||
);
|
||||
|
||||
@ -1,19 +1,25 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import { memo, useEffect, useMemo } from '../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type {
|
||||
ApiChatMember, ApiTypingStatus, ApiUser, ApiUserStatus,
|
||||
ApiChatMember, ApiTopic, ApiTypingStatus, ApiUser, ApiUserStatus,
|
||||
} from '../../api/types';
|
||||
import type { CustomPeer, StoryViewerOrigin } from '../../types';
|
||||
import type { CustomPeer, StoryViewerOrigin, ThreadId } from '../../types';
|
||||
import type { IconName } from '../../types/icons';
|
||||
import { MediaViewerOrigin } from '../../types';
|
||||
|
||||
import {
|
||||
getMainUsername, getUserStatus, isSystemBot, isUserOnline,
|
||||
} from '../../global/helpers';
|
||||
import { selectChatMessages, selectUser, selectUserStatus } from '../../global/selectors';
|
||||
import {
|
||||
selectChatMessages,
|
||||
selectThreadMessagesCount,
|
||||
selectTopic,
|
||||
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';
|
||||
@ -22,15 +28,17 @@ 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';
|
||||
|
||||
type OwnProps = {
|
||||
userId?: string;
|
||||
customPeer?: CustomPeer;
|
||||
const TOPIC_ICON_SIZE = 2.5 * REM;
|
||||
|
||||
type BaseOwnProps = {
|
||||
typingStatus?: ApiTypingStatus;
|
||||
avatarSize?: 'tiny' | 'small' | 'medium' | 'large' | 'jumbo';
|
||||
forceShowSelf?: boolean;
|
||||
@ -52,25 +60,39 @@ type OwnProps = {
|
||||
noRtl?: boolean;
|
||||
adminMember?: ApiChatMember;
|
||||
isSavedDialog?: boolean;
|
||||
noAvatar?: boolean;
|
||||
className?: string;
|
||||
onEmojiStatusClick?: NoneToVoidFunction;
|
||||
iconElement?: React.ReactNode;
|
||||
rightElement?: React.ReactNode;
|
||||
onClick?: VoidFunction;
|
||||
onEmojiStatusClick?: VoidFunction;
|
||||
};
|
||||
|
||||
type StateProps =
|
||||
{
|
||||
user?: ApiUser;
|
||||
userStatus?: ApiUserStatus;
|
||||
self?: ApiUser;
|
||||
isSavedMessages?: boolean;
|
||||
areMessagesLoaded: boolean;
|
||||
isSynced?: boolean;
|
||||
};
|
||||
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;
|
||||
};
|
||||
|
||||
const UPDATE_INTERVAL = 1000 * 60; // 1 min
|
||||
|
||||
const PrivateChatInfo: FC<OwnProps & StateProps> = ({
|
||||
const PrivateChatInfo = ({
|
||||
userId,
|
||||
customPeer,
|
||||
typingStatus,
|
||||
avatarSize = 'medium',
|
||||
@ -91,6 +113,8 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
|
||||
user,
|
||||
userStatus,
|
||||
self,
|
||||
topic,
|
||||
messagesCount,
|
||||
isSavedMessages,
|
||||
isSavedDialog,
|
||||
areMessagesLoaded,
|
||||
@ -98,11 +122,13 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
|
||||
ripple,
|
||||
className,
|
||||
storyViewerOrigin,
|
||||
noAvatar,
|
||||
isSynced,
|
||||
onEmojiStatusClick,
|
||||
iconElement,
|
||||
rightElement,
|
||||
}) => {
|
||||
onClick,
|
||||
onEmojiStatusClick,
|
||||
}: OwnProps & StateProps) => {
|
||||
const {
|
||||
loadFullUser,
|
||||
openMediaViewer,
|
||||
@ -112,8 +138,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
|
||||
const oldLang = useOldLang();
|
||||
const lang = useLang();
|
||||
|
||||
const { id: userId } = user || {};
|
||||
|
||||
const isTopic = Boolean(user?.isBotForum && topic);
|
||||
const hasAvatarMediaViewer = withMediaViewer && !isSavedMessages;
|
||||
|
||||
useEffect(() => {
|
||||
@ -127,11 +152,11 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const handleAvatarViewerOpen = useLastCallback(
|
||||
(e: React.MouseEvent<HTMLDivElement, MouseEvent>, hasMedia: boolean) => {
|
||||
if (user && hasMedia) {
|
||||
if (hasMedia) {
|
||||
e.stopPropagation();
|
||||
openMediaViewer({
|
||||
isAvatarView: true,
|
||||
chatId: user.id,
|
||||
chatId: userId,
|
||||
mediaIndex: 0,
|
||||
origin: avatarSize === 'jumbo' ? MediaViewerOrigin.ProfileAvatar : MediaViewerOrigin.MiddleHeaderAvatar,
|
||||
});
|
||||
@ -179,6 +204,21 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
|
||||
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;
|
||||
}
|
||||
@ -198,6 +238,12 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
|
||||
: undefined;
|
||||
|
||||
function renderNameTitle() {
|
||||
if (isTopic) {
|
||||
return (
|
||||
<h3 dir="auto" className="fullName">{renderText(topic!.title)}</h3>
|
||||
);
|
||||
}
|
||||
|
||||
if (customTitle) {
|
||||
return (
|
||||
<div className="info-name-title">
|
||||
@ -230,7 +276,11 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={buildClassName('ChatInfo', className)} dir={!noRtl && lang.isRtl ? 'rtl' : undefined}>
|
||||
<div
|
||||
className={buildClassName('ChatInfo', className)}
|
||||
dir={!noRtl && lang.isRtl ? 'rtl' : undefined}
|
||||
onClick={onClick}
|
||||
>
|
||||
{isSavedDialog && self && (
|
||||
<Avatar
|
||||
key="saved-messages"
|
||||
@ -240,18 +290,27 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
|
||||
className="saved-dialog-avatar"
|
||||
/>
|
||||
)}
|
||||
<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}
|
||||
/>
|
||||
{!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}
|
||||
/>
|
||||
)}
|
||||
<div className="info">
|
||||
{renderNameTitle()}
|
||||
{(status || (!isSavedMessages && !noStatusOrTyping)) && renderStatusOrTyping()}
|
||||
@ -263,13 +322,16 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { userId, forceShowSelf }): Complete<StateProps> => {
|
||||
(global, { userId, threadId, 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));
|
||||
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;
|
||||
|
||||
return {
|
||||
user,
|
||||
@ -278,6 +340,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
areMessagesLoaded,
|
||||
self,
|
||||
isSynced,
|
||||
topic,
|
||||
messagesCount,
|
||||
};
|
||||
},
|
||||
)(PrivateChatInfo));
|
||||
|
||||
71
src/components/common/TypingWrapper.tsx
Normal file
71
src/components/common/TypingWrapper.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
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);
|
||||
@ -1,5 +1,4 @@
|
||||
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';
|
||||
@ -10,7 +9,6 @@ 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';
|
||||
|
||||
@ -301,6 +299,12 @@ 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[] = [];
|
||||
|
||||
@ -761,7 +765,9 @@ function handleHashtagClick(hashtag?: string, username?: string) {
|
||||
function handleCodeClick(e: React.MouseEvent<HTMLElement>) {
|
||||
copyTextToClipboard(e.currentTarget.innerText);
|
||||
getActions().showNotification({
|
||||
message: oldTranslate('TextCopied'),
|
||||
message: {
|
||||
key: 'TextCopied',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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/ForumPanel';
|
||||
import ForumPanel from './main/forum/ForumPanel';
|
||||
|
||||
import './ArchivedChats.scss';
|
||||
|
||||
|
||||
@ -103,7 +103,7 @@
|
||||
font-size: 0.875rem !important;
|
||||
}
|
||||
|
||||
.selected {
|
||||
.selected:not(.onAvatar) {
|
||||
.badge:not(.pinned) {
|
||||
color: var(--color-chat-active);
|
||||
background: var(--color-white);
|
||||
|
||||
@ -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 './ForumPanel';
|
||||
import ForumPanel from './forum/ForumPanel';
|
||||
import LeftMainHeader from './LeftMainHeader';
|
||||
|
||||
import './LeftMain.scss';
|
||||
|
||||
87
src/components/left/main/forum/AllMessagesTopic.tsx
Normal file
87
src/components/left/main/forum/AllMessagesTopic.tsx
Normal file
@ -0,0 +1,87 @@
|
||||
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));
|
||||
@ -1,19 +1,20 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import { memo, useCallback } from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
import { memo } 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 useOldLang from '../../../hooks/useOldLang';
|
||||
import useAppLayout from '../../../../hooks/useAppLayout';
|
||||
import useLang from '../../../../hooks/useLang';
|
||||
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||
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';
|
||||
|
||||
@ -28,26 +29,27 @@ type StateProps = {
|
||||
|
||||
const ICON_SIZE = 7 * REM;
|
||||
|
||||
const EmptyForum: FC<OwnProps & StateProps> = ({
|
||||
const EmptyForum = ({
|
||||
chatId, animatedEmoji, canManageTopics,
|
||||
}) => {
|
||||
}: OwnProps & StateProps) => {
|
||||
const { openCreateTopicPanel } = getActions();
|
||||
|
||||
const lang = useOldLang();
|
||||
const lang = useLang();
|
||||
const oldLang = useOldLang();
|
||||
const { isMobile } = useAppLayout();
|
||||
|
||||
const handleCreateTopic = useCallback(() => {
|
||||
const handleCreateTopic = useLastCallback(() => {
|
||||
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">{lang('ChatList.EmptyTopicsTitle')}</h3>
|
||||
<h3 className={styles.title} dir="auto">{oldLang('ChatList.EmptyTopicsTitle')}</h3>
|
||||
<p className={buildClassName(styles.description, styles.centered)} dir="auto">
|
||||
{lang('ChatList.EmptyTopicsDescription')}
|
||||
{oldLang('ChatList.EmptyTopicsDescription')}
|
||||
</p>
|
||||
{canManageTopics && (
|
||||
<Button
|
||||
@ -57,7 +59,7 @@ const EmptyForum: FC<OwnProps & StateProps> = ({
|
||||
isRtl={lang.isRtl}
|
||||
>
|
||||
<div className={styles.buttonText}>
|
||||
{lang('ChatList.EmptyTopicsCreate')}
|
||||
{oldLang('ChatList.EmptyTopicsCreate')}
|
||||
</div>
|
||||
</Button>
|
||||
)}
|
||||
@ -1,19 +1,18 @@
|
||||
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,
|
||||
@ -21,29 +20,32 @@ 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';
|
||||
} 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';
|
||||
|
||||
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 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 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 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 EmptyForum from './EmptyForum';
|
||||
import Topic from './Topic';
|
||||
|
||||
@ -66,17 +68,17 @@ type StateProps = {
|
||||
|
||||
const INTERSECTION_THROTTLE = 200;
|
||||
|
||||
const ForumPanel: FC<OwnProps & StateProps> = ({
|
||||
const ForumPanel = ({
|
||||
chat,
|
||||
currentTopicId,
|
||||
isOpen,
|
||||
isHidden,
|
||||
topicsInfo,
|
||||
withInterfaceAnimations,
|
||||
onTopicSearch,
|
||||
onCloseAnimationEnd,
|
||||
onOpenAnimationStart,
|
||||
withInterfaceAnimations,
|
||||
}) => {
|
||||
}: OwnProps & StateProps) => {
|
||||
const {
|
||||
closeForumPanel, openChatWithInfo, loadTopics,
|
||||
} = getActions();
|
||||
@ -95,7 +97,7 @@ const ForumPanel: FC<OwnProps & StateProps> = ({
|
||||
}, [topicsInfo, chatId]);
|
||||
|
||||
const [isScrolled, setIsScrolled] = useState(false);
|
||||
const lang = useOldLang();
|
||||
const lang = useLang();
|
||||
|
||||
const handleClose = useLastCallback(() => {
|
||||
closeForumPanel();
|
||||
@ -122,13 +124,17 @@ const ForumPanel: FC<OwnProps & StateProps> = ({
|
||||
});
|
||||
|
||||
const orderedIds = useMemo(() => {
|
||||
return topicsInfo
|
||||
const ids = topicsInfo
|
||||
? getOrderedTopics(
|
||||
Object.values(topicsInfo.topicsById),
|
||||
topicsInfo.orderedPinnedTopicIds,
|
||||
).map(({ id }) => id)
|
||||
: [];
|
||||
}, [topicsInfo]);
|
||||
|
||||
if (!chat?.isBotForum) return ids;
|
||||
|
||||
return [MAIN_THREAD_ID, ...ids];
|
||||
}, [chat?.isBotForum, topicsInfo]);
|
||||
|
||||
const { orderDiffById, getAnimationType, onReorderAnimationEnd } = useOrderDiff(orderedIds, chat?.id);
|
||||
|
||||
@ -197,23 +203,37 @@ const ForumPanel: FC<OwnProps & StateProps> = ({
|
||||
function renderTopics() {
|
||||
const viewportOffset = orderedIds.indexOf(viewportIds![0]);
|
||||
|
||||
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}
|
||||
/>
|
||||
));
|
||||
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}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
const isLoading = topicsInfo === undefined;
|
||||
|
||||
if (!chat) return undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
@ -236,7 +256,14 @@ const ForumPanel: FC<OwnProps & StateProps> = ({
|
||||
<Icon name="close" />
|
||||
</Button>
|
||||
|
||||
{chat && (
|
||||
{isUserId(chat.id) ? (
|
||||
<PrivateChatInfo
|
||||
noAvatar
|
||||
className={styles.info}
|
||||
userId={chat.id}
|
||||
onClick={handleToggleChatInfo}
|
||||
/>
|
||||
) : (
|
||||
<GroupChatInfo
|
||||
noAvatar
|
||||
className={styles.info}
|
||||
@ -245,21 +272,18 @@ const ForumPanel: FC<OwnProps & StateProps> = ({
|
||||
/>
|
||||
)}
|
||||
|
||||
{chat
|
||||
&& (
|
||||
<HeaderActions
|
||||
chatId={chat.id}
|
||||
threadId={MAIN_THREAD_ID}
|
||||
messageListType="thread"
|
||||
canExpandActions={false}
|
||||
isForForum
|
||||
isMobile={isMobile}
|
||||
onTopicSearch={onTopicSearch}
|
||||
/>
|
||||
)}
|
||||
<HeaderActions
|
||||
chatId={chat.id}
|
||||
threadId={MAIN_THREAD_ID}
|
||||
messageListType="thread"
|
||||
canExpandActions={false}
|
||||
isForForum
|
||||
isMobile={isMobile}
|
||||
onTopicSearch={onTopicSearch}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{chat && <GroupCallTopPane chatId={chat.id} />}
|
||||
{!isUserId(chat.id) && <GroupCallTopPane chatId={chat.id} />}
|
||||
|
||||
<div className={styles.notch} />
|
||||
|
||||
@ -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 });
|
||||
setViewForumAsMessages({ chatId, isEnabled: false });
|
||||
if (!chat.isBotForum && !chat.isMonoforum) 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 = Boolean(threadInfo?.lastReadInboxMessageId);
|
||||
const wasTopicOpened = chat?.isBotForum || Boolean(threadInfo?.lastReadInboxMessageId);
|
||||
const topics = selectTopics(global, chatId);
|
||||
|
||||
const { chatId: currentChatId, threadId: currentThreadId } = selectCurrentMessageList(global) || {};
|
||||
@ -5,6 +5,7 @@ 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';
|
||||
|
||||
@ -48,11 +49,11 @@ export default function useTopicContextActions({
|
||||
openQuickPreview,
|
||||
} = getActions();
|
||||
|
||||
const canToggleClosed = getCanManageTopic(chat, topic);
|
||||
const canToggleClosed = getCanManageTopic(chat, topic) && !chat.isBotForum;
|
||||
const canTogglePinned = chat.isCreator || getHasAdminRight(chat, 'manageTopics');
|
||||
|
||||
const actionOpenInNewTab = IS_OPEN_IN_NEW_TAB_SUPPORTED && {
|
||||
title: 'Open in new tab',
|
||||
title: IS_TAURI ? lang('ChatListOpenInNewWindow') : lang('ChatListOpenInNewTab'),
|
||||
icon: 'open-in-new-tab',
|
||||
handler: () => {
|
||||
openChatInNewTab({ chatId: chat.id, threadId: topicId });
|
||||
|
||||
@ -863,7 +863,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
const canGift = selectCanGift(global, chatId);
|
||||
|
||||
const topic = selectTopic(global, chatId, threadId);
|
||||
const canCreateTopic = chat.isForum && (
|
||||
// Disable manual creation for bot forums
|
||||
const canCreateTopic = chat.isForum && !chat.isBotForum && (
|
||||
chat.isCreator || !isUserRightBanned(chat, 'manageTopics') || getHasAdminRight(chat, 'manageTopics')
|
||||
);
|
||||
const canEditTopic = topic && getCanManageTopic(chat, topic);
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
.MessageList {
|
||||
--action-message-bg: var(--pattern-color);
|
||||
|
||||
scroll-snap-type: y proximity;
|
||||
|
||||
overflow-x: hidden;
|
||||
overflow-y: scroll;
|
||||
flex: 1;
|
||||
@ -36,6 +38,10 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
&.no-bottom-snap {
|
||||
scroll-snap-type: none;
|
||||
}
|
||||
|
||||
.messages-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -51,6 +57,10 @@
|
||||
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
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
MESSAGE_LIST_SLICE,
|
||||
SERVICE_NOTIFICATIONS_USER_ID,
|
||||
} from '../../config';
|
||||
import { forceMeasure, requestForcedReflow, requestMeasure } from '../../lib/fasterdom/fasterdom';
|
||||
import { forceMeasure, requestForcedReflow, requestMeasure, requestMutation } from '../../lib/fasterdom/fasterdom';
|
||||
import {
|
||||
getIsSavedDialog,
|
||||
getMessageHtmlId,
|
||||
@ -54,6 +54,7 @@ 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';
|
||||
@ -145,6 +146,7 @@ type StateProps = {
|
||||
translationLanguage?: string;
|
||||
shouldAutoTranslate?: boolean;
|
||||
isActive?: boolean;
|
||||
isBotForum?: boolean;
|
||||
shouldScrollToBottom?: boolean;
|
||||
};
|
||||
|
||||
@ -166,13 +168,19 @@ 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);
|
||||
|
||||
@ -188,6 +196,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
||||
canPost,
|
||||
isSynced,
|
||||
isActive,
|
||||
isBotForum,
|
||||
shouldScrollToBottom,
|
||||
// eslint-disable-next-line @typescript-eslint/no-shadow
|
||||
isChatMonoforum,
|
||||
@ -258,6 +267,7 @@ 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;
|
||||
@ -498,6 +508,33 @@ 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) {
|
||||
@ -591,21 +628,30 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
||||
isViewportNewest
|
||||
&& wasMessageAdded
|
||||
&& (messageIds && messageIds.length < MESSAGE_LIST_SLICE / 2)
|
||||
&& !container.parentElement!.classList.contains('force-messages-scroll')
|
||||
&& !container.parentElement!.classList.contains(FORCE_MESSAGES_SCROLL_CLASS)
|
||||
&& forceMeasure(() => (
|
||||
(container.firstElementChild as HTMLDivElement).clientHeight <= container.offsetHeight * 2
|
||||
))
|
||||
) {
|
||||
addExtraClass(container.parentElement!, 'force-messages-scroll');
|
||||
container.parentElement!.classList.add('force-messages-scroll');
|
||||
addExtraClass(container.parentElement!, FORCE_MESSAGES_SCROLL_CLASS);
|
||||
|
||||
setTimeout(() => {
|
||||
if (container.parentElement) {
|
||||
removeExtraClass(container.parentElement, 'force-messages-scroll');
|
||||
removeExtraClass(container.parentElement, FORCE_MESSAGES_SCROLL_CLASS);
|
||||
}
|
||||
}, 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;
|
||||
@ -722,6 +768,7 @@ 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);
|
||||
@ -804,6 +851,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
||||
noAppearanceAnimation={!messageGroups || !shouldAnimateAppearanceRef.current}
|
||||
isQuickPreview={isQuickPreview}
|
||||
canPost={canPost}
|
||||
isBotForum={isBotForum}
|
||||
shouldScrollToBottom={shouldScrollToBottom}
|
||||
onScrollDownToggle={onScrollDownToggle}
|
||||
onNotchToggle={onNotchToggle}
|
||||
@ -822,6 +870,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
||||
activeKey={activeKey}
|
||||
shouldCleanup
|
||||
onScroll={handleScroll}
|
||||
onWheel={handleWheel}
|
||||
onMouseDown={preventMessageInputBlur}
|
||||
>
|
||||
{renderContent()}
|
||||
@ -937,6 +986,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
canTranslate,
|
||||
translationLanguage,
|
||||
shouldAutoTranslate,
|
||||
isBotForum: chat.isBotForum,
|
||||
shouldScrollToBottom,
|
||||
};
|
||||
},
|
||||
|
||||
@ -37,6 +37,7 @@ 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';
|
||||
@ -59,6 +60,7 @@ interface OwnProps {
|
||||
withUsers: boolean;
|
||||
isChannelChat: boolean | undefined;
|
||||
isChatMonoforum?: boolean;
|
||||
isBotForum?: boolean;
|
||||
isEmptyThread?: boolean;
|
||||
isComments?: boolean;
|
||||
noAvatars: boolean;
|
||||
@ -99,6 +101,7 @@ const MessageListContent = ({
|
||||
withUsers,
|
||||
isChannelChat,
|
||||
isChatMonoforum,
|
||||
isBotForum,
|
||||
noAvatars,
|
||||
containerRef,
|
||||
anchorIdRef,
|
||||
@ -243,6 +246,20 @@ 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);
|
||||
@ -448,6 +465,7 @@ const MessageListContent = ({
|
||||
{shouldRenderAccountInfo
|
||||
&& <MessageListAccountInfo key={`account_info_${chatId}`} chatId={chatId} hasMessages />}
|
||||
{dateGroups.flat()}
|
||||
{isViewportNewest && renderBotForumTopicAction()}
|
||||
{withHistoryTriggers && (
|
||||
<div
|
||||
ref={forwardsTriggerRef}
|
||||
|
||||
@ -279,11 +279,12 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
||||
<PrivateChatInfo
|
||||
key={displayChatId}
|
||||
userId={displayChatId}
|
||||
threadId={!isSavedDialog ? threadId : undefined}
|
||||
typingStatus={typingStatus}
|
||||
status={connectionStatusText || savedMessagesStatus}
|
||||
withDots={Boolean(connectionStatusText)}
|
||||
withFullInfo
|
||||
withMediaViewer
|
||||
withFullInfo={threadId === MAIN_THREAD_ID}
|
||||
withMediaViewer={threadId === MAIN_THREAD_ID}
|
||||
withStory={!isChatWithSelf}
|
||||
withUpdatingStatus
|
||||
isSavedDialog={isSavedDialog}
|
||||
|
||||
@ -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 = useOldLang();
|
||||
const lang = useLang();
|
||||
|
||||
const [handleMouseEnter, handleMouseLeave] = useMouseInside(isOpen, onClose);
|
||||
const { isKeyboardSingleUse } = message || {};
|
||||
|
||||
@ -7,11 +7,9 @@ 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: OldLangFn | LangFn, button: ApiKeyboardButton): TeactNode {
|
||||
export default function renderKeyboardButtonText(lang: LangFn, button: ApiKeyboardButton): TeactNode {
|
||||
if (button.type === 'receipt') {
|
||||
return lang('PaymentReceipt');
|
||||
}
|
||||
|
||||
@ -83,7 +83,7 @@ export default function useMessageObservers(
|
||||
});
|
||||
|
||||
if (!isQuickPreview) {
|
||||
if (memoFirstUnreadIdRef.current && maxId >= memoFirstUnreadIdRef.current) {
|
||||
if (memoFirstUnreadIdRef.current && maxId && maxId >= memoFirstUnreadIdRef.current) {
|
||||
markMessageListRead({ maxId });
|
||||
}
|
||||
|
||||
|
||||
@ -283,3 +283,23 @@
|
||||
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;
|
||||
}
|
||||
|
||||
@ -40,7 +40,6 @@ 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';
|
||||
@ -125,11 +124,11 @@ const ActionMessage = ({
|
||||
hasUnreadReaction,
|
||||
isResizingContainer,
|
||||
scrollTargetPosition,
|
||||
isAccountFrozen,
|
||||
onIntersectPinnedMessage,
|
||||
observeIntersectionForBottom,
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
isAccountFrozen,
|
||||
}: OwnProps & StateProps) => {
|
||||
const {
|
||||
requestConfetti,
|
||||
@ -168,8 +167,6 @@ const ActionMessage = ({
|
||||
|
||||
useOnIntersect(ref, !shouldSkipRender ? observeIntersectionForBottom : undefined);
|
||||
|
||||
useMessageResizeObserver(ref, !shouldSkipRender && isLastInList && action.type !== 'channelJoined');
|
||||
|
||||
useEnsureMessage(
|
||||
replyToPeerId || chatId,
|
||||
replyToMsgId,
|
||||
|
||||
@ -40,6 +40,7 @@ import {
|
||||
getPinnedMediaValue,
|
||||
renderMessageLink,
|
||||
renderPeerLink,
|
||||
renderTopicLink,
|
||||
translateWithYou,
|
||||
} from './helpers/messageActions';
|
||||
|
||||
@ -81,7 +82,6 @@ const ActionMessageText = ({
|
||||
asPreview,
|
||||
}: OwnProps & StateProps) => {
|
||||
const {
|
||||
openThread,
|
||||
openTelegramLink,
|
||||
openUrl,
|
||||
} = getActions();
|
||||
@ -231,18 +231,15 @@ const ActionMessageText = ({
|
||||
|
||||
const topicId = selectThreadIdFromMessage(global, message);
|
||||
|
||||
const topicLink = (
|
||||
<Link
|
||||
className={styles.topicLink}
|
||||
|
||||
onClick={() => openThread({ chatId, threadId: topicId })}
|
||||
>
|
||||
const topicLinkContent = (
|
||||
<>
|
||||
{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 });
|
||||
}
|
||||
|
||||
@ -253,12 +250,8 @@ const ActionMessageText = ({
|
||||
|
||||
const topicId = selectThreadIdFromMessage(global, message);
|
||||
const currentTopic = selectTopic(global, chatId, topicId);
|
||||
const topicLink = (
|
||||
<Link
|
||||
className={styles.topicLink}
|
||||
|
||||
onClick={() => openThread({ chatId, threadId: topicId })}
|
||||
>
|
||||
const topicLinkContent = (
|
||||
<>
|
||||
{iconEmojiId && iconEmojiId !== DEFAULT_TOPIC_ICON_ID
|
||||
? <CustomEmoji documentId={iconEmojiId} isSelectable />
|
||||
: (
|
||||
@ -270,17 +263,12 @@ const ActionMessageText = ({
|
||||
)}
|
||||
{topicId !== GENERAL_TOPIC_ID && NBSP}
|
||||
{renderText(title || currentTopic?.title || lang('ActionTopicPlaceholder'))}
|
||||
</Link>
|
||||
</>
|
||||
);
|
||||
const topicLink = renderTopicLink(chatId, Number(topicId), topicLinkContent, asPreview);
|
||||
|
||||
const topicPlaceholderLink = (
|
||||
<Link
|
||||
className={styles.topicLink}
|
||||
|
||||
onClick={() => openThread({ chatId, threadId: topicId })}
|
||||
>
|
||||
{lang('ActionTopicPlaceholder')}
|
||||
</Link>
|
||||
const topicPlaceholderLink = renderTopicLink(
|
||||
chatId, Number(topicId), lang('ActionTopicPlaceholder'), asPreview,
|
||||
);
|
||||
|
||||
if (isClosed !== undefined) {
|
||||
|
||||
@ -1,13 +1,12 @@
|
||||
import type { FC, TeactNode } from '../../../lib/teact/teact';
|
||||
import type { TeactNode } from '../../../lib/teact/teact';
|
||||
import { memo, useMemo } from '../../../lib/teact/teact';
|
||||
|
||||
import type { ApiKeyboardButton, ApiMessage } from '../../../api/types';
|
||||
import type { ActionPayloads } from '../../../global/types';
|
||||
import type { ApiKeyboardButton } from '../../../api/types';
|
||||
|
||||
import { RE_TME_LINK, TME_LINK_PREFIX } from '../../../config';
|
||||
import renderKeyboardButtonText from '../composer/helpers/renderKeyboardButtonText';
|
||||
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
|
||||
import Icon from '../../common/icons/Icon';
|
||||
import Button from '../../ui/Button';
|
||||
@ -15,12 +14,12 @@ import Button from '../../ui/Button';
|
||||
import './InlineButtons.scss';
|
||||
|
||||
type OwnProps = {
|
||||
message: ApiMessage;
|
||||
onClick: (payload: ActionPayloads['clickBotInlineButton']) => void;
|
||||
inlineButtons: ApiKeyboardButton[][];
|
||||
onClick: (payload: ApiKeyboardButton) => void;
|
||||
};
|
||||
|
||||
const InlineButtons: FC<OwnProps> = ({ message, onClick }) => {
|
||||
const lang = useOldLang();
|
||||
const InlineButtons = ({ inlineButtons, onClick }: OwnProps) => {
|
||||
const lang = useLang();
|
||||
|
||||
const renderIcon = (button: ApiKeyboardButton) => {
|
||||
const { type } = button;
|
||||
@ -66,15 +65,15 @@ const InlineButtons: FC<OwnProps> = ({ message, onClick }) => {
|
||||
|
||||
const buttonTexts = useMemo(() => {
|
||||
const texts: TeactNode[][] = [];
|
||||
message.inlineButtons!.forEach((row) => {
|
||||
inlineButtons.forEach((row) => {
|
||||
texts.push(row.map((button) => renderKeyboardButtonText(lang, button)));
|
||||
});
|
||||
return texts;
|
||||
}, [lang, message.inlineButtons]);
|
||||
}, [lang, inlineButtons]);
|
||||
|
||||
return (
|
||||
<div className="InlineButtons">
|
||||
{message.inlineButtons!.map((row, i) => (
|
||||
{inlineButtons.map((row, i) => (
|
||||
<div className="row">
|
||||
{row.map((button, j) => (
|
||||
<Button
|
||||
@ -82,7 +81,7 @@ const InlineButtons: FC<OwnProps> = ({ message, onClick }) => {
|
||||
ripple
|
||||
disabled={button.type === 'unsupported' || (button.type === 'suggestedMessage' && button.disabled)}
|
||||
|
||||
onClick={() => onClick({ chatId: message.chatId, messageId: message.id, button })}
|
||||
onClick={() => onClick(button)}
|
||||
>
|
||||
{renderIcon(button)}
|
||||
<span className="inline-button-text">
|
||||
|
||||
@ -14,6 +14,7 @@ import type {
|
||||
ApiAvailableReaction,
|
||||
ApiChat,
|
||||
ApiChatMember,
|
||||
ApiKeyboardButton,
|
||||
ApiMessage,
|
||||
ApiMessageOutgoingStatus,
|
||||
ApiPeer,
|
||||
@ -27,7 +28,6 @@ import type {
|
||||
ApiUser,
|
||||
ApiWebPage,
|
||||
} from '../../../api/types';
|
||||
import type { ActionPayloads } from '../../../global/types';
|
||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
import type {
|
||||
ActiveEmojiInteraction,
|
||||
@ -150,7 +150,6 @@ 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';
|
||||
@ -215,27 +214,25 @@ 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;
|
||||
@ -267,6 +264,7 @@ type StateProps = {
|
||||
isResizingContainer?: boolean;
|
||||
isForwarding?: boolean;
|
||||
isChatWithSelf?: boolean;
|
||||
isBotForum?: boolean;
|
||||
isRepliesChat?: boolean;
|
||||
isAnonymousForwards?: boolean;
|
||||
isChannel?: boolean;
|
||||
@ -395,6 +393,7 @@ const Message = ({
|
||||
isResizingContainer,
|
||||
isForwarding,
|
||||
isChatWithSelf,
|
||||
isBotForum,
|
||||
isRepliesChat,
|
||||
isAnonymousForwards,
|
||||
isChannel,
|
||||
@ -464,6 +463,7 @@ const Message = ({
|
||||
animateUnreadReaction,
|
||||
focusMessage,
|
||||
markMentionsRead,
|
||||
openThread,
|
||||
} = getActions();
|
||||
|
||||
const ref = useRef<HTMLDivElement>();
|
||||
@ -524,6 +524,7 @@ const Message = ({
|
||||
|
||||
const {
|
||||
id: messageId, chatId, forwardInfo, viaBotId, isTranscriptionError, factCheck,
|
||||
isTypingDraft,
|
||||
} = message;
|
||||
|
||||
useUnmountCleanup(() => {
|
||||
@ -909,8 +910,6 @@ const Message = ({
|
||||
|| ((asForwarded || isChatWithSelf) && forwardInfo?.postAuthorTitle)
|
||||
|| undefined;
|
||||
|
||||
useMessageResizeObserver(ref, isLastInList);
|
||||
|
||||
useEffect(() => {
|
||||
const bottomMarker = bottomMarkerRef.current;
|
||||
if (!bottomMarker || !isElementInViewport(bottomMarker)) return;
|
||||
@ -1027,6 +1026,7 @@ const Message = ({
|
||||
canBeEmpty={hasFactCheck}
|
||||
maxTimestamp={maxTimestamp}
|
||||
threadId={threadId}
|
||||
shouldAnimateTyping={isTypingDraft}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@ -1540,25 +1540,45 @@ const Message = ({
|
||||
);
|
||||
}
|
||||
|
||||
const handleSuggestedMessageButton = useLastCallback((payload: ActionPayloads['clickBotInlineButton']) => {
|
||||
if (payload.button.type !== 'suggestedMessage') return;
|
||||
if (payload.button.buttonType === 'approve') {
|
||||
openSuggestedPostApprovalModal({
|
||||
const handleInlineButtonClick = useLastCallback((button: ApiKeyboardButton) => {
|
||||
clickBotInlineButton({
|
||||
chatId,
|
||||
messageId: message.id,
|
||||
threadId,
|
||||
button,
|
||||
});
|
||||
});
|
||||
|
||||
const handleLocalInlineButtonClick = useLastCallback((button: ApiKeyboardButton) => {
|
||||
if (button.type === 'openThread') {
|
||||
openThread({
|
||||
chatId,
|
||||
messageId: message.id,
|
||||
threadId: messageTopic!.id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (payload.button.buttonType === 'decline') {
|
||||
openDeclineDialog();
|
||||
if (button.type === 'suggestedMessage') {
|
||||
if (button.buttonType === 'approve') {
|
||||
openSuggestedPostApprovalModal({
|
||||
chatId,
|
||||
messageId: message.id,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (button.buttonType === 'decline') {
|
||||
openDeclineDialog();
|
||||
return;
|
||||
}
|
||||
|
||||
clickSuggestedMessageButton({
|
||||
chatId,
|
||||
messageId: message.id,
|
||||
button,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
clickSuggestedMessageButton({
|
||||
...payload,
|
||||
button: payload.button,
|
||||
});
|
||||
});
|
||||
|
||||
const handleDeclineReasonChange = useLastCallback((e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
@ -1672,11 +1692,52 @@ const Message = ({
|
||||
const shouldRenderSuggestedPostButtons = message.suggestedPostInfo
|
||||
&& !message.isOutgoing && !message.suggestedPostInfo.isAccepted && !message.suggestedPostInfo.isRejected;
|
||||
|
||||
const isSuggestedPostExpired = useMemo(() => {
|
||||
const isSuggestedPostExpired = (() => {
|
||||
if (!message.suggestedPostInfo?.scheduleDate || !minFutureTime) return false;
|
||||
const now = getServerTime();
|
||||
return message.suggestedPostInfo.scheduleDate <= now + minFutureTime;
|
||||
}, [message.suggestedPostInfo, minFutureTime]);
|
||||
})();
|
||||
|
||||
const suggestedPostButtons: ApiKeyboardButton[][] | undefined = useMemo(() => {
|
||||
if (!shouldRenderSuggestedPostButtons) return undefined;
|
||||
return [
|
||||
[
|
||||
{
|
||||
type: 'suggestedMessage',
|
||||
buttonType: 'decline',
|
||||
text: lang('SuggestedPostDecline'),
|
||||
},
|
||||
{
|
||||
type: 'suggestedMessage',
|
||||
buttonType: 'approve',
|
||||
text: lang('SuggestedPostApprove'),
|
||||
disabled: isSuggestedPostExpired,
|
||||
},
|
||||
],
|
||||
[
|
||||
{
|
||||
type: 'suggestedMessage',
|
||||
buttonType: 'suggestChanges',
|
||||
text: lang('SuggestedPostSuggestChanges'),
|
||||
},
|
||||
],
|
||||
];
|
||||
}, [isSuggestedPostExpired, lang, shouldRenderSuggestedPostButtons]);
|
||||
|
||||
const openThreadButtons: ApiKeyboardButton[][] | undefined = useMemo(() => {
|
||||
if (!isBotForum || message.inlineButtons || !messageTopic || !isLastInList ||
|
||||
threadId !== MAIN_THREAD_ID
|
||||
) return undefined;
|
||||
|
||||
return [
|
||||
[{
|
||||
type: 'openThread',
|
||||
text: lang('BotForumContinueThreadButton'),
|
||||
}],
|
||||
];
|
||||
}, [isBotForum, lang, message.inlineButtons, messageTopic, isLastInList, threadId]);
|
||||
|
||||
const additionalInlineButtons = suggestedPostButtons || openThreadButtons;
|
||||
|
||||
return (
|
||||
<div
|
||||
@ -1791,36 +1852,12 @@ const Message = ({
|
||||
{withQuickReactionButton && quickReactionPosition === 'in-content' && renderQuickReactionButton()}
|
||||
</div>
|
||||
{message.inlineButtons && (
|
||||
<InlineButtons message={message} onClick={clickBotInlineButton} />
|
||||
<InlineButtons inlineButtons={message.inlineButtons} onClick={handleInlineButtonClick} />
|
||||
)}
|
||||
{shouldRenderSuggestedPostButtons && (
|
||||
{additionalInlineButtons && (
|
||||
<InlineButtons
|
||||
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}
|
||||
inlineButtons={additionalInlineButtons}
|
||||
onClick={handleLocalInlineButtonClick}
|
||||
/>
|
||||
)}
|
||||
{reactionsPosition === 'outside' && !isStoryMention && (
|
||||
@ -1985,8 +2022,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
|
||||
const hasUnreadReaction = chat?.unreadReactions?.includes(message.id);
|
||||
|
||||
const hasTopicChip = threadId === MAIN_THREAD_ID && chat?.isForum && isFirstInGroup;
|
||||
const messageTopic = hasTopicChip ? selectTopicFromMessage(global, message) : undefined;
|
||||
const hasTopicChip = threadId === MAIN_THREAD_ID && chat?.isForum && !chat.isBotForum && isFirstInGroup;
|
||||
const messageTopic = selectTopicFromMessage(global, message);
|
||||
|
||||
const chatTranslations = selectChatTranslations(global, chatId);
|
||||
|
||||
@ -2047,6 +2084,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
isForwarding,
|
||||
reactionMessage,
|
||||
isChatWithSelf,
|
||||
isBotForum: chat?.isBotForum,
|
||||
isRepliesChat: isSystemBotChat,
|
||||
isAnonymousForwards,
|
||||
isChannel,
|
||||
|
||||
@ -96,6 +96,7 @@ 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)}
|
||||
@ -129,6 +130,23 @@ 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;
|
||||
|
||||
@ -42,14 +42,15 @@ 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;
|
||||
@ -87,14 +88,15 @@ function Transition({
|
||||
slideClassName,
|
||||
withSwipeControl,
|
||||
isBlockingAnimation,
|
||||
onStart,
|
||||
onStop,
|
||||
onScroll,
|
||||
onMouseDown,
|
||||
children,
|
||||
'data-tauri-drag-region': dataTauriDragRegion,
|
||||
contentSelector,
|
||||
restoreHeightKey,
|
||||
onStart,
|
||||
onStop,
|
||||
onScroll,
|
||||
onMouseDown,
|
||||
onWheel,
|
||||
}: TransitionProps) {
|
||||
const currentKeyRef = useRef<number>();
|
||||
// No need for a container to update on change
|
||||
@ -411,6 +413,7 @@ function Transition({
|
||||
data-tauri-drag-region={dataTauriDragRegion}
|
||||
onScroll={onScroll}
|
||||
onMouseDown={onMouseDown}
|
||||
onWheel={onWheel}
|
||||
>
|
||||
{contents}
|
||||
</div>
|
||||
|
||||
@ -338,6 +338,7 @@ 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;
|
||||
|
||||
@ -2386,9 +2386,6 @@ 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;
|
||||
|
||||
|
||||
@ -148,6 +148,7 @@ import {
|
||||
selectTabState,
|
||||
selectThreadIdFromMessage,
|
||||
selectThreadInfo,
|
||||
selectThreadParam,
|
||||
selectTopic,
|
||||
selectTranslationLanguage,
|
||||
selectUser,
|
||||
@ -423,6 +424,16 @@ 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,
|
||||
@ -442,6 +453,24 @@ 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[] = [];
|
||||
@ -1233,6 +1262,7 @@ 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, {
|
||||
@ -1241,7 +1271,7 @@ addActionHandler('markMessageListRead', (global, actions, payload): ActionReturn
|
||||
return global;
|
||||
}
|
||||
|
||||
if (!viewportIds || !minId || !chat.unreadCount) {
|
||||
if (!viewportIds || !minId || (!chat.unreadCount && !topic?.unreadCount)) {
|
||||
return global;
|
||||
}
|
||||
|
||||
@ -1250,17 +1280,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) {
|
||||
if (newTopicUnreadCount === 0 && !chat.isBotForum && chat.unreadCount) {
|
||||
global = updateChat(global, chatId, {
|
||||
unreadCount: Math.max(0, chat.unreadCount - 1),
|
||||
});
|
||||
}
|
||||
|
||||
return updateTopic(global, chatId, Number(threadId), {
|
||||
unreadCount: newTopicUnreadCount,
|
||||
});
|
||||
@ -1268,7 +1298,7 @@ addActionHandler('markMessageListRead', (global, actions, payload): ActionReturn
|
||||
|
||||
return updateChat(global, chatId, {
|
||||
lastReadInboxMessageId: maxId,
|
||||
unreadCount: Math.max(0, chat.unreadCount - readCount),
|
||||
unreadCount: Math.max(0, (chat.unreadCount || 0) - readCount),
|
||||
});
|
||||
});
|
||||
|
||||
@ -1742,9 +1772,13 @@ 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);
|
||||
|
||||
@ -156,7 +156,8 @@ async function loadAndReplaceMessages<T extends GlobalState>(global: T, actions:
|
||||
currentChatId,
|
||||
activeThreadId,
|
||||
),
|
||||
activeThreadId !== MAIN_THREAD_ID && !getIsSavedDialog(currentChat.id, activeThreadId, global.currentUserId)
|
||||
activeThreadId !== MAIN_THREAD_ID && !currentChat.isForum
|
||||
&& !getIsSavedDialog(currentChat.id, activeThreadId, global.currentUserId)
|
||||
? callApi('fetchDiscussionMessage', {
|
||||
chat: currentChat,
|
||||
messageId: Number(activeThreadId),
|
||||
|
||||
@ -24,6 +24,7 @@ import {
|
||||
updateChatFullInfo,
|
||||
updateChatListType,
|
||||
updatePeerStoriesHidden,
|
||||
updateThreadInfo,
|
||||
updateTopic,
|
||||
} from '../../reducers';
|
||||
import { updateUnreadReactions } from '../../reducers/reactions';
|
||||
@ -148,7 +149,21 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
}
|
||||
|
||||
case 'updateChatInbox': {
|
||||
return updateChat(global, update.id, update.chat);
|
||||
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,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
case 'updateChatTypingStatus': {
|
||||
@ -200,6 +215,8 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
unreadCount: topic.unreadCount ? topic.unreadCount + 1 : 1,
|
||||
});
|
||||
}
|
||||
|
||||
// TODO Replace draft with new message
|
||||
}
|
||||
|
||||
setGlobal(global);
|
||||
|
||||
@ -20,9 +20,15 @@ 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, getIsSavedDialog, getMessageContent, getMessageText, isActionMessage,
|
||||
checkIfHasUnreadReactions,
|
||||
createApiMessageFromTypingDraft,
|
||||
getIsSavedDialog,
|
||||
getMessageContent,
|
||||
getMessageText,
|
||||
isActionMessage,
|
||||
isMessageLocal,
|
||||
} from '../../helpers';
|
||||
import { getMessageReplyInfo, getStoryReplyInfo } from '../../helpers/replies';
|
||||
@ -85,9 +91,11 @@ import {
|
||||
selectScheduledIds,
|
||||
selectScheduledMessage,
|
||||
selectTabState,
|
||||
selectThread,
|
||||
selectThreadByMessage,
|
||||
selectThreadIdFromMessage,
|
||||
selectThreadInfo,
|
||||
selectThreadParam,
|
||||
selectTopic,
|
||||
selectTopicFromMessage,
|
||||
selectViewportIds,
|
||||
@ -178,6 +186,14 @@ 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
|
||||
@ -913,6 +929,78 @@ 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;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@ -500,6 +500,11 @@ addActionHandler('scrollMessageListToBottom', (global, actions, payload): Action
|
||||
cancelScrollBlockingAnimation();
|
||||
}
|
||||
|
||||
const isViewportNewest = selectIsViewportNewest(global, chatId, threadId, tabId);
|
||||
if (isViewportNewest) {
|
||||
return;
|
||||
}
|
||||
|
||||
actions.loadViewportMessages({
|
||||
chatId,
|
||||
threadId,
|
||||
|
||||
@ -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) return acc;
|
||||
if (!message || message.isTypingDraft) return acc;
|
||||
|
||||
let cleanedMessage = omitLocalMedia(message);
|
||||
cleanedMessage = omitLocalPaidReactions(cleanedMessage);
|
||||
|
||||
@ -9,7 +9,8 @@ import type {
|
||||
ApiTypeStory,
|
||||
} from '../../api/types';
|
||||
import type {
|
||||
ApiPoll, ApiWebPage, MediaContainer, StatefulMediaContent,
|
||||
ApiFormattedText,
|
||||
ApiPoll, ApiReplyInfo, ApiWebPage, MediaContainer, StatefulMediaContent,
|
||||
} from '../../api/types/messages';
|
||||
import type { ThreadId } from '../../types';
|
||||
import type { LangFn } from '../../util/localization';
|
||||
@ -17,6 +18,7 @@ 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,
|
||||
@ -38,6 +40,11 @@ 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('-');
|
||||
@ -504,3 +511,38 @@ 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(),
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type {
|
||||
ApiFormattedText,
|
||||
ApiMessage, ApiPoll, ApiPollResult, ApiQuickReply, ApiSponsoredMessage, ApiThreadInfo,
|
||||
ApiWebPage,
|
||||
ApiWebPageFull,
|
||||
@ -46,6 +47,7 @@ import {
|
||||
selectTabState,
|
||||
selectThreadIdFromMessage,
|
||||
selectThreadInfo,
|
||||
selectThreadParam,
|
||||
selectViewportIds,
|
||||
selectWebPage,
|
||||
} from '../selectors';
|
||||
@ -1064,3 +1066,24 @@ 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;
|
||||
}
|
||||
|
||||
@ -127,7 +127,7 @@ export function updatePeerPhotos<T extends GlobalState>(
|
||||
});
|
||||
}
|
||||
|
||||
const hasFallbackPhoto = currentPhotos.photos.at(-1).id === fallbackPhoto?.id;
|
||||
const hasFallbackPhoto = fallbackPhoto && 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');
|
||||
|
||||
@ -551,7 +551,8 @@ export function selectCanDeleteTopic<T extends GlobalState>(global: T, chatId: s
|
||||
|
||||
if (topicId === GENERAL_TOPIC_ID) return false;
|
||||
|
||||
return chat.isCreator
|
||||
return chat.isBotForum
|
||||
|| chat.isCreator
|
||||
|| getHasAdminRight(chat, 'deleteMessages')
|
||||
|| (chat.isForum
|
||||
&& selectCanDeleteOwnerTopic(global, chat.id, topicId));
|
||||
@ -1559,7 +1560,7 @@ export function selectTopicLink<T extends GlobalState>(
|
||||
global: T, chatId: string, topicId?: ThreadId,
|
||||
) {
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat || !chat?.isForum) {
|
||||
if (!chat || !chat.isForum || chat.isBotForum) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@ -22,7 +22,6 @@ import type {
|
||||
ApiInputSavedStarGift,
|
||||
ApiInputSuggestedPostInfo,
|
||||
ApiKeyboardButton,
|
||||
ApiKeyboardButtonSuggestedMessage,
|
||||
ApiLimitTypeWithModal,
|
||||
ApiMessage,
|
||||
ApiMessageEntity,
|
||||
@ -60,6 +59,7 @@ import type {
|
||||
ApiUser,
|
||||
ApiVideo,
|
||||
BotsPrivacyType,
|
||||
KeyboardButtonSuggestedMessage,
|
||||
LinkContext,
|
||||
PrivacyVisibility,
|
||||
} from '../../api/types';
|
||||
@ -2084,7 +2084,7 @@ export interface ActionPayloads {
|
||||
clickSuggestedMessageButton: {
|
||||
chatId: string;
|
||||
messageId: number;
|
||||
button: ApiKeyboardButtonSuggestedMessage;
|
||||
button: KeyboardButtonSuggestedMessage;
|
||||
} & WithTabId;
|
||||
|
||||
switchBotInline: {
|
||||
|
||||
@ -1,53 +0,0 @@
|
||||
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;
|
||||
@ -157,4 +157,5 @@ export const DEFAULT_APP_CONFIG: ApiAppConfig = {
|
||||
'fragment.com',
|
||||
'translations.telegram.org',
|
||||
],
|
||||
typingDraftTtl: 10,
|
||||
};
|
||||
|
||||
@ -3,8 +3,8 @@
|
||||
font-weight: normal;
|
||||
font-style: normal;
|
||||
font-display: block;
|
||||
src: url("./icons.woff2?cba4fd3111b01885aeeafb1a9ec56b6b") format("woff2"),
|
||||
url("./icons.woff?cba4fd3111b01885aeeafb1a9ec56b6b") format("woff");
|
||||
src: url("./icons.woff2?33f6294c2f4a2ffb1e77473fb35bc539") format("woff2"),
|
||||
url("./icons.woff?33f6294c2f4a2ffb1e77473fb35bc539") format("woff");
|
||||
}
|
||||
|
||||
.icon-char::before {
|
||||
@ -810,102 +810,105 @@ url("./icons.woff?cba4fd3111b01885aeeafb1a9ec56b6b") format("woff");
|
||||
.icon-toncoin::before {
|
||||
content: "\f207";
|
||||
}
|
||||
.icon-trade::before {
|
||||
.icon-topic-new::before {
|
||||
content: "\f208";
|
||||
}
|
||||
.icon-transcribe::before {
|
||||
.icon-trade::before {
|
||||
content: "\f209";
|
||||
}
|
||||
.icon-truck::before {
|
||||
.icon-transcribe::before {
|
||||
content: "\f20a";
|
||||
}
|
||||
.icon-unarchive::before {
|
||||
.icon-truck::before {
|
||||
content: "\f20b";
|
||||
}
|
||||
.icon-underlined::before {
|
||||
.icon-unarchive::before {
|
||||
content: "\f20c";
|
||||
}
|
||||
.icon-understood::before {
|
||||
.icon-underlined::before {
|
||||
content: "\f20d";
|
||||
}
|
||||
.icon-unique-profile::before {
|
||||
.icon-understood::before {
|
||||
content: "\f20e";
|
||||
}
|
||||
.icon-unlist-outline::before {
|
||||
.icon-unique-profile::before {
|
||||
content: "\f20f";
|
||||
}
|
||||
.icon-unlist::before {
|
||||
.icon-unlist-outline::before {
|
||||
content: "\f210";
|
||||
}
|
||||
.icon-unlock-badge::before {
|
||||
.icon-unlist::before {
|
||||
content: "\f211";
|
||||
}
|
||||
.icon-unlock::before {
|
||||
.icon-unlock-badge::before {
|
||||
content: "\f212";
|
||||
}
|
||||
.icon-unmute::before {
|
||||
.icon-unlock::before {
|
||||
content: "\f213";
|
||||
}
|
||||
.icon-unpin::before {
|
||||
.icon-unmute::before {
|
||||
content: "\f214";
|
||||
}
|
||||
.icon-unread::before {
|
||||
.icon-unpin::before {
|
||||
content: "\f215";
|
||||
}
|
||||
.icon-up::before {
|
||||
.icon-unread::before {
|
||||
content: "\f216";
|
||||
}
|
||||
.icon-user-filled::before {
|
||||
.icon-up::before {
|
||||
content: "\f217";
|
||||
}
|
||||
.icon-user-online::before {
|
||||
.icon-user-filled::before {
|
||||
content: "\f218";
|
||||
}
|
||||
.icon-user-stars::before {
|
||||
.icon-user-online::before {
|
||||
content: "\f219";
|
||||
}
|
||||
.icon-user::before {
|
||||
.icon-user-stars::before {
|
||||
content: "\f21a";
|
||||
}
|
||||
.icon-video-outlined::before {
|
||||
.icon-user::before {
|
||||
content: "\f21b";
|
||||
}
|
||||
.icon-video-stop::before {
|
||||
.icon-video-outlined::before {
|
||||
content: "\f21c";
|
||||
}
|
||||
.icon-video::before {
|
||||
.icon-video-stop::before {
|
||||
content: "\f21d";
|
||||
}
|
||||
.icon-view-once::before {
|
||||
.icon-video::before {
|
||||
content: "\f21e";
|
||||
}
|
||||
.icon-voice-chat::before {
|
||||
.icon-view-once::before {
|
||||
content: "\f21f";
|
||||
}
|
||||
.icon-volume-1::before {
|
||||
.icon-voice-chat::before {
|
||||
content: "\f220";
|
||||
}
|
||||
.icon-volume-2::before {
|
||||
.icon-volume-1::before {
|
||||
content: "\f221";
|
||||
}
|
||||
.icon-volume-3::before {
|
||||
.icon-volume-2::before {
|
||||
content: "\f222";
|
||||
}
|
||||
.icon-warning::before {
|
||||
.icon-volume-3::before {
|
||||
content: "\f223";
|
||||
}
|
||||
.icon-web::before {
|
||||
.icon-warning::before {
|
||||
content: "\f224";
|
||||
}
|
||||
.icon-webapp::before {
|
||||
.icon-web::before {
|
||||
content: "\f225";
|
||||
}
|
||||
.icon-word-wrap::before {
|
||||
.icon-webapp::before {
|
||||
content: "\f226";
|
||||
}
|
||||
.icon-zoom-in::before {
|
||||
.icon-word-wrap::before {
|
||||
content: "\f227";
|
||||
}
|
||||
.icon-zoom-out::before {
|
||||
.icon-zoom-in::before {
|
||||
content: "\f228";
|
||||
}
|
||||
.icon-zoom-out::before {
|
||||
content: "\f229";
|
||||
}
|
||||
|
||||
@ -279,37 +279,38 @@ $icons-map: (
|
||||
"tag": "\f205",
|
||||
"timer": "\f206",
|
||||
"toncoin": "\f207",
|
||||
"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",
|
||||
"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",
|
||||
);
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -262,6 +262,7 @@ export type FontIconName =
|
||||
| 'tag'
|
||||
| 'timer'
|
||||
| 'toncoin'
|
||||
| 'topic-new'
|
||||
| 'trade'
|
||||
| 'transcribe'
|
||||
| 'truck'
|
||||
|
||||
@ -588,6 +588,13 @@ export type StarGiftInfo = {
|
||||
shouldUpgrade?: boolean;
|
||||
};
|
||||
|
||||
export type TypingDraft = {
|
||||
senderId: string;
|
||||
id: string;
|
||||
date: number;
|
||||
text: ApiFormattedText;
|
||||
};
|
||||
|
||||
export interface TabThread {
|
||||
scrollOffset?: number;
|
||||
replyStack?: number[];
|
||||
@ -610,6 +617,7 @@ export interface Thread {
|
||||
threadInfo?: ApiThreadInfo;
|
||||
firstMessageId?: number;
|
||||
typingStatus?: ApiTypingStatus;
|
||||
typingDraftIdByRandomId?: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface ServiceNotification {
|
||||
|
||||
6
src/types/language.d.ts
vendored
6
src/types/language.d.ts
vendored
@ -1720,6 +1720,10 @@ export interface LangPair {
|
||||
'ConfirmBuyGiftForTonDescription': undefined;
|
||||
'TitleGiftLocked': undefined;
|
||||
'QuickPreview': undefined;
|
||||
'BotForumContinueThreadButton': undefined;
|
||||
'BotForumActionNew': undefined;
|
||||
'BotForumActionNewDescription': undefined;
|
||||
'BotForumTopicTitlePlaceholder': undefined;
|
||||
'DropOriginalDetailsTransaction': undefined;
|
||||
'StarGiftReasonDropOriginalDetails': undefined;
|
||||
'GiftAnUpgradeButton': undefined;
|
||||
@ -1728,6 +1732,8 @@ export interface LangPair {
|
||||
'UserNoteTitle': undefined;
|
||||
'UserNoteHint': undefined;
|
||||
'EditUserNoteHint': undefined;
|
||||
'BotForumAllTopicTitle': undefined;
|
||||
'BotForumAllTopicDescription': undefined;
|
||||
'AriaStoryTogglerOpen': undefined;
|
||||
'InviteBlockedTitle': undefined;
|
||||
'InviteBlockedOneMessage': undefined;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user