Fix various problems for comments and threads (#3809)

This commit is contained in:
Alexander Zinchuk 2023-12-04 14:39:44 +01:00
parent 4571745654
commit dcba75a11a
46 changed files with 1009 additions and 652 deletions

View File

@ -29,7 +29,7 @@ import type {
PhoneCallAction, PhoneCallAction,
} from '../../types'; } from '../../types';
import { import {
ApiMessageEntityTypes, ApiMessageEntityTypes, MAIN_THREAD_ID,
} from '../../types'; } from '../../types';
import { import {
@ -41,7 +41,7 @@ import {
SUPPORTED_VIDEO_CONTENT_TYPES, SUPPORTED_VIDEO_CONTENT_TYPES,
} from '../../../config'; } from '../../../config';
import { getEmojiOnlyCountForMessage } from '../../../global/helpers/getEmojiOnlyCountForMessage'; import { getEmojiOnlyCountForMessage } from '../../../global/helpers/getEmojiOnlyCountForMessage';
import { pick } from '../../../util/iteratees'; import { omitUndefined, pick } from '../../../util/iteratees';
import { getServerTime, getServerTimeOffset } from '../../../util/serverTime'; import { getServerTime, getServerTimeOffset } from '../../../util/serverTime';
import { interpolateArray } from '../../../util/waveform'; import { interpolateArray } from '../../../util/waveform';
import { buildPeer } from '../gramjsBuilders'; import { buildPeer } from '../gramjsBuilders';
@ -178,12 +178,12 @@ export function buildApiMessageWithChatId(
const isInvoiceMedia = mtpMessage.media instanceof GramJs.MessageMediaInvoice const isInvoiceMedia = mtpMessage.media instanceof GramJs.MessageMediaInvoice
&& Boolean(mtpMessage.media.extendedMedia); && Boolean(mtpMessage.media.extendedMedia);
const isEdited = mtpMessage.editDate && !mtpMessage.editHide; const isEdited = Boolean(mtpMessage.editDate) && !mtpMessage.editHide;
const { const {
inlineButtons, keyboardButtons, keyboardPlaceholder, isKeyboardSingleUse, isKeyboardSelective, inlineButtons, keyboardButtons, keyboardPlaceholder, isKeyboardSingleUse, isKeyboardSelective,
} = buildReplyButtons(mtpMessage, isInvoiceMedia) || {}; } = buildReplyButtons(mtpMessage, isInvoiceMedia) || {};
const forwardInfo = mtpMessage.fwdFrom && buildApiMessageForwardInfo(mtpMessage.fwdFrom, isChatWithSelf); const forwardInfo = mtpMessage.fwdFrom && buildApiMessageForwardInfo(mtpMessage.fwdFrom, isChatWithSelf);
const { replies, mediaUnread: isMediaUnread, postAuthor } = mtpMessage; const { mediaUnread: isMediaUnread, postAuthor } = mtpMessage;
const groupedId = mtpMessage.groupedId && String(mtpMessage.groupedId); const groupedId = mtpMessage.groupedId && String(mtpMessage.groupedId);
const isInAlbum = Boolean(groupedId) && !(content.document || content.audio || content.sticker); const isInAlbum = Boolean(groupedId) && !(content.document || content.audio || content.sticker);
const shouldHideKeyboardButtons = mtpMessage.replyMarkup instanceof GramJs.ReplyKeyboardHide; const shouldHideKeyboardButtons = mtpMessage.replyMarkup instanceof GramJs.ReplyKeyboardHide;
@ -192,8 +192,9 @@ export function buildApiMessageWithChatId(
const isProtected = mtpMessage.noforwards || isInvoiceMedia; const isProtected = mtpMessage.noforwards || isInvoiceMedia;
const isForwardingAllowed = !mtpMessage.noforwards; const isForwardingAllowed = !mtpMessage.noforwards;
const emojiOnlyCount = getEmojiOnlyCountForMessage(content, groupedId); const emojiOnlyCount = getEmojiOnlyCountForMessage(content, groupedId);
const hasComments = mtpMessage.replies?.comments;
return { return omitUndefined({
id: mtpMessage.id, id: mtpMessage.id,
chatId, chatId,
isOutgoing, isOutgoing,
@ -209,12 +210,12 @@ export function buildApiMessageWithChatId(
reactions: mtpMessage.reactions && buildMessageReactions(mtpMessage.reactions), reactions: mtpMessage.reactions && buildMessageReactions(mtpMessage.reactions),
emojiOnlyCount, emojiOnlyCount,
...(mtpMessage.replyTo && { replyInfo: buildApiReplyInfo(mtpMessage.replyTo) }), ...(mtpMessage.replyTo && { replyInfo: buildApiReplyInfo(mtpMessage.replyTo) }),
...(forwardInfo && { forwardInfo }), forwardInfo,
...(isEdited && { isEdited }), isEdited,
...(mtpMessage.editDate && { editDate: mtpMessage.editDate }), editDate: mtpMessage.editDate,
...(isMediaUnread && { isMediaUnread }), isMediaUnread,
...(mtpMessage.mentioned && isMediaUnread && { hasUnreadMention: true }), hasUnreadMention: mtpMessage.mentioned && isMediaUnread,
...(mtpMessage.mentioned && { isMentioned: true }), isMentioned: mtpMessage.mentioned,
...(groupedId && { ...(groupedId && {
groupedId, groupedId,
isInAlbum, isInAlbum,
@ -225,11 +226,11 @@ export function buildApiMessageWithChatId(
}), }),
...(shouldHideKeyboardButtons && { shouldHideKeyboardButtons, isHideKeyboardSelective }), ...(shouldHideKeyboardButtons && { shouldHideKeyboardButtons, isHideKeyboardSelective }),
...(mtpMessage.viaBotId && { viaBotId: buildApiPeerId(mtpMessage.viaBotId, 'user') }), ...(mtpMessage.viaBotId && { viaBotId: buildApiPeerId(mtpMessage.viaBotId, 'user') }),
...(replies && { repliesThreadInfo: buildThreadInfo(replies, mtpMessage.id, chatId) }), postAuthorTitle: postAuthor,
...(postAuthor && { postAuthorTitle: postAuthor }),
isProtected, isProtected,
isForwardingAllowed, isForwardingAllowed,
}; hasComments,
} satisfies ApiMessage);
} }
export function buildMessageDraft(draft: GramJs.TypeDraftMessage): ApiDraft | undefined { export function buildMessageDraft(draft: GramJs.TypeDraftMessage): ApiDraft | undefined {
@ -830,8 +831,11 @@ export function buildLocalForwardedMessage({
text: !shouldHideText ? strippedText : undefined, text: !shouldHideText ? strippedText : undefined,
}; };
const replyInfo: ApiReplyInfo | undefined = toThreadId ? { // TODO Prepare reply info between forwarded messages locally, to prevent height jumps
const isToMainThread = toThreadId === MAIN_THREAD_ID;
const replyInfo: ApiReplyInfo | undefined = toThreadId && !isToMainThread ? {
type: 'message', type: 'message',
replyToMsgId: toThreadId,
replyToTopId: toThreadId, replyToTopId: toThreadId,
isForumTopic: toChat.isForum || undefined, isForumTopic: toChat.isForum || undefined,
} : undefined; } : undefined;
@ -968,7 +972,21 @@ function buildUploadingMedia(
}; };
} }
function buildThreadInfo( export function buildApiThreadInfoFromMessage(
mtpMessage: GramJs.TypeMessage,
): ApiThreadInfo | undefined {
const chatId = resolveMessageApiChatId(mtpMessage);
if (
!chatId
|| !(mtpMessage instanceof GramJs.Message)
|| !mtpMessage.replies) {
return undefined;
}
return buildApiThreadInfo(mtpMessage.replies, mtpMessage.id, chatId);
}
export function buildApiThreadInfo(
messageReplies: GramJs.TypeMessageReplies, messageId: number, chatId: string, messageReplies: GramJs.TypeMessageReplies, messageId: number, chatId: string,
): ApiThreadInfo | undefined { ): ApiThreadInfo | undefined {
const { const {
@ -980,21 +998,28 @@ function buildThreadInfo(
return undefined; return undefined;
} }
const isPostThread = apiChannelId && chatId !== apiChannelId; const baseThreadInfo = {
messagesCount: replies,
...(maxId && { lastMessageId: maxId }),
...(readMaxId && { lastReadMessageId: readMaxId }),
...(recentRepliers && { recentReplierIds: recentRepliers.map(getApiChatIdFromMtpPeer) }),
};
if (comments) {
return {
...baseThreadInfo,
isCommentsInfo: true,
chatId: apiChannelId!,
originChannelId: chatId,
originMessageId: messageId,
};
}
return { return {
isComments: comments, ...baseThreadInfo,
isCommentsInfo: false,
chatId,
threadId: messageId, threadId: messageId,
...(isPostThread ? {
chatId: apiChannelId,
originChannelId: chatId,
} : {
chatId,
}),
messagesCount: replies,
lastMessageId: maxId,
lastReadInboxMessageId: readMaxId,
...(recentRepliers && { recentReplierIds: recentRepliers.map(getApiChatIdFromMtpPeer) }),
}; };
} }

View File

@ -72,6 +72,7 @@ import {
import localDb from '../localDb'; import localDb from '../localDb';
import { scheduleMutedChatUpdate } from '../scheduleUnmute'; import { scheduleMutedChatUpdate } from '../scheduleUnmute';
import { applyState, processUpdate, updateChannelState } from '../updateManager'; import { applyState, processUpdate, updateChannelState } from '../updateManager';
import { dispatchThreadInfoUpdates } from '../updater';
import { invokeRequest, uploadFile } from './client'; import { invokeRequest, uploadFile } from './client';
type FullChatData = { type FullChatData = {
@ -130,6 +131,8 @@ export async function fetchChats({
'chatId', 'chatId',
); );
dispatchThreadInfoUpdates(result.messages);
const peersByKey = preparePeers(result); const peersByKey = preparePeers(result);
if (resultPinned) { if (resultPinned) {
Object.assign(peersByKey, preparePeers(resultPinned, peersByKey)); Object.assign(peersByKey, preparePeers(resultPinned, peersByKey));
@ -340,6 +343,8 @@ export async function requestChatUpdate({
updateLocalDb(result); updateLocalDb(result);
const lastRemoteMessage = buildApiMessage(result.messages[0]); const lastRemoteMessage = buildApiMessage(result.messages[0]);
dispatchThreadInfoUpdates(result.messages);
const lastMessage = lastLocalMessage && (!lastRemoteMessage || (lastLocalMessage.date > lastRemoteMessage.date)) const lastMessage = lastLocalMessage && (!lastRemoteMessage || (lastLocalMessage.date > lastRemoteMessage.date))
? lastLocalMessage ? lastLocalMessage
: lastRemoteMessage; : lastRemoteMessage;
@ -542,7 +547,7 @@ async function getFullChannelInfo(
kickedMembers, kickedMembers,
adminMembersById: adminMembers ? buildCollectionByKey(adminMembers, 'userId') : undefined, adminMembersById: adminMembers ? buildCollectionByKey(adminMembers, 'userId') : undefined,
groupCallId: call ? String(call.id) : undefined, groupCallId: call ? String(call.id) : undefined,
linkedChatId: linkedChatId ? buildApiPeerId(linkedChatId, 'chat') : undefined, linkedChatId: linkedChatId ? buildApiPeerId(linkedChatId, 'channel') : undefined,
botCommands, botCommands,
enabledReactions: buildApiChatReactions(availableReactions), enabledReactions: buildApiChatReactions(availableReactions),
sendAsId: defaultSendAs ? getApiChatIdFromMtpPeer(defaultSendAs) : undefined, sendAsId: defaultSendAs ? getApiChatIdFromMtpPeer(defaultSendAs) : undefined,
@ -1584,6 +1589,7 @@ export async function fetchTopics({
const topics = result.topics.map(buildApiTopic).filter(Boolean); const topics = result.topics.map(buildApiTopic).filter(Boolean);
const messages = result.messages.map(buildApiMessage).filter(Boolean); const messages = result.messages.map(buildApiMessage).filter(Boolean);
dispatchThreadInfoUpdates(result.messages);
const users = result.users.map(buildApiUser).filter(Boolean); const users = result.users.map(buildApiUser).filter(Boolean);
const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean); const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean);
const draftsById = result.topics.reduce((acc, topic) => { const draftsById = result.topics.reduce((acc, topic) => {
@ -1637,6 +1643,7 @@ export async function fetchTopicById({
updateLocalDb(result); updateLocalDb(result);
const messages = result.messages.map(buildApiMessage).filter(Boolean); const messages = result.messages.map(buildApiMessage).filter(Boolean);
dispatchThreadInfoUpdates(result.messages);
const users = result.users.map(buildApiUser).filter(Boolean); const users = result.users.map(buildApiUser).filter(Boolean);
const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean); const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean);

View File

@ -28,12 +28,12 @@ export {
export { export {
fetchMessages, fetchMessage, sendMessage, pinMessage, unpinAllMessages, deleteMessages, deleteHistory, fetchMessages, fetchMessage, sendMessage, pinMessage, unpinAllMessages, deleteMessages, deleteHistory,
markMessageListRead, markMessagesRead, requestThreadInfoUpdate, searchMessagesLocal, searchMessagesGlobal, markMessageListRead, markMessagesRead, searchMessagesLocal, searchMessagesGlobal,
fetchWebPagePreview, editMessage, forwardMessages, loadPollOptionResults, sendPollVote, findFirstMessageIdAfterDate, fetchWebPagePreview, editMessage, forwardMessages, loadPollOptionResults, sendPollVote, findFirstMessageIdAfterDate,
fetchPinnedMessages, fetchScheduledHistory, sendScheduledMessages, rescheduleMessage, deleteScheduledMessages, fetchPinnedMessages, fetchScheduledHistory, sendScheduledMessages, rescheduleMessage, deleteScheduledMessages,
reportMessages, sendMessageAction, fetchSeenBy, fetchSponsoredMessages, viewSponsoredMessage, fetchSendAs, reportMessages, sendMessageAction, fetchSeenBy, fetchSponsoredMessages, viewSponsoredMessage, fetchSendAs,
saveDefaultSendAs, fetchUnreadReactions, readAllReactions, fetchUnreadMentions, readAllMentions, transcribeAudio, saveDefaultSendAs, fetchUnreadReactions, readAllReactions, fetchUnreadMentions, readAllMentions, transcribeAudio,
closePoll, fetchExtendedMedia, translateText, fetchMessageViews, closePoll, fetchExtendedMedia, translateText, fetchMessageViews, fetchDiscussionMessage,
} from './messages'; } from './messages';
export { export {

View File

@ -49,6 +49,8 @@ import {
import { import {
buildApiMessage, buildApiMessage,
buildApiSponsoredMessage, buildApiSponsoredMessage,
buildApiThreadInfo,
buildApiThreadInfoFromMessage,
buildLocalForwardedMessage, buildLocalForwardedMessage,
buildLocalMessage, buildLocalMessage,
} from '../apiBuilders/messages'; } from '../apiBuilders/messages';
@ -76,9 +78,9 @@ import {
addEntitiesToLocalDb, addEntitiesToLocalDb,
addMessageToLocalDb, addMessageToLocalDb,
deserializeBytes, deserializeBytes,
resolveMessageApiChatId,
} from '../helpers'; } from '../helpers';
import { updateChannelState } from '../updateManager'; import { updateChannelState } from '../updateManager';
import { dispatchThreadInfoUpdates } from '../updater';
import { requestChatUpdate } from './chats'; import { requestChatUpdate } from './chats';
import { handleGramJsUpdate, invokeRequest, uploadFile } from './client'; import { handleGramJsUpdate, invokeRequest, uploadFile } from './client';
@ -156,13 +158,12 @@ export async function fetchMessages({
const messages = result.messages.map(buildApiMessage).filter(Boolean); const messages = result.messages.map(buildApiMessage).filter(Boolean);
const users = result.users.map(buildApiUser).filter(Boolean); const users = result.users.map(buildApiUser).filter(Boolean);
const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean); const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean);
const repliesThreadInfos = messages.map(({ repliesThreadInfo }) => repliesThreadInfo).filter(Boolean); dispatchThreadInfoUpdates(result.messages);
return { return {
messages, messages,
users, users,
chats, chats,
repliesThreadInfos,
}; };
} }
@ -220,6 +221,8 @@ export async function fetchMessage({ chat, messageId }: { chat: ApiChat; message
} }
const message = mtpMessage && buildApiMessage(mtpMessage); const message = mtpMessage && buildApiMessage(mtpMessage);
dispatchThreadInfoUpdates([mtpMessage]);
if (!message) { if (!message) {
return undefined; return undefined;
} }
@ -857,8 +860,6 @@ export async function markMessageListRead({
if (threadId === MAIN_THREAD_ID) { if (threadId === MAIN_THREAD_ID) {
void requestChatUpdate({ chat, noLastMessage: true }); void requestChatUpdate({ chat, noLastMessage: true });
} else {
void requestThreadInfoUpdate({ chat, threadId });
} }
} }
@ -923,10 +924,7 @@ export async function fetchMessageViews({
id, id,
views, views,
forwards, forwards,
messagesCount: replies?.replies, threadInfo: replies ? buildApiThreadInfo(replies, id, chat.id) : undefined,
recentReplierIds: replies?.recentRepliers?.map(getApiChatIdFromMtpPeer),
maxId: replies?.maxId,
readMaxId: replies?.readMaxId,
}; };
}); });
@ -937,94 +935,73 @@ export async function fetchMessageViews({
}; };
} }
export async function requestThreadInfoUpdate({ export async function fetchDiscussionMessage({
chat, threadId, originChannelId, chat, messageId,
}: { }: {
chat: ApiChat; threadId: number; originChannelId?: string; chat: ApiChat;
messageId: number;
}) { }) {
if (threadId === MAIN_THREAD_ID) { const [result, replies] = await Promise.all([
return undefined;
}
const [topMessageResult, repliesResult] = await Promise.all([
invokeRequest(new GramJs.messages.GetDiscussionMessage({ invokeRequest(new GramJs.messages.GetDiscussionMessage({
peer: buildInputPeer(chat.id, chat.accessHash), peer: buildInputPeer(chat.id, chat.accessHash),
msgId: Number(threadId), msgId: messageId,
})), }), {
invokeRequest(new GramJs.messages.GetReplies({ abortControllerChatId: chat.id,
peer: buildInputPeer(chat.id, chat.accessHash), abortControllerThreadId: messageId,
msgId: Number(threadId), }),
fetchMessages({
chat,
threadId: messageId,
offsetId: 1, offsetId: 1,
addOffset: -1, addOffset: -1,
limit: 1, limit: 1,
})), }),
]); ]);
if (!topMessageResult || !topMessageResult.messages.length) { if (!result || !replies) return undefined;
return undefined;
}
const discussionChatId = resolveMessageApiChatId(topMessageResult.messages[0]); updateLocalDb(result);
if (!discussionChatId) {
return undefined;
}
const topMessageId = topMessageResult.messages[topMessageResult.messages.length - 1].id; const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean)
.concat(replies.chats);
const users = result.users.map(buildApiUser).filter(Boolean)
.concat(replies.users);
const topMessages = result.messages.map(buildApiMessage).filter(Boolean);
const messages = topMessages.concat(replies.messages);
const threadId = result.messages[result.messages.length - 1]?.id;
onUpdate({ if (!threadId) return undefined;
'@type': 'updateThreadInfo',
chatId: discussionChatId,
threadId: topMessageId,
threadInfo: {
threadId: topMessageId,
topMessageId,
lastReadInboxMessageId: topMessageResult.readInboxMaxId,
messagesCount: (repliesResult instanceof GramJs.messages.ChannelMessages) ? repliesResult.count : undefined,
lastMessageId: topMessageResult.maxId,
...(originChannelId ? { originChannelId } : undefined),
},
firstMessageId: repliesResult && 'messages' in repliesResult && repliesResult.messages.length
? repliesResult.messages[0].id
: undefined,
});
const chats = topMessageResult.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean); dispatchThreadInfoUpdates(result.messages);
chats.forEach((newChat) => { const threadInfoUpdates = result.messages.map(buildApiThreadInfoFromMessage).filter(Boolean);
onUpdate({
'@type': 'updateChat',
id: newChat.id,
chat: newChat,
noTopChatsRequest: true,
});
});
if (chat.isForum) { const {
onUpdate({ unreadCount, maxId, readInboxMaxId, readOutboxMaxId,
'@type': 'updateTopic', } = result;
chatId: chat.id,
topicId: threadId,
});
}
addEntitiesToLocalDb(topMessageResult.users);
addEntitiesToLocalDb(topMessageResult.chats);
const users = topMessageResult.users.map(buildApiUser).filter(Boolean);
return { return {
topMessageId, chats,
discussionChatId,
users, users,
messages,
topMessages,
unreadCount,
threadId,
lastReadInboxMessageId: readInboxMaxId,
lastReadOutboxMessageId: readOutboxMaxId,
lastMessageId: maxId,
chatId: topMessages[0]?.chatId,
firstMessageId: replies.messages[0]?.id,
threadInfoUpdates,
}; };
} }
export async function searchMessagesLocal({ export async function searchMessagesLocal({
chat, type, query, topMessageId, minDate, maxDate, ...pagination chat, type, query, threadId, minDate, maxDate, ...pagination
}: { }: {
chat: ApiChat; chat: ApiChat;
type?: ApiMessageSearchType | ApiGlobalMessageSearchType; type?: ApiMessageSearchType | ApiGlobalMessageSearchType;
query?: string; query?: string;
topMessageId?: number; threadId?: number;
offsetId?: number; offsetId?: number;
addOffset?: number; addOffset?: number;
limit: number; limit: number;
@ -1059,7 +1036,7 @@ export async function searchMessagesLocal({
const result = await invokeRequest(new GramJs.messages.Search({ const result = await invokeRequest(new GramJs.messages.Search({
peer: buildInputPeer(chat.id, chat.accessHash), peer: buildInputPeer(chat.id, chat.accessHash),
topMsgId: topMessageId, topMsgId: threadId === MAIN_THREAD_ID ? undefined : threadId,
filter, filter,
q: query || '', q: query || '',
minDate, minDate,
@ -1067,7 +1044,7 @@ export async function searchMessagesLocal({
...pagination, ...pagination,
}), { }), {
abortControllerChatId: chat.id, abortControllerChatId: chat.id,
abortControllerThreadId: topMessageId, abortControllerThreadId: threadId,
}); });
if ( if (
@ -1083,6 +1060,7 @@ export async function searchMessagesLocal({
const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean); const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean);
const users = result.users.map(buildApiUser).filter(Boolean); const users = result.users.map(buildApiUser).filter(Boolean);
const messages = result.messages.map(buildApiMessage).filter(Boolean); const messages = result.messages.map(buildApiMessage).filter(Boolean);
dispatchThreadInfoUpdates(result.messages);
let totalCount = messages.length; let totalCount = messages.length;
let nextOffsetId: number | undefined; let nextOffsetId: number | undefined;
@ -1168,6 +1146,7 @@ export async function searchMessagesGlobal({
const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean); const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean);
const users = result.users.map(buildApiUser).filter(Boolean); const users = result.users.map(buildApiUser).filter(Boolean);
const messages = result.messages.map(buildApiMessage).filter(Boolean); const messages = result.messages.map(buildApiMessage).filter(Boolean);
dispatchThreadInfoUpdates(result.messages);
let totalCount = messages.length; let totalCount = messages.length;
let nextRate: number | undefined; let nextRate: number | undefined;
@ -1417,6 +1396,7 @@ export async function fetchScheduledHistory({ chat }: { chat: ApiChat }) {
updateLocalDb(result); updateLocalDb(result);
const messages = result.messages.map(buildApiMessage).filter(Boolean); const messages = result.messages.map(buildApiMessage).filter(Boolean);
dispatchThreadInfoUpdates(result.messages);
return { return {
messages, messages,
@ -1475,6 +1455,7 @@ export async function fetchPinnedMessages({ chat, threadId }: { chat: ApiChat; t
const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean); const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean);
const users = result.users.map(buildApiUser).filter(Boolean); const users = result.users.map(buildApiUser).filter(Boolean);
const messages = result.messages.map(buildApiMessage).filter(Boolean); const messages = result.messages.map(buildApiMessage).filter(Boolean);
dispatchThreadInfoUpdates(result.messages);
return { return {
messages, messages,
@ -1617,6 +1598,7 @@ export async function fetchUnreadMentions({
updateLocalDb(result); updateLocalDb(result);
const messages = result.messages.map(buildApiMessage).filter(Boolean); const messages = result.messages.map(buildApiMessage).filter(Boolean);
dispatchThreadInfoUpdates(result.messages);
const users = result.users.map(buildApiUser).filter(Boolean); const users = result.users.map(buildApiUser).filter(Boolean);
const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean); const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean);
@ -1653,6 +1635,7 @@ export async function fetchUnreadReactions({
updateLocalDb(result); updateLocalDb(result);
const messages = result.messages.map(buildApiMessage).filter(Boolean); const messages = result.messages.map(buildApiMessage).filter(Boolean);
dispatchThreadInfoUpdates(result.messages);
const users = result.users.map(buildApiUser).filter(Boolean); const users = result.users.map(buildApiUser).filter(Boolean);
const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean); const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean);

View File

@ -7,7 +7,7 @@ import type {
} from '../types'; } from '../types';
import { DEBUG, GENERAL_TOPIC_ID } from '../../config'; import { DEBUG, GENERAL_TOPIC_ID } from '../../config';
import { omit, pick } from '../../util/iteratees'; import { compact, omit, pick } from '../../util/iteratees';
import { getServerTimeOffset, setServerTimeOffset } from '../../util/serverTime'; import { getServerTimeOffset, setServerTimeOffset } from '../../util/serverTime';
import { buildApiBotMenuButton } from './apiBuilders/bots'; import { buildApiBotMenuButton } from './apiBuilders/bots';
import { import {
@ -38,6 +38,7 @@ import {
buildApiMessageFromNotification, buildApiMessageFromNotification,
buildApiMessageFromShort, buildApiMessageFromShort,
buildApiMessageFromShortChat, buildApiMessageFromShortChat,
buildApiThreadInfoFromMessage,
buildMessageDraft, buildMessageDraft,
} from './apiBuilders/messages'; } from './apiBuilders/messages';
import { import {
@ -125,6 +126,16 @@ export function dispatchUserAndChatUpdates(entities: (GramJs.TypeUser | GramJs.T
}); });
} }
export function dispatchThreadInfoUpdates(messages: (GramJs.TypeMessage | undefined)[]) {
const threadInfoUpdates = compact(messages).map(buildApiThreadInfoFromMessage).filter(Boolean);
if (!threadInfoUpdates.length) return;
onUpdate({
'@type': 'updateThreadInfos',
threadInfoUpdates,
});
}
export function sendUpdate(update: ApiUpdate) { export function sendUpdate(update: ApiUpdate) {
onUpdate(update); onUpdate(update);
} }
@ -199,6 +210,8 @@ export function updater(update: Update) {
} }
message = buildApiMessage(update.message)!; message = buildApiMessage(update.message)!;
dispatchThreadInfoUpdates([update.message]);
shouldForceReply = 'replyMarkup' in update.message shouldForceReply = 'replyMarkup' in update.message
&& update.message?.replyMarkup instanceof GramJs.ReplyKeyboardForceReply && update.message?.replyMarkup instanceof GramJs.ReplyKeyboardForceReply
&& (!update.message.replyMarkup.selective || message.isMentioned); && (!update.message.replyMarkup.selective || message.isMentioned);
@ -348,6 +361,7 @@ export function updater(update: Update) {
// Workaround for a weird server behavior when own message is marked as incoming // Workaround for a weird server behavior when own message is marked as incoming
const message = omit(buildApiMessage(update.message)!, ['isOutgoing']); const message = omit(buildApiMessage(update.message)!, ['isOutgoing']);
dispatchThreadInfoUpdates([update.message]);
onUpdate({ onUpdate({
'@type': 'updateMessage', '@type': 'updateMessage',
@ -548,12 +562,12 @@ export function updater(update: Update) {
}); });
} else if (update instanceof GramJs.UpdateReadChannelDiscussionInbox) { } else if (update instanceof GramJs.UpdateReadChannelDiscussionInbox) {
onUpdate({ onUpdate({
'@type': 'updateThreadInfo', '@type': 'updateThreadInfos',
chatId: buildApiPeerId(update.channelId, 'channel'), threadInfoUpdates: [{
threadId: update.topMsgId, chatId: buildApiPeerId(update.channelId, 'channel'),
threadInfo: { threadId: update.topMsgId,
lastReadInboxMessageId: update.readMaxId, lastReadInboxMessageId: update.readMaxId,
}, }],
}); });
} else if (update instanceof GramJs.UpdateReadChannelDiscussionOutbox) { } else if (update instanceof GramJs.UpdateReadChannelDiscussionOutbox) {
onUpdate({ onUpdate({

View File

@ -480,7 +480,6 @@ export interface ApiMessage {
isKeyboardSingleUse?: boolean; isKeyboardSingleUse?: boolean;
isKeyboardSelective?: boolean; isKeyboardSelective?: boolean;
viaBotId?: string; viaBotId?: string;
repliesThreadInfo?: ApiThreadInfo;
postAuthorTitle?: string; postAuthorTitle?: string;
isScheduled?: boolean; isScheduled?: boolean;
shouldHideKeyboardButtons?: boolean; shouldHideKeyboardButtons?: boolean;
@ -500,6 +499,7 @@ export interface ApiMessage {
reactions: ApiPeerReaction[]; reactions: ApiPeerReaction[];
}; };
reactions?: ApiReactions; reactions?: ApiReactions;
hasComments?: boolean;
} }
export interface ApiReactions { export interface ApiReactions {
@ -559,18 +559,31 @@ export type ApiReactionCustomEmoji = {
export type ApiReaction = ApiReactionEmoji | ApiReactionCustomEmoji; export type ApiReaction = ApiReactionEmoji | ApiReactionCustomEmoji;
export interface ApiThreadInfo { interface ApiBaseThreadInfo {
isComments?: boolean;
threadId: number;
chatId: string; chatId: string;
topMessageId?: number;
originChannelId?: string;
messagesCount: number; messagesCount: number;
lastMessageId?: number; lastMessageId?: number;
lastReadInboxMessageId?: number; lastReadInboxMessageId?: number;
recentReplierIds?: string[]; recentReplierIds?: string[];
} }
export interface ApiCommentsInfo extends ApiBaseThreadInfo {
isCommentsInfo: true;
threadId?: number;
originChannelId: string;
originMessageId: number;
}
export interface ApiMessageThreadInfo extends ApiBaseThreadInfo {
isCommentsInfo: false;
threadId: number;
// For linked messages in discussion
fromChannelId?: string;
fromMessageId?: number;
}
export type ApiThreadInfo = ApiCommentsInfo | ApiMessageThreadInfo;
export type ApiMessageOutgoingStatus = 'read' | 'succeeded' | 'pending' | 'failed'; export type ApiMessageOutgoingStatus = 'read' | 'succeeded' | 'pending' | 'failed';
export type ApiSponsoredMessage = { export type ApiSponsoredMessage = {

View File

@ -222,12 +222,9 @@ export type ApiUpdatePinnedMessageIds = {
messageIds: number[]; messageIds: number[];
}; };
export type ApiUpdateThreadInfo = { export type ApiUpdateThreadInfos = {
'@type': 'updateThreadInfo'; '@type': 'updateThreadInfos';
chatId: string; threadInfoUpdates: Partial<ApiThreadInfo>[];
threadId: number;
threadInfo: Partial<ApiThreadInfo>;
firstMessageId?: number;
}; };
export type ApiUpdateScheduledMessageSendSucceeded = { export type ApiUpdateScheduledMessageSendSucceeded = {
@ -685,7 +682,7 @@ export type ApiUpdate = (
ApiUpdateChat | ApiUpdateChatInbox | ApiUpdateChatTypingStatus | ApiUpdateChatFullInfo | ApiUpdatePinnedChatIds | ApiUpdateChat | ApiUpdateChatInbox | ApiUpdateChatTypingStatus | ApiUpdateChatFullInfo | ApiUpdatePinnedChatIds |
ApiUpdateChatMembers | ApiUpdateChatJoin | ApiUpdateChatLeave | ApiUpdateChatPinned | ApiUpdatePinnedMessageIds | ApiUpdateChatMembers | ApiUpdateChatJoin | ApiUpdateChatLeave | ApiUpdateChatPinned | ApiUpdatePinnedMessageIds |
ApiUpdateChatListType | ApiUpdateChatFolder | ApiUpdateChatFoldersOrder | ApiUpdateRecommendedChatFolders | ApiUpdateChatListType | ApiUpdateChatFolder | ApiUpdateChatFoldersOrder | ApiUpdateRecommendedChatFolders |
ApiUpdateNewMessage | ApiUpdateMessage | ApiUpdateThreadInfo | ApiUpdateCommonBoxMessages | ApiUpdateChannelMessages | ApiUpdateNewMessage | ApiUpdateMessage | ApiUpdateThreadInfos | ApiUpdateCommonBoxMessages |
ApiUpdateDeleteMessages | ApiUpdateMessagePoll | ApiUpdateMessagePollVote | ApiUpdateDeleteHistory | ApiUpdateDeleteMessages | ApiUpdateMessagePoll | ApiUpdateMessagePollVote | ApiUpdateDeleteHistory |
ApiUpdateMessageSendSucceeded | ApiUpdateMessageSendFailed | ApiUpdateServiceNotification | ApiUpdateMessageSendSucceeded | ApiUpdateMessageSendFailed | ApiUpdateServiceNotification |
ApiDeleteContact | ApiUpdateUser | ApiUpdateUserStatus | ApiUpdateUserFullInfo | ApiUpdateDeleteProfilePhotos | ApiDeleteContact | ApiUpdateUser | ApiUpdateUserStatus | ApiUpdateUserFullInfo | ApiUpdateDeleteProfilePhotos |

View File

@ -38,7 +38,7 @@ const ChatForumLastMessage: FC<OwnProps> = ({
renderLastMessage, renderLastMessage,
observeIntersection, observeIntersection,
}) => { }) => {
const { openChat } = getActions(); const { openThread } = getActions();
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const lastMessageRef = useRef<HTMLDivElement>(null); const lastMessageRef = useRef<HTMLDivElement>(null);
@ -67,8 +67,8 @@ const ChatForumLastMessage: FC<OwnProps> = ({
e.stopPropagation(); e.stopPropagation();
e.preventDefault(); e.preventDefault();
openChat({ openThread({
id: chat.id, chatId: chat.id,
threadId: lastActiveTopic.id, threadId: lastActiveTopic.id,
shouldReplaceHistory: true, shouldReplaceHistory: true,
noForumTopicPanel: getIsMobile(), noForumTopicPanel: getIsMobile(),

View File

@ -349,7 +349,7 @@ const Composer: FC<OwnProps & StateProps> = ({
openPollModal, openPollModal,
closePollModal, closePollModal,
loadScheduledHistory, loadScheduledHistory,
openChat, openThread,
addRecentEmoji, addRecentEmoji,
sendInlineBotResult, sendInlineBotResult,
loadSendAs, loadSendAs,
@ -1246,8 +1246,8 @@ const Composer: FC<OwnProps & StateProps> = ({
}); });
const handleAllScheduledClick = useLastCallback(() => { const handleAllScheduledClick = useLastCallback(() => {
openChat({ openThread({
id: chatId, threadId, type: 'scheduled', noForumTopicPanel: true, chatId, threadId, type: 'scheduled', noForumTopicPanel: true,
}); });
}); });

View File

@ -260,9 +260,9 @@ export default memo(withGlobal<OwnProps>(
const chat = chatId && selectChat(global, chatId); const chat = chatId && selectChat(global, chatId);
const sendOptions = chat ? getAllowedAttachmentOptions(chat) : undefined; const sendOptions = chat ? getAllowedAttachmentOptions(chat) : undefined;
const threadInfo = chatId && threadId ? selectThreadInfo(global, chatId, threadId) : undefined; const threadInfo = chatId && threadId ? selectThreadInfo(global, chatId, threadId) : undefined;
const isComments = Boolean(threadInfo?.originChannelId); const isMessageThread = Boolean(!threadInfo?.isCommentsInfo && threadInfo?.fromChannelId);
const canSendStickers = Boolean( const canSendStickers = Boolean(
chat && threadId && getCanPostInChat(chat, threadId, isComments) && sendOptions?.canSendStickers, chat && threadId && getCanPostInChat(chat, threadId, isMessageThread) && sendOptions?.canSendStickers,
); );
const isSavedMessages = Boolean(chatId) && selectIsChatWithSelf(global, chatId); const isSavedMessages = Boolean(chatId) && selectIsChatWithSelf(global, chatId);

View File

@ -19,7 +19,6 @@ import buildClassName from '../../../util/buildClassName';
import captureEscKeyListener from '../../../util/captureEscKeyListener'; import captureEscKeyListener from '../../../util/captureEscKeyListener';
import { captureEvents, SwipeDirection } from '../../../util/captureEvents'; import { captureEvents, SwipeDirection } from '../../../util/captureEvents';
import { waitForTransitionEnd } from '../../../util/cssAnimationEndListeners'; import { waitForTransitionEnd } from '../../../util/cssAnimationEndListeners';
import { createLocationHash } from '../../../util/routing';
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment'; import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
import useAppLayout from '../../../hooks/useAppLayout'; import useAppLayout from '../../../hooks/useAppLayout';
@ -139,7 +138,6 @@ const ForumPanel: FC<OwnProps & StateProps> = ({
useHistoryBack({ useHistoryBack({
isActive: isVisible, isActive: isVisible,
onBack: handleClose, onBack: handleClose,
hash: chat ? createLocationHash(chat.id, 'thread', MAIN_THREAD_ID) : undefined,
}); });
useEffect(() => (isVisible ? captureEscKeyListener(handleClose) : undefined), [handleClose, isVisible]); useEffect(() => (isVisible ? captureEscKeyListener(handleClose) : undefined), [handleClose, isVisible]);

View File

@ -92,7 +92,7 @@ const Topic: FC<OwnProps & StateProps> = ({
draft, draft,
wasTopicOpened, wasTopicOpened,
}) => { }) => {
const { openChat, deleteTopic, focusLastMessage } = getActions(); const { openThread, deleteTopic, focusLastMessage } = getActions();
const lang = useLang(); const lang = useLang();
@ -140,7 +140,7 @@ const Topic: FC<OwnProps & StateProps> = ({
}); });
const handleOpenTopic = useLastCallback(() => { const handleOpenTopic = useLastCallback(() => {
openChat({ id: chatId, threadId: topic.id, shouldReplaceHistory: true }); openThread({ chatId, threadId: topic.id, shouldReplaceHistory: true });
if (canScrollDown) { if (canScrollDown) {
focusLastMessage(); focusLastMessage();

View File

@ -50,7 +50,7 @@ const ChatMessageResults: FC<OwnProps & StateProps> = ({
onSearchDateSelect, onSearchDateSelect,
onReset, onReset,
}) => { }) => {
const { searchMessagesGlobal, openChat } = getActions(); const { searchMessagesGlobal, openThread } = getActions();
const lang = useLang(); const lang = useLang();
const { isMobile } = useAppLayout(); const { isMobile } = useAppLayout();
@ -68,13 +68,14 @@ const ChatMessageResults: FC<OwnProps & StateProps> = ({
const handleTopicClick = useCallback( const handleTopicClick = useCallback(
(id: number) => { (id: number) => {
openChat({ id: searchChatId, threadId: id, shouldReplaceHistory: true }); if (!searchChatId) return;
openThread({ chatId: searchChatId, threadId: id, shouldReplaceHistory: true });
if (!isMobile) { if (!isMobile) {
onReset(); onReset();
} }
}, },
[openChat, searchChatId, isMobile, onReset], [searchChatId, isMobile, onReset],
); );
const foundMessages = useMemo(() => { const foundMessages = useMemo(() => {

View File

@ -256,7 +256,7 @@ const Main: FC<OwnProps & StateProps> = ({
closePaymentModal, closePaymentModal,
clearReceipt, clearReceipt,
checkAppVersion, checkAppVersion,
openChat, openThread,
toggleLeftColumn, toggleLeftColumn,
loadRecentEmojiStatuses, loadRecentEmojiStatuses,
updatePageTitle, updatePageTitle,
@ -419,8 +419,8 @@ const Main: FC<OwnProps & StateProps> = ({
const parsedLocationHash = parseLocationHash(); const parsedLocationHash = parseLocationHash();
if (!parsedLocationHash) return; if (!parsedLocationHash) return;
openChat({ openThread({
id: parsedLocationHash.chatId, chatId: parsedLocationHash.chatId,
threadId: parsedLocationHash.threadId, threadId: parsedLocationHash.threadId,
type: parsedLocationHash.type, type: parsedLocationHash.type,
}); });

View File

@ -208,7 +208,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
}); });
const handleAsMessagesClick = useLastCallback(() => { const handleAsMessagesClick = useLastCallback(() => {
openChat({ id: chatId, threadId: MAIN_THREAD_ID }); openChat({ id: chatId });
}); });
function handleRequestCall() { function handleRequestCall() {

View File

@ -181,7 +181,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
toggleStatistics, toggleStatistics,
openBoostStatistics, openBoostStatistics,
openGiftPremiumModal, openGiftPremiumModal,
openChatWithInfo, openThreadWithInfo,
openCreateTopicPanel, openCreateTopicPanel,
openEditTopicPanel, openEditTopicPanel,
openChat, openChat,
@ -231,7 +231,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
}); });
const handleViewGroupInfo = useLastCallback(() => { const handleViewGroupInfo = useLastCallback(() => {
openChatWithInfo({ id: chatId, threadId }); openThreadWithInfo({ chatId, threadId });
setShouldCloseFast(!isRightColumnShown); setShouldCloseFast(!isRightColumnShown);
closeMenu(); closeMenu();
}); });

View File

@ -49,7 +49,6 @@ import {
selectScrollOffset, selectScrollOffset,
selectTabState, selectTabState,
selectThreadInfo, selectThreadInfo,
selectThreadTopMessageId,
} from '../../global/selectors'; } from '../../global/selectors';
import animateScroll, { isAnimatingScroll, restartCurrentScrollAnimation } from '../../util/animateScroll'; import animateScroll, { isAnimatingScroll, restartCurrentScrollAnimation } from '../../util/animateScroll';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
@ -83,6 +82,7 @@ type OwnProps = {
chatId: string; chatId: string;
threadId: number; threadId: number;
type: MessageListType; type: MessageListType;
isComments?: boolean;
canPost: boolean; canPost: boolean;
isReady: boolean; isReady: boolean;
onFabToggle: (shouldShow: boolean) => void; onFabToggle: (shouldShow: boolean) => void;
@ -106,18 +106,18 @@ type StateProps = {
messageIds?: number[]; messageIds?: number[];
messagesById?: Record<number, ApiMessage>; messagesById?: Record<number, ApiMessage>;
firstUnreadId?: number; firstUnreadId?: number;
isComments?: boolean;
isViewportNewest?: boolean; isViewportNewest?: boolean;
isRestricted?: boolean; isRestricted?: boolean;
restrictionReason?: ApiRestrictionReason; restrictionReason?: ApiRestrictionReason;
focusingId?: number; focusingId?: number;
isSelectModeActive?: boolean; isSelectModeActive?: boolean;
lastMessage?: ApiMessage; lastMessage?: ApiMessage;
threadTopMessageId?: number;
hasLinkedChat?: boolean; hasLinkedChat?: boolean;
topic?: ApiTopic; topic?: ApiTopic;
noMessageSendingAnimation?: boolean; noMessageSendingAnimation?: boolean;
isServiceNotificationsChat?: boolean; isServiceNotificationsChat?: boolean;
isEmptyThread?: boolean;
isForum?: boolean;
}; };
const MESSAGE_REACTIONS_POLLING_INTERVAL = 20 * 1000; const MESSAGE_REACTIONS_POLLING_INTERVAL = 20 * 1000;
@ -143,6 +143,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
onNotchToggle, onNotchToggle,
isCurrentUserPremium, isCurrentUserPremium,
isChatLoaded, isChatLoaded,
isForum,
isChannelChat, isChannelChat,
isGroupChat, isGroupChat,
canPost, canPost,
@ -158,10 +159,10 @@ const MessageList: FC<OwnProps & StateProps> = ({
isViewportNewest, isViewportNewest,
isRestricted, isRestricted,
restrictionReason, restrictionReason,
isEmptyThread,
focusingId, focusingId,
isSelectModeActive, isSelectModeActive,
lastMessage, lastMessage,
threadTopMessageId,
hasLinkedChat, hasLinkedChat,
withBottomShift, withBottomShift,
withDefaultBg, withDefaultBg,
@ -244,15 +245,20 @@ const MessageList: FC<OwnProps & StateProps> = ({
: ['id']; : ['id'];
return listedMessages.length return listedMessages.length
? groupMessages(orderBy(listedMessages, orderRule), memoUnreadDividerBeforeIdRef.current, isChatWithSelf) ? groupMessages(
orderBy(listedMessages, orderRule),
memoUnreadDividerBeforeIdRef.current,
!isForum ? threadId : undefined,
isChatWithSelf,
)
: undefined; : undefined;
}, [messageIds, messagesById, type, isServiceNotificationsChat, isChatWithSelf]); }, [messageIds, messagesById, type, isServiceNotificationsChat, isForum, threadId, isChatWithSelf]);
useInterval(() => { useInterval(() => {
if (!messageIds || !messagesById || type === 'scheduled') { if (!messageIds || !messagesById || type === 'scheduled') {
return; return;
} }
const ids = messageIds.filter((id) => messagesById[id]?.reactions); const ids = messageIds.filter((id) => messagesById[id]?.reactions?.results.length);
if (!ids.length) return; if (!ids.length) return;
@ -285,7 +291,8 @@ const MessageList: FC<OwnProps & StateProps> = ({
if (!messageIds || !messagesById || threadId !== MAIN_THREAD_ID || type === 'scheduled') { if (!messageIds || !messagesById || threadId !== MAIN_THREAD_ID || type === 'scheduled') {
return; return;
} }
const ids = messageIds.filter((id) => messagesById[id]?.repliesThreadInfo?.isComments const global = getGlobal();
const ids = messageIds.filter((id) => selectThreadInfo(global, chatId, id)?.isCommentsInfo
|| messagesById[id]?.views !== undefined); || messagesById[id]?.views !== undefined);
if (!ids.length) return; if (!ids.length) return;
@ -606,6 +613,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
getContainerHeight={getContainerHeight} getContainerHeight={getContainerHeight}
isViewportNewest={Boolean(isViewportNewest)} isViewportNewest={Boolean(isViewportNewest)}
isUnread={Boolean(firstUnreadId)} isUnread={Boolean(firstUnreadId)}
isEmptyThread={isEmptyThread}
withUsers={withUsers} withUsers={withUsers}
noAvatars={noAvatars} noAvatars={noAvatars}
containerRef={containerRef} containerRef={containerRef}
@ -615,7 +623,6 @@ const MessageList: FC<OwnProps & StateProps> = ({
threadId={threadId} threadId={threadId}
type={type} type={type}
isReady={isReady} isReady={isReady}
threadTopMessageId={threadTopMessageId}
hasLinkedChat={hasLinkedChat} hasLinkedChat={hasLinkedChat}
isSchedule={messageGroups ? type === 'scheduled' : false} isSchedule={messageGroups ? type === 'scheduled' : false}
shouldRenderBotInfo={isBot} shouldRenderBotInfo={isBot}
@ -642,12 +649,10 @@ export default memo(withGlobal<OwnProps>(
const messagesById = type === 'scheduled' const messagesById = type === 'scheduled'
? selectChatScheduledMessages(global, chatId) ? selectChatScheduledMessages(global, chatId)
: selectChatMessages(global, chatId); : selectChatMessages(global, chatId);
const threadTopMessageId = selectThreadTopMessageId(global, chatId, threadId);
const threadInfo = selectThreadInfo(global, chatId, threadId);
if ( if (
threadId !== MAIN_THREAD_ID && !chat?.isForum threadId !== MAIN_THREAD_ID && !chat?.isForum
&& !(messagesById && threadTopMessageId && messagesById[threadTopMessageId]) && !(messagesById && threadId && messagesById[threadId])
) { ) {
return {}; return {};
} }
@ -664,6 +669,7 @@ export default memo(withGlobal<OwnProps>(
const topic = chat.topics?.[threadId]; const topic = chat.topics?.[threadId];
const chatFullInfo = !isUserId(chatId) ? selectChatFullInfo(global, chatId) : undefined; const chatFullInfo = !isUserId(chatId) ? selectChatFullInfo(global, chatId) : undefined;
const isEmptyThread = !selectThreadInfo(global, chatId, threadId)?.messagesCount;
return { return {
isCurrentUserPremium: selectIsCurrentUserPremium(global), isCurrentUserPremium: selectIsCurrentUserPremium(global),
@ -678,16 +684,16 @@ export default memo(withGlobal<OwnProps>(
isBot: Boolean(chatBot), isBot: Boolean(chatBot),
messageIds, messageIds,
messagesById, messagesById,
isComments: Boolean(threadInfo?.originChannelId),
firstUnreadId: selectFirstUnreadId(global, chatId, threadId), firstUnreadId: selectFirstUnreadId(global, chatId, threadId),
isViewportNewest: type !== 'thread' || selectIsViewportNewest(global, chatId, threadId), isViewportNewest: type !== 'thread' || selectIsViewportNewest(global, chatId, threadId),
focusingId, focusingId,
isSelectModeActive: selectIsInSelectMode(global), isSelectModeActive: selectIsInSelectMode(global),
threadTopMessageId,
hasLinkedChat: chatFullInfo ? Boolean(chatFullInfo.linkedChatId) : undefined, hasLinkedChat: chatFullInfo ? Boolean(chatFullInfo.linkedChatId) : undefined,
topic, topic,
noMessageSendingAnimation: !selectPerformanceSettingsValue(global, 'messageSendingAnimations'), noMessageSendingAnimation: !selectPerformanceSettingsValue(global, 'messageSendingAnimations'),
isServiceNotificationsChat: chatId === SERVICE_NOTIFICATIONS_USER_ID, isServiceNotificationsChat: chatId === SERVICE_NOTIFICATIONS_USER_ID,
isForum: chat.isForum,
isEmptyThread,
...(withLastMessageWhenPreloading && { lastMessage }), ...(withLastMessageWhenPreloading && { lastMessage }),
}; };
}, },

View File

@ -41,6 +41,7 @@ interface OwnProps {
isUnread: boolean; isUnread: boolean;
withUsers: boolean; withUsers: boolean;
isChannelChat: boolean | undefined; isChannelChat: boolean | undefined;
isEmptyThread?: boolean;
isComments?: boolean; isComments?: boolean;
noAvatars: boolean; noAvatars: boolean;
containerRef: RefObject<HTMLDivElement>; containerRef: RefObject<HTMLDivElement>;
@ -49,7 +50,6 @@ interface OwnProps {
memoFirstUnreadIdRef: { current: number | undefined }; memoFirstUnreadIdRef: { current: number | undefined };
type: MessageListType; type: MessageListType;
isReady: boolean; isReady: boolean;
threadTopMessageId: number | undefined;
hasLinkedChat: boolean | undefined; hasLinkedChat: boolean | undefined;
isSchedule: boolean; isSchedule: boolean;
shouldRenderBotInfo?: boolean; shouldRenderBotInfo?: boolean;
@ -71,6 +71,7 @@ const MessageListContent: FC<OwnProps> = ({
isViewportNewest, isViewportNewest,
isUnread, isUnread,
isComments, isComments,
isEmptyThread,
withUsers, withUsers,
isChannelChat, isChannelChat,
noAvatars, noAvatars,
@ -80,7 +81,6 @@ const MessageListContent: FC<OwnProps> = ({
memoFirstUnreadIdRef, memoFirstUnreadIdRef,
type, type,
isReady, isReady,
threadTopMessageId,
hasLinkedChat, hasLinkedChat,
isSchedule, isSchedule,
shouldRenderBotInfo, shouldRenderBotInfo,
@ -193,6 +193,7 @@ const MessageListContent: FC<OwnProps> = ({
const documentGroupId = !isMessageAlbum && message.groupedId ? message.groupedId : undefined; const documentGroupId = !isMessageAlbum && message.groupedId ? message.groupedId : undefined;
const nextDocumentGroupId = nextMessage && !isAlbum(nextMessage) ? nextMessage.groupedId : undefined; const nextDocumentGroupId = nextMessage && !isAlbum(nextMessage) ? nextMessage.groupedId : undefined;
const isTopicTopMessage = message.id === threadId;
const position = { const position = {
isFirstInGroup: messageIndex === 0, isFirstInGroup: messageIndex === 0,
@ -214,8 +215,6 @@ const MessageListContent: FC<OwnProps> = ({
const noComments = hasLinkedChat === false || !isChannelChat; const noComments = hasLinkedChat === false || !isChannelChat;
const isTopicTopMessage = message.id === threadTopMessageId;
return compact([ return compact([
message.id === memoUnreadDividerBeforeIdRef.current && unreadDivider, message.id === memoUnreadDividerBeforeIdRef.current && unreadDivider,
<Message <Message
@ -243,9 +242,11 @@ const MessageListContent: FC<OwnProps> = ({
onPinnedIntersectionChange={onPinnedIntersectionChange} onPinnedIntersectionChange={onPinnedIntersectionChange}
getIsMessageListReady={getIsReady} getIsMessageListReady={getIsReady}
/>, />,
message.id === threadTopMessageId && ( message.id === threadId && (
<div className="local-action-message" key="discussion-started"> <div className="local-action-message" key="discussion-started">
<span>{lang('DiscussionStarted')}</span> <span>{lang(isEmptyThread
? (isComments ? 'NoComments' : 'NoReplies') : 'DiscussionStarted')}
</span>
</div> </div>
), ),
]); ]);

View File

@ -53,7 +53,6 @@ import {
selectTabState, selectTabState,
selectTheme, selectTheme,
selectThreadInfo, selectThreadInfo,
selectThreadTopMessageId,
} from '../../global/selectors'; } from '../../global/selectors';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import buildStyle from '../../util/buildStyle'; import buildStyle from '../../util/buildStyle';
@ -103,6 +102,7 @@ interface OwnProps {
type StateProps = { type StateProps = {
chatId?: string; chatId?: string;
threadId?: number; threadId?: number;
isComments?: boolean;
messageListType?: MessageListType; messageListType?: MessageListType;
chat?: ApiChat; chat?: ApiChat;
draftReplyInfo?: ApiInputMessageReplyInfo; draftReplyInfo?: ApiInputMessageReplyInfo;
@ -157,6 +157,7 @@ function MiddleColumn({
leftColumnRef, leftColumnRef,
chatId, chatId,
threadId, threadId,
isComments,
messageListType, messageListType,
isMobile, isMobile,
chat, chat,
@ -510,6 +511,7 @@ function MiddleColumn({
chatId={renderingChatId!} chatId={renderingChatId!}
threadId={renderingThreadId!} threadId={renderingThreadId!}
messageListType={renderingMessageListType!} messageListType={renderingMessageListType!}
isComments={isComments}
isReady={isReady} isReady={isReady}
isMobile={isMobile} isMobile={isMobile}
getCurrentPinnedIndexes={getCurrentPinnedIndexes} getCurrentPinnedIndexes={getCurrentPinnedIndexes}
@ -528,6 +530,7 @@ function MiddleColumn({
chatId={renderingChatId!} chatId={renderingChatId!}
threadId={renderingThreadId!} threadId={renderingThreadId!}
type={renderingMessageListType!} type={renderingMessageListType!}
isComments={isComments}
canPost={renderingCanPost!} canPost={renderingCanPost!}
hasTools={renderingHasTools} hasTools={renderingHasTools}
onFabToggle={setIsFabShown} onFabToggle={setIsFabShown}
@ -734,8 +737,8 @@ export default memo(withGlobal<OwnProps>(
const { chatId: audioChatId, messageId: audioMessageId } = audioPlayer; const { chatId: audioChatId, messageId: audioMessageId } = audioPlayer;
const threadInfo = selectThreadInfo(global, chatId, threadId); const threadInfo = selectThreadInfo(global, chatId, threadId);
const isComments = Boolean(threadInfo?.originChannelId); const isMessageThread = Boolean(!threadInfo?.isCommentsInfo && threadInfo?.fromChannelId);
const canPost = chat && getCanPostInChat(chat, threadId, isComments); const canPost = chat && getCanPostInChat(chat, threadId, isMessageThread);
const isBotNotStarted = selectIsChatBotNotStarted(global, chatId); const isBotNotStarted = selectIsChatBotNotStarted(global, chatId);
const isPinnedMessageList = messageListType === 'pinned'; const isPinnedMessageList = messageListType === 'pinned';
const isMainThread = messageListType === 'thread' && threadId === MAIN_THREAD_ID; const isMainThread = messageListType === 'thread' && threadId === MAIN_THREAD_ID;
@ -761,7 +764,7 @@ export default memo(withGlobal<OwnProps>(
: undefined; : undefined;
const isCommentThread = threadId !== MAIN_THREAD_ID && !chat?.isForum; const isCommentThread = threadId !== MAIN_THREAD_ID && !chat?.isForum;
const topMessageId = isCommentThread ? selectThreadTopMessageId(global, chatId, threadId) : undefined; const topMessageId = isCommentThread ? threadId : undefined;
const canUnpin = chat && ( const canUnpin = chat && (
isPrivate || ( isPrivate || (
@ -779,6 +782,7 @@ export default memo(withGlobal<OwnProps>(
draftReplyInfo, draftReplyInfo,
isPrivate, isPrivate,
areChatSettingsLoaded: Boolean(chat?.settings), areChatSettingsLoaded: Boolean(chat?.settings),
isComments: isMessageThread,
canPost: !isPinnedMessageList canPost: !isPinnedMessageList
&& (!chat || canPost) && (!chat || canPost)
&& !isBotNotStarted && !isBotNotStarted

View File

@ -47,7 +47,6 @@ import {
selectTabState, selectTabState,
selectThreadInfo, selectThreadInfo,
selectThreadParam, selectThreadParam,
selectThreadTopMessageId,
} from '../../global/selectors'; } from '../../global/selectors';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import cycleRestrict from '../../util/cycleRestrict'; import cycleRestrict from '../../util/cycleRestrict';
@ -86,6 +85,7 @@ type OwnProps = {
chatId: string; chatId: string;
threadId: number; threadId: number;
messageListType: MessageListType; messageListType: MessageListType;
isComments?: boolean;
isReady?: boolean; isReady?: boolean;
isMobile?: boolean; isMobile?: boolean;
getCurrentPinnedIndexes: Signal<Record<string, number>>; getCurrentPinnedIndexes: Signal<Record<string, number>>;
@ -105,7 +105,6 @@ type StateProps = {
isRightColumnShown?: boolean; isRightColumnShown?: boolean;
audioMessage?: ApiMessage; audioMessage?: ApiMessage;
messagesCount?: number; messagesCount?: number;
isComments?: boolean;
isChatWithSelf?: boolean; isChatWithSelf?: boolean;
hasButtonInHeader?: boolean; hasButtonInHeader?: boolean;
shouldSkipHistoryAnimations?: boolean; shouldSkipHistoryAnimations?: boolean;
@ -147,7 +146,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
onFocusPinnedMessage, onFocusPinnedMessage,
}) => { }) => {
const { const {
openChatWithInfo, openThreadWithInfo,
pinMessage, pinMessage,
focusMessage, focusMessage,
openChat, openChat,
@ -156,6 +155,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
toggleLeftColumn, toggleLeftColumn,
exitMessageSelectMode, exitMessageSelectMode,
openPremiumModal, openPremiumModal,
openThread,
} = getActions(); } = getActions();
const lang = useLang(); const lang = useLang();
@ -197,7 +197,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
} = useFastClick((e: React.MouseEvent<HTMLDivElement | HTMLButtonElement>) => { } = useFastClick((e: React.MouseEvent<HTMLDivElement | HTMLButtonElement>) => {
if (e.type === 'mousedown' && (e.target as Element).closest('.title > .custom-emoji')) return; if (e.type === 'mousedown' && (e.target as Element).closest('.title > .custom-emoji')) return;
openChatWithInfo({ id: chatId, threadId }); openThreadWithInfo({ chatId, threadId });
}); });
const handleUnpinMessage = useLastCallback((messageId: number) => { const handleUnpinMessage = useLastCallback((messageId: number) => {
@ -217,7 +217,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
}); });
const handleAllPinnedClick = useLastCallback(() => { const handleAllPinnedClick = useLastCallback(() => {
openChat({ id: chatId, threadId, type: 'pinned' }); openThread({ chatId, threadId, type: 'pinned' });
}); });
const setBackButtonActive = useLastCallback(() => { const setBackButtonActive = useLastCallback(() => {
@ -354,7 +354,9 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
<h3> <h3>
{messagesCount !== undefined ? ( {messagesCount !== undefined ? (
messageListType === 'thread' ? ( messageListType === 'thread' ? (
lang(isComments ? 'CommentsCount' : 'Replies', messagesCount, 'i')) (messagesCount
? lang(isComments ? 'Comments' : 'Replies', messagesCount, 'i')
: lang(isComments ? 'CommentsTitle' : 'RepliesTitle')))
: messageListType === 'pinned' ? (lang('PinnedMessagesCount', messagesCount, 'i')) : messageListType === 'pinned' ? (lang('PinnedMessagesCount', messagesCount, 'i'))
: messageListType === 'scheduled' ? ( : messageListType === 'scheduled' ? (
isChatWithSelf ? lang('Reminders') : lang('messages', messagesCount, 'i') isChatWithSelf ? lang('Reminders') : lang('messages', messagesCount, 'i')
@ -560,10 +562,9 @@ export default memo(withGlobal<OwnProps>(
} }
if (threadId !== MAIN_THREAD_ID && !chat?.isForum) { if (threadId !== MAIN_THREAD_ID && !chat?.isForum) {
const pinnedMessageId = selectThreadTopMessageId(global, chatId, threadId); const pinnedMessageId = threadId;
const message = pinnedMessageId ? selectChatMessage(global, chatId, pinnedMessageId) : undefined; const message = pinnedMessageId ? selectChatMessage(global, chatId, pinnedMessageId) : undefined;
const topMessageSender = message ? selectForwardedSender(global, message) : undefined; const topMessageSender = message ? selectForwardedSender(global, message) : undefined;
const threadInfo = selectThreadInfo(global, chatId, threadId);
return { return {
...state, ...state,
@ -571,7 +572,6 @@ export default memo(withGlobal<OwnProps>(
messagesById, messagesById,
canUnpin: false, canUnpin: false,
topMessageSender, topMessageSender,
isComments: Boolean(threadInfo?.originChannelId),
}; };
} }

View File

@ -18,7 +18,9 @@ export function isAlbum(messageOrAlbum: ApiMessage | IAlbum): messageOrAlbum is
return 'albumId' in messageOrAlbum; return 'albumId' in messageOrAlbum;
} }
export function groupMessages(messages: ApiMessage[], firstUnreadId?: number, isChatWithSelf = false) { export function groupMessages(
messages: ApiMessage[], firstUnreadId?: number, topMessageId?: number, isChatWithSelf?: boolean,
) {
let currentSenderGroup: SenderGroup = []; let currentSenderGroup: SenderGroup = [];
let currentDateGroup = { let currentDateGroup = {
originalDate: messages[0].date, originalDate: messages[0].date,
@ -39,7 +41,7 @@ export function groupMessages(messages: ApiMessage[], firstUnreadId?: number, is
}; };
} else { } else {
currentAlbum.messages.push(message); currentAlbum.messages.push(message);
if (message.content.text) { if (message.hasComments || (message.content.text && !currentAlbum.mainMessage.hasComments)) {
currentAlbum.mainMessage = message; currentAlbum.mainMessage = message;
} }
} }
@ -56,6 +58,7 @@ export function groupMessages(messages: ApiMessage[], firstUnreadId?: number, is
currentSenderGroup.push(currentAlbum); currentSenderGroup.push(currentAlbum);
currentAlbum = undefined; currentAlbum = undefined;
} }
const lastSenderGroupItem = currentSenderGroup[currentSenderGroup.length - 1];
if (nextMessage) { if (nextMessage) {
const nextMessageDayStartsAt = getDayStartAt(nextMessage.date * 1000); const nextMessageDayStartsAt = getDayStartAt(nextMessage.date * 1000);
if (currentDateGroup.datetime !== nextMessageDayStartsAt) { if (currentDateGroup.datetime !== nextMessageDayStartsAt) {
@ -77,6 +80,11 @@ export function groupMessages(messages: ApiMessage[], firstUnreadId?: number, is
|| message.inlineButtons || message.inlineButtons
|| nextMessage.inlineButtons || nextMessage.inlineButtons
|| (nextMessage.date - message.date) > GROUP_INTERVAL_SECONDS || (nextMessage.date - message.date) > GROUP_INTERVAL_SECONDS
|| (topMessageId
&& (message.id === topMessageId
|| (lastSenderGroupItem
&& 'mainMessage' in lastSenderGroupItem && lastSenderGroupItem.mainMessage?.id === topMessageId))
&& nextMessage.id !== topMessageId)
|| (isChatWithSelf && message.forwardInfo?.senderUserId !== nextMessage.forwardInfo?.senderUserId) || (isChatWithSelf && message.forwardInfo?.senderUserId !== nextMessage.forwardInfo?.senderUserId)
) { ) {
currentSenderGroup = []; currentSenderGroup = [];

View File

@ -7,7 +7,6 @@ import type { Signal } from '../../../util/signals';
import { LoadMoreDirection } from '../../../types'; import { LoadMoreDirection } from '../../../types';
import { requestMeasure } from '../../../lib/fasterdom/fasterdom'; import { requestMeasure } from '../../../lib/fasterdom/fasterdom';
import { isLocalMessageId } from '../../../global/helpers';
import { debounce } from '../../../util/schedulers'; import { debounce } from '../../../util/schedulers';
import { MESSAGE_LIST_SENSITIVE_AREA } from '../../../util/windowEnvironment'; import { MESSAGE_LIST_SENSITIVE_AREA } from '../../../util/windowEnvironment';
@ -92,12 +91,6 @@ export default function useScrollHooks(
return; return;
} }
// Loading history while sending a message can return the same message and cause ambiguity
const isFirstMessageLocal = isLocalMessageId(messageIds[0]);
if (isFirstMessageLocal) {
return;
}
entries.forEach(({ isIntersecting, target }) => { entries.forEach(({ isIntersecting, target }) => {
if (!isIntersecting) return; if (!isIntersecting) return;

View File

@ -20,6 +20,11 @@
transition: background-color 0.15s, color 0.15s; transition: background-color 0.15s, color 0.15s;
user-select: none; user-select: none;
.label {
overflow: hidden;
text-overflow: ellipsis;
}
.Message .has-appendix &::before { .Message .has-appendix &::before {
content: ""; content: "";
display: block; display: block;
@ -45,7 +50,7 @@
bottom: 3rem; bottom: 3rem;
height: 3.375rem; height: 3.375rem;
border-radius: 1.375rem; border-radius: 1.375rem;
padding: 0.375rem 0.3125rem 0.25rem; padding: 0.375rem;
align-items: flex-start; align-items: flex-start;
color: white; color: white;
background-color: var(--pattern-color); background-color: var(--pattern-color);
@ -66,7 +71,7 @@
} }
} }
.Message:hover & { .Message:hover &, &.loading {
opacity: 1; opacity: 1;
} }
@ -97,7 +102,7 @@
.recent-repliers, .recent-repliers,
.icon-comments, .icon-comments,
.label, .label,
.icon-next { .CommentButton_icon-open {
display: none; display: none;
} }
} }
@ -153,8 +158,8 @@
margin-inline-end: 0.875rem; margin-inline-end: 0.875rem;
} }
.icon-next { .CommentButton_icon-open {
margin-inline-start: auto; position: absolute;
font-size: 1.5rem; font-size: 1.5rem;
} }
@ -210,3 +215,34 @@
pointer-events: none; pointer-events: none;
} }
} }
.CommentButton_loading, .CommentButton_icon-open, .CommentButton_icon-comments {
transition: transform 250ms ease-in-out, opacity 250ms ease-in-out;
}
.CommentButton_icon-open {
right: 0;
}
.CommentButton_loading {
position: absolute;
--spinner-size: 1.5rem;
flex-shrink: 0;
right: 0.5rem;
.CommentButton-custom-shape & {
right: 0;
}
}
.CommentButton_right {
position: relative;
margin-inline-start: auto;
height: 1.5rem;
width: 2.5rem;
}
.CommentButton_hidden {
opacity: 0;
transform: scale(0.4);
}

View File

@ -2,9 +2,7 @@ import type { FC } from '../../../lib/teact/teact';
import React, { memo, useMemo } from '../../../lib/teact/teact'; import React, { memo, useMemo } from '../../../lib/teact/teact';
import { getActions, getGlobal } from '../../../global'; import { getActions, getGlobal } from '../../../global';
import type { import type { ApiCommentsInfo } from '../../../api/types';
ApiThreadInfo,
} from '../../../api/types';
import { selectPeer } from '../../../global/selectors'; import { selectPeer } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
@ -12,30 +10,42 @@ import { formatIntegerCompact } from '../../../util/textFormat';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
import useAsyncRendering from '../../right/hooks/useAsyncRendering';
import AnimatedCounter from '../../common/AnimatedCounter'; import AnimatedCounter from '../../common/AnimatedCounter';
import Avatar from '../../common/Avatar'; import Avatar from '../../common/Avatar';
import Spinner from '../../ui/Spinner';
import './CommentButton.scss'; import './CommentButton.scss';
type OwnProps = { type OwnProps = {
threadInfo: ApiThreadInfo; threadInfo: ApiCommentsInfo;
disabled?: boolean; disabled?: boolean;
isLoading?: boolean;
isCustomShape?: boolean;
}; };
const SHOW_LOADER_DELAY = 450;
const CommentButton: FC<OwnProps> = ({ const CommentButton: FC<OwnProps> = ({
isCustomShape,
threadInfo, threadInfo,
disabled, disabled,
isLoading,
}) => { }) => {
const { openComments } = getActions(); const { openThread } = getActions();
const shouldRenderLoading = useAsyncRendering([isLoading], SHOW_LOADER_DELAY);
const lang = useLang(); const lang = useLang();
const { const {
threadId, chatId, messagesCount, lastMessageId, lastReadInboxMessageId, recentReplierIds, originChannelId, originMessageId, chatId, messagesCount, lastMessageId, lastReadInboxMessageId, recentReplierIds, originChannelId,
} = threadInfo; } = threadInfo;
const handleClick = useLastCallback(() => { const handleClick = useLastCallback(() => {
openComments({ id: chatId, threadId, originChannelId }); openThread({
isComments: true, chatId, originMessageId, originChannelId,
});
}); });
const recentRepliers = useMemo(() => { const recentRepliers = useMemo(() => {
@ -73,7 +83,7 @@ const CommentButton: FC<OwnProps> = ({
const hasUnread = Boolean(lastReadInboxMessageId && lastMessageId && lastReadInboxMessageId < lastMessageId); const hasUnread = Boolean(lastReadInboxMessageId && lastMessageId && lastReadInboxMessageId < lastMessageId);
const commentsText = messagesCount ? (lang('Comments', '%COMMENTS_COUNT%', undefined, messagesCount) as string) const commentsText = messagesCount ? (lang('CommentsCount', '%COMMENTS_COUNT%', undefined, messagesCount) as string)
.split('%') .split('%')
.map((s) => { .map((s) => {
return (s === 'COMMENTS_COUNT' ? <AnimatedCounter text={formatIntegerCompact(messagesCount)} /> : s); return (s === 'COMMENTS_COUNT' ? <AnimatedCounter text={formatIntegerCompact(messagesCount)} /> : s);
@ -83,17 +93,48 @@ const CommentButton: FC<OwnProps> = ({
return ( return (
<div <div
data-cnt={formatIntegerCompact(messagesCount)} data-cnt={formatIntegerCompact(messagesCount)}
className={buildClassName('CommentButton', hasUnread && 'has-unread', disabled && 'disabled')} className={buildClassName(
'CommentButton',
hasUnread && 'has-unread',
disabled && 'disabled',
isCustomShape && 'CommentButton-custom-shape',
isLoading && 'loading',
)}
dir={lang.isRtl ? 'rtl' : 'ltr'} dir={lang.isRtl ? 'rtl' : 'ltr'}
onClick={handleClick} onClick={handleClick}
role="button"
tabIndex={0}
> >
<i className="icon icon-comments-sticker" /> <i
{(!recentRepliers || recentRepliers.length === 0) && <i className="icon icon-comments" />} className={buildClassName(
'CommentButton_icon-comments icon icon-comments-sticker',
isLoading && shouldRenderLoading && 'CommentButton_hidden',
)}
aria-hidden
/>
{!recentRepliers?.length && <i className="icon icon-comments" aria-hidden />}
{renderRecentRepliers()} {renderRecentRepliers()}
<div className="label" dir="auto"> <div className="label" dir="auto">
{messagesCount ? commentsText : lang('LeaveAComment')} {messagesCount ? commentsText : lang('LeaveAComment')}
</div> </div>
<i className="icon icon-next" /> <div className="CommentButton_right">
{isLoading && (
<Spinner
className={buildClassName(
'CommentButton_loading',
!shouldRenderLoading && 'CommentButton_hidden',
)}
color={isCustomShape ? 'white' : 'blue'}
/>
) }
<i
className={buildClassName(
'CommentButton_icon-open icon icon-next',
isLoading && shouldRenderLoading && 'CommentButton_hidden',
)}
aria-hidden
/>
</div>
</div> </div>
); );
}; };

View File

@ -166,7 +166,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
onCloseAnimationEnd, onCloseAnimationEnd,
}) => { }) => {
const { const {
openChat, openThread,
updateDraftReplyInfo, updateDraftReplyInfo,
setEditingId, setEditingId,
pinMessage, pinMessage,
@ -301,8 +301,8 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
}); });
const handleOpenThread = useLastCallback(() => { const handleOpenThread = useLastCallback(() => {
openChat({ openThread({
id: message.chatId, chatId: message.chatId,
threadId: message.id, threadId: message.id,
}); });
closeMenu(); closeMenu();

View File

@ -714,6 +714,10 @@
} }
} }
.message-action-button-shown {
opacity: 1;
}
&.own .message-action-button { &.own .message-action-button {
left: -3rem; left: -3rem;
} }

View File

@ -88,7 +88,6 @@ import {
selectTabState, selectTabState,
selectTheme, selectTheme,
selectThreadInfo, selectThreadInfo,
selectThreadTopMessageId,
selectTopicFromMessage, selectTopicFromMessage,
selectUploadProgress, selectUploadProgress,
selectUser, selectUser,
@ -271,6 +270,7 @@ type StateProps = {
withStickerEffects?: boolean; withStickerEffects?: boolean;
webPageStory?: ApiTypeStory; webPageStory?: ApiTypeStory;
isConnected: boolean; isConnected: boolean;
isLoadingComments?: boolean;
shouldWarnAboutSvg?: boolean; shouldWarnAboutSvg?: boolean;
}; };
@ -334,6 +334,7 @@ const Message: FC<OwnProps & StateProps> = ({
outgoingStatus, outgoingStatus,
uploadProgress, uploadProgress,
isInDocumentGroup, isInDocumentGroup,
isLoadingComments,
isProtected, isProtected,
isChatProtected, isChatProtected,
isFocused, isFocused,
@ -663,7 +664,8 @@ const Message: FC<OwnProps & StateProps> = ({
&& !isInDocumentGroupNotLast && !isInDocumentGroupNotLast
&& messageListType === 'thread' && messageListType === 'thread'
&& !noComments; && !noComments;
const withCommentButton = repliesThreadInfo && !isInDocumentGroupNotLast && messageListType === 'thread' const withCommentButton = repliesThreadInfo?.isCommentsInfo
&& !isInDocumentGroupNotLast && messageListType === 'thread'
&& !noComments; && !noComments;
const withQuickReactionButton = !isTouchScreen && !phoneCall && !isInSelectMode && defaultReaction const withQuickReactionButton = !isTouchScreen && !phoneCall && !isInSelectMode && defaultReaction
&& !isInDocumentGroupNotLast && !isStoryMention; && !isInDocumentGroupNotLast && !isStoryMention;
@ -719,7 +721,7 @@ const Message: FC<OwnProps & StateProps> = ({
replyToMsgId, replyToMsgId,
replyMessage, replyMessage,
message.id, message.id,
isQuote || isReplyPrivate, shouldHideReply || isQuote || isReplyPrivate,
); );
useEnsureStory( useEnsureStory(
@ -1370,7 +1372,9 @@ const Message: FC<OwnProps & StateProps> = ({
{!isInDocumentGroupNotLast && metaPosition === 'standalone' && !isStoryMention && renderReactionsAndMeta()} {!isInDocumentGroupNotLast && metaPosition === 'standalone' && !isStoryMention && renderReactionsAndMeta()}
{canShowActionButton && canForward ? ( {canShowActionButton && canForward ? (
<Button <Button
className="message-action-button" className={buildClassName(
'message-action-button', isLoadingComments && 'message-action-button-shown',
)}
color="translucent-white" color="translucent-white"
round round
size="tiny" size="tiny"
@ -1381,7 +1385,9 @@ const Message: FC<OwnProps & StateProps> = ({
</Button> </Button>
) : canShowActionButton && canFocus ? ( ) : canShowActionButton && canFocus ? (
<Button <Button
className="message-action-button" className={buildClassName(
'message-action-button', isLoadingComments && 'message-action-button-shown',
)}
color="translucent-white" color="translucent-white"
round round
size="tiny" size="tiny"
@ -1391,7 +1397,14 @@ const Message: FC<OwnProps & StateProps> = ({
<i className="icon icon-arrow-right" /> <i className="icon icon-arrow-right" />
</Button> </Button>
) : undefined} ) : undefined}
{withCommentButton && <CommentButton threadInfo={repliesThreadInfo!} disabled={noComments} />} {withCommentButton && (
<CommentButton
threadInfo={repliesThreadInfo}
disabled={noComments}
isLoading={isLoadingComments}
isCustomShape={isCustomShape}
/>
)}
{withAppendix && <MessageAppendix isOwn={isOwn} />} {withAppendix && <MessageAppendix isOwn={isOwn} />}
{withQuickReactionButton && quickReactionPosition === 'in-content' && renderQuickReactionButton()} {withQuickReactionButton && quickReactionPosition === 'in-content' && renderQuickReactionButton()}
</div> </div>
@ -1430,13 +1443,14 @@ const Message: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global, ownProps): StateProps => { (global, ownProps): StateProps => {
const { const {
focusedMessage, forwardMessages, activeEmojiInteractions, activeReactions, focusedMessage, forwardMessages, activeReactions, activeEmojiInteractions,
loadingThread,
} = selectTabState(global); } = selectTabState(global);
const { const {
message, album, withSenderName, withAvatar, threadId, messageListType, isLastInDocumentGroup, isFirstInGroup, message, album, withSenderName, withAvatar, threadId, messageListType, isLastInDocumentGroup, isFirstInGroup,
} = ownProps; } = ownProps;
const { const {
id, chatId, viaBotId, isOutgoing, forwardInfo, transcriptionId, isPinned, repliesThreadInfo, id, chatId, viaBotId, isOutgoing, forwardInfo, transcriptionId, isPinned,
} = message; } = message;
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
@ -1460,16 +1474,13 @@ export default memo(withGlobal<OwnProps>(
? chatFullInfo?.adminMembersById?.[sender?.id] ? chatFullInfo?.adminMembersById?.[sender?.id]
: undefined; : undefined;
const threadTopMessageId = threadId ? selectThreadTopMessageId(global, chatId, threadId) : undefined; const isThreadTop = message.id === threadId;
const isThreadTop = message.id === threadTopMessageId;
const { replyToMsgId, replyToPeerId, replyFrom } = getMessageReplyInfo(message) || {}; const { replyToMsgId, replyToPeerId, replyFrom } = getMessageReplyInfo(message) || {};
const { userId: storyReplyUserId, storyId: storyReplyId } = getStoryReplyInfo(message) || {}; const { userId: storyReplyUserId, storyId: storyReplyId } = getStoryReplyInfo(message) || {};
const shouldHideReply = replyToMsgId && replyToMsgId === threadTopMessageId; const shouldHideReply = replyToMsgId && replyToMsgId === threadId;
const replyMessage = replyToMsgId && !shouldHideReply const replyMessage = replyToMsgId ? selectChatMessage(global, replyToPeerId || chatId, replyToMsgId) : undefined;
? selectChatMessage(global, replyToPeerId || chatId, replyToMsgId)
: undefined;
const forwardHeader = forwardInfo || replyFrom; const forwardHeader = forwardInfo || replyFrom;
const replyMessageSender = replyMessage ? selectReplySender(global, replyMessage) : forwardHeader && !isRepliesChat const replyMessageSender = replyMessage ? selectReplySender(global, replyMessage) : forwardHeader && !isRepliesChat
? selectSenderFromHeader(global, forwardHeader) : undefined; ? selectSenderFromHeader(global, forwardHeader) : undefined;
@ -1509,9 +1520,8 @@ export default memo(withGlobal<OwnProps>(
const { canReply } = (messageListType === 'thread' && selectAllowedMessageActions(global, message, threadId)) || {}; const { canReply } = (messageListType === 'thread' && selectAllowedMessageActions(global, message, threadId)) || {};
const isDownloading = selectIsDownloading(global, message); const isDownloading = selectIsDownloading(global, message);
const actualRepliesThreadInfo = repliesThreadInfo
? selectThreadInfo(global, repliesThreadInfo.chatId, repliesThreadInfo.threadId) || repliesThreadInfo const repliesThreadInfo = selectThreadInfo(global, chatId, album?.mainMessage.id || id);
: undefined;
const isInDocumentGroup = Boolean(message.groupedId) && !message.isInAlbum; const isInDocumentGroup = Boolean(message.groupedId) && !message.isInAlbum;
const documentGroupFirstMessageId = isInDocumentGroup const documentGroupFirstMessageId = isInDocumentGroup
@ -1584,7 +1594,7 @@ export default memo(withGlobal<OwnProps>(
canAutoPlayMedia: selectCanAutoPlayMedia(global, message), canAutoPlayMedia: selectCanAutoPlayMedia(global, message),
autoLoadFileMaxSizeMb: global.settings.byKey.autoLoadFileMaxSizeMb, autoLoadFileMaxSizeMb: global.settings.byKey.autoLoadFileMaxSizeMb,
shouldLoopStickers: selectShouldLoopStickers(global), shouldLoopStickers: selectShouldLoopStickers(global),
repliesThreadInfo: actualRepliesThreadInfo, repliesThreadInfo,
availableReactions: global.availableReactions, availableReactions: global.availableReactions,
defaultReaction: isMessageLocal(message) || messageListType === 'scheduled' defaultReaction: isMessageLocal(message) || messageListType === 'scheduled'
? undefined : selectDefaultReaction(global, chatId), ? undefined : selectDefaultReaction(global, chatId),
@ -1606,6 +1616,9 @@ export default memo(withGlobal<OwnProps>(
withStickerEffects: selectPerformanceSettingsValue(global, 'stickerEffects'), withStickerEffects: selectPerformanceSettingsValue(global, 'stickerEffects'),
webPageStory, webPageStory,
isConnected, isConnected,
isLoadingComments: repliesThreadInfo?.isCommentsInfo
&& loadingThread?.loadingChatId === repliesThreadInfo?.originChannelId
&& loadingThread?.loadingMessageId === repliesThreadInfo?.originMessageId,
shouldWarnAboutSvg: global.settings.byKey.shouldWarnAboutSvg, shouldWarnAboutSvg: global.settings.byKey.shouldWarnAboutSvg,
...(isOutgoing && { outgoingStatus: selectOutgoingStatus(global, message, messageListType === 'scheduled') }), ...(isOutgoing && { outgoingStatus: selectOutgoingStatus(global, message, messageListType === 'scheduled') }),
...(typeof uploadProgress === 'number' && { uploadProgress }), ...(typeof uploadProgress === 'number' && { uploadProgress }),

View File

@ -35,7 +35,7 @@ export default function useInnerHandlers(
const { const {
openChat, showNotification, focusMessage, openMediaViewer, openAudioPlayer, openChat, showNotification, focusMessage, openMediaViewer, openAudioPlayer,
markMessagesRead, cancelSendingMessage, sendPollVote, openForwardMenu, markMessagesRead, cancelSendingMessage, sendPollVote, openForwardMenu,
openChatLanguageModal, openStoryViewer, focusMessageInComments, openChatLanguageModal, openThread, openStoryViewer,
} = getActions(); } = getActions();
const { const {
@ -156,7 +156,7 @@ export default function useInnerHandlers(
} }
if (replyToPeerId && replyToTopId) { if (replyToPeerId && replyToTopId) {
focusMessageInComments({ focusMessage({
chatId: replyToPeerId, chatId: replyToPeerId,
threadId: replyToTopId, threadId: replyToTopId,
messageId: forwardInfo!.fromMessageId!, messageId: forwardInfo!.fromMessageId!,
@ -181,8 +181,8 @@ export default function useInnerHandlers(
}); });
const handleOpenThread = useLastCallback(() => { const handleOpenThread = useLastCallback(() => {
openChat({ openThread({
id: message.chatId, chatId: message.chatId,
threadId: message.id, threadId: message.id,
}); });
}); });

View File

@ -169,8 +169,8 @@ export default memo(withGlobal(
const isChatWithBot = chat ? selectIsChatWithBot(global, chat) : undefined; const isChatWithBot = chat ? selectIsChatWithBot(global, chat) : undefined;
const isSavedMessages = Boolean(chatId) && selectIsChatWithSelf(global, chatId); const isSavedMessages = Boolean(chatId) && selectIsChatWithSelf(global, chatId);
const threadInfo = chatId && threadId ? selectThreadInfo(global, chatId, threadId) : undefined; const threadInfo = chatId && threadId ? selectThreadInfo(global, chatId, threadId) : undefined;
const isComments = Boolean(threadInfo?.originChannelId); const isMessageThread = Boolean(!threadInfo?.isCommentsInfo && threadInfo?.fromChannelId);
const canPostInChat = Boolean(chat) && Boolean(threadId) && getCanPostInChat(chat, threadId, isComments); const canPostInChat = Boolean(chat) && Boolean(threadId) && getCanPostInChat(chat, threadId, isMessageThread);
return { return {
query, query,

View File

@ -1,9 +1,10 @@
import type {
ApiChat, ApiChatType, ApiContact, ApiInputMessageReplyInfo, ApiPeer, ApiUrlAuthResult,
} from '../../../api/types';
import type { InlineBotSettings } from '../../../types'; import type { InlineBotSettings } from '../../../types';
import type { RequiredGlobalActions } from '../../index'; import type { RequiredGlobalActions } from '../../index';
import type { ActionReturnType, GlobalState, TabArgs } from '../../types'; import type { ActionReturnType, GlobalState, TabArgs } from '../../types';
import {
type ApiChat, type ApiChatType, type ApiContact, type ApiInputMessageReplyInfo, type ApiPeer, type ApiUrlAuthResult,
MAIN_THREAD_ID,
} from '../../../api/types';
import { GENERAL_REFETCH_INTERVAL } from '../../../config'; import { GENERAL_REFETCH_INTERVAL } from '../../../config';
import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { getCurrentTabId } from '../../../util/establishMultitabRole';
@ -793,8 +794,8 @@ addActionHandler('callAttachBot', (global, actions, payload): ActionReturnType =
} }
if ('chatId' in payload) { if ('chatId' in payload) {
const { chatId, threadId, url } = payload; const { chatId, threadId = MAIN_THREAD_ID, url } = payload;
actions.openChat({ id: chatId, threadId, tabId }); actions.openThread({ chatId, threadId, tabId });
actions.requestWebView({ actions.requestWebView({
url, url,
peerId: chatId!, peerId: chatId!,

View File

@ -30,7 +30,7 @@ import {
import { formatShareText, parseChooseParameter, processDeepLink } from '../../../util/deeplink'; import { formatShareText, parseChooseParameter, processDeepLink } from '../../../util/deeplink';
import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { getOrderedIds } from '../../../util/folderManager'; import { getOrderedIds } from '../../../util/folderManager';
import { buildCollectionByKey, omit } from '../../../util/iteratees'; import { buildCollectionByKey, omit, pick } from '../../../util/iteratees';
import * as langProvider from '../../../util/langProvider'; import * as langProvider from '../../../util/langProvider';
import { debounce, pause, throttle } from '../../../util/schedulers'; import { debounce, pause, throttle } from '../../../util/schedulers';
import { extractCurrentThemeParams } from '../../../util/themeStyle'; import { extractCurrentThemeParams } from '../../../util/themeStyle';
@ -70,6 +70,7 @@ import {
updateListedTopicIds, updateListedTopicIds,
updateManagementProgress, updateManagementProgress,
updatePeerFullInfo, updatePeerFullInfo,
updateThread,
updateThreadInfo, updateThreadInfo,
updateTopic, updateTopic,
updateTopics, updateTopics,
@ -78,13 +79,24 @@ import {
import { updateGroupCall } from '../../reducers/calls'; import { updateGroupCall } from '../../reducers/calls';
import { updateTabState } from '../../reducers/tabs'; import { updateTabState } from '../../reducers/tabs';
import { import {
selectChat, selectChatByUsername, selectChat,
selectChatFolder, selectChatFullInfo, selectChatListType, selectCurrentChat, selectCurrentMessageList, selectDraft, selectChatByUsername,
selectChatFolder,
selectChatFullInfo,
selectChatListType,
selectCurrentChat,
selectCurrentMessageList,
selectDraft,
selectIsChatPinned, selectIsChatPinned,
selectLastServiceNotification, selectLastServiceNotification,
selectStickerSet, selectStickerSet,
selectSupportChat, selectTabState, selectThread, selectThreadInfo, selectThreadOriginChat, selectThreadTopMessageId, selectSupportChat,
selectUser, selectUserByPhoneNumber, selectVisibleUsers, selectTabState,
selectThread,
selectThreadInfo,
selectUser,
selectUserByPhoneNumber,
selectVisibleUsers,
} from '../../selectors'; } from '../../selectors';
import { selectGroupCall } from '../../selectors/calls'; import { selectGroupCall } from '../../selectors/calls';
import { selectCurrentLimit } from '../../selectors/limits'; import { selectCurrentLimit } from '../../selectors/limits';
@ -132,16 +144,19 @@ addActionHandler('preloadTopChatMessages', async (global, actions): Promise<void
} }
}); });
addActionHandler('openChat', (global, actions, payload): ActionReturnType => { function abortChatRequests(chatId: string, threadId?: number) {
const { callApi('abortChatRequests', { chatId, threadId });
id, threadId = MAIN_THREAD_ID, noRequestThreadInfoUpdate, tabId = getCurrentTabId(), }
} = payload;
function abortChatRequestsForCurrentChat<T extends GlobalState>(
global: T, newChatId?: string, newThreadId?: number,
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
const currentMessageList = selectCurrentMessageList(global, tabId); const currentMessageList = selectCurrentMessageList(global, tabId);
const currentChatId = currentMessageList?.chatId; const currentChatId = currentMessageList?.chatId;
const currentThreadId = currentMessageList?.threadId; const currentThreadId = currentMessageList?.threadId;
if (currentChatId && (currentChatId !== id || currentThreadId !== threadId)) { if (currentChatId && (currentChatId !== newChatId || currentThreadId !== newThreadId)) {
const [isChatOpened, isThreadOpened] = Object.values(global.byTabId) const [isChatOpened, isThreadOpened] = Object.values(global.byTabId)
.reduce(([accHasChatOpened, accHasThreadOpened], { id: otherTabId }) => { .reduce(([accHasChatOpened, accHasThreadOpened], { id: otherTabId }) => {
if (otherTabId === tabId || (accHasChatOpened && accHasThreadOpened)) { if (otherTabId === tabId || (accHasChatOpened && accHasThreadOpened)) {
@ -153,14 +168,33 @@ addActionHandler('openChat', (global, actions, payload): ActionReturnType => {
const isSameThread = isSameChat && otherMessageList?.threadId === currentThreadId; const isSameThread = isSameChat && otherMessageList?.threadId === currentThreadId;
return [accHasChatOpened || isSameChat, accHasThreadOpened || isSameThread]; return [accHasChatOpened || isSameChat, accHasThreadOpened || isSameThread];
}, [currentChatId === id, false]); }, [currentChatId === newChatId, false]);
const shouldAbortChatRequests = !isChatOpened || !isThreadOpened; const shouldAbortChatRequests = !isChatOpened || !isThreadOpened;
if (shouldAbortChatRequests) { if (shouldAbortChatRequests) {
callApi('abortChatRequests', { chatId: currentChatId, threadId: isChatOpened ? currentThreadId : undefined }); abortChatRequests(currentChatId, isChatOpened ? currentThreadId : undefined);
} }
} }
}
addActionHandler('openChat', (global, actions, payload): ActionReturnType => {
const {
id, type, noForumTopicPanel, shouldReplaceHistory, shouldReplaceLast,
tabId = getCurrentTabId(),
} = payload;
actions.processOpenChatOrThread({
chatId: id,
type,
threadId: MAIN_THREAD_ID,
noForumTopicPanel,
shouldReplaceHistory,
shouldReplaceLast,
tabId,
});
abortChatRequestsForCurrentChat(global, id, MAIN_THREAD_ID, tabId);
if (!id) { if (!id) {
return; return;
@ -186,54 +220,227 @@ addActionHandler('openChat', (global, actions, payload): ActionReturnType => {
actions.requestChatUpdate({ chatId: id }); actions.requestChatUpdate({ chatId: id });
} }
actions.closeStoryViewer({ tabId }); actions.closeStoryViewer({ tabId });
if (threadId !== MAIN_THREAD_ID && !noRequestThreadInfoUpdate) {
actions.requestThreadInfoUpdate({ chatId: id, threadId });
}
}); });
addActionHandler('openComments', async (global, actions, payload): Promise<void> => { addActionHandler('openThread', async (global, actions, payload): Promise<void> => {
const { const {
id, threadId, originChannelId, tabId = getCurrentTabId(), type, isComments, noForumTopicPanel, shouldReplaceHistory, shouldReplaceLast,
focusMessageId,
tabId = getCurrentTabId(),
} = payload; } = payload;
let { chatId } = payload;
let threadId: number | undefined;
let loadingChatId: string;
let loadingThreadId: number;
if (threadId !== MAIN_THREAD_ID) { if (!isComments) {
const topMessageId = selectThreadTopMessageId(global, id, threadId); loadingChatId = payload.chatId;
if (!topMessageId) { threadId = payload.threadId;
const chat = selectThreadOriginChat(global, id, threadId); loadingThreadId = threadId;
if (!chat) {
return;
}
const originalChat = selectChat(global, loadingChatId);
if (threadId === MAIN_THREAD_ID) {
actions.openChat({ actions.openChat({
id, threadId, tabId, noRequestThreadInfoUpdate: true, id: chatId,
type,
noForumTopicPanel,
shouldReplaceHistory,
shouldReplaceLast,
tabId,
}); });
return;
} else if (originalChat?.isForum) {
actions.processOpenChatOrThread({
chatId,
type,
threadId,
isComments,
noForumTopicPanel,
shouldReplaceHistory,
shouldReplaceLast,
tabId,
});
return;
}
} else {
const { originChannelId, originMessageId } = payload;
const result = await callApi('requestThreadInfoUpdate', { chat, threadId, originChannelId }); loadingChatId = originChannelId;
if (!result) { loadingThreadId = originMessageId;
actions.openPreviousChat({ tabId }); }
return;
} const chat = selectChat(global, loadingChatId);
const threadInfo = selectThreadInfo(global, loadingChatId, loadingThreadId);
const thread = selectThread(global, loadingChatId, loadingThreadId);
if (!chat) return;
abortChatRequestsForCurrentChat(global, loadingChatId, loadingThreadId, tabId);
if (chatId
&& threadInfo?.threadId
&& (isComments || (thread?.listedIds?.length && thread.listedIds.includes(threadInfo.threadId)))) {
global = updateTabState(global, {
loadingThread: undefined,
}, tabId);
setGlobal(global);
actions.processOpenChatOrThread({
chatId,
type,
threadId: threadInfo.threadId,
isComments,
noForumTopicPanel,
shouldReplaceHistory,
shouldReplaceLast,
tabId,
});
return;
}
let { loadingThread } = selectTabState(global, tabId);
if (loadingThread) {
abortChatRequests(loadingThread.loadingChatId, loadingThread.loadingMessageId);
}
global = updateTabState(global, {
loadingThread: {
loadingChatId,
loadingMessageId: loadingThreadId,
},
}, tabId);
setGlobal(global);
const openPreviousChat = () => {
// eslint-disable-next-line eslint-multitab-tt/no-immediate-global
const currentGlobal = getGlobal();
if (isComments
|| selectCurrentMessageList(currentGlobal, tabId)?.chatId !== loadingChatId
|| selectCurrentMessageList(currentGlobal, tabId)?.threadId !== loadingThreadId) {
return;
}
actions.openPreviousChat({ tabId });
};
if (!isComments) {
actions.processOpenChatOrThread({
chatId,
type,
threadId: threadId!,
tabId,
isComments,
noForumTopicPanel,
shouldReplaceHistory,
shouldReplaceLast,
});
}
const result = await callApi('fetchDiscussionMessage', {
chat: selectChat(global, loadingChatId)!,
messageId: loadingThreadId,
});
global = getGlobal();
loadingThread = selectTabState(global, tabId).loadingThread;
if (loadingThread?.loadingChatId !== loadingChatId || loadingThread?.loadingMessageId !== loadingThreadId) {
openPreviousChat();
return;
}
if (!result) {
global = updateTabState(global, {
loadingThread: undefined,
}, tabId);
setGlobal(global);
actions.showNotification({
message: langProvider.translate(isComments ? 'ChannelPostDeleted' : 'lng_message_not_found'),
tabId,
});
openPreviousChat();
return;
}
threadId ??= result.threadId;
chatId ??= result.chatId;
if (!chatId) {
openPreviousChat();
return;
}
global = getGlobal();
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
global = addChats(global, buildCollectionByKey(result.chats, 'id'));
global = addMessages(global, result.messages);
if (isComments) {
global = updateThreadInfo(global, loadingChatId, loadingThreadId, {
threadId,
});
global = updateThreadInfo(global, chatId, threadId, {
isCommentsInfo: false,
threadId,
chatId,
fromChannelId: loadingChatId,
fromMessageId: loadingThreadId,
...(threadInfo
&& pick(threadInfo, ['messagesCount', 'lastMessageId', 'lastReadInboxMessageId', 'recentReplierIds'])),
});
}
global = updateThread(global, chatId, threadId, {
firstMessageId: result.firstMessageId,
});
setGlobal(global);
if (focusMessageId) {
actions.focusMessage({
chatId,
threadId: threadId!,
messageId: focusMessageId,
tabId,
});
}
actions.loadViewportMessages({
chatId,
threadId,
tabId,
onError: () => {
global = getGlobal(); global = getGlobal();
global = addUsers(global, buildCollectionByKey(result.users, 'id')); global = updateTabState(global, {
loadingThread: undefined,
}, tabId);
setGlobal(global); setGlobal(global);
actions.openChat({ actions.showNotification({
id, message: langProvider.translate('Group.ErrorAccessDenied'),
threadId: result.topMessageId,
tabId, tabId,
shouldReplaceLast: true,
noRequestThreadInfoUpdate: true,
}); });
} else { },
actions.openChat({ onLoaded: () => {
id, global = getGlobal();
threadId: topMessageId, loadingThread = selectTabState(global, tabId).loadingThread;
if (loadingThread?.loadingChatId !== loadingChatId || loadingThread?.loadingMessageId !== loadingThreadId) {
return;
}
global = updateTabState(global, {
loadingThread: undefined,
}, tabId);
setGlobal(global);
actions.processOpenChatOrThread({
chatId,
type,
threadId: threadId!,
tabId, tabId,
noRequestThreadInfoUpdate: true, isComments,
noForumTopicPanel,
shouldReplaceHistory,
shouldReplaceLast,
}); });
} },
} });
}); });
addActionHandler('openLinkedChat', async (global, actions, payload): Promise<void> => { addActionHandler('openLinkedChat', async (global, actions, payload): Promise<void> => {
@ -250,28 +457,6 @@ addActionHandler('openLinkedChat', async (global, actions, payload): Promise<voi
} }
}); });
addActionHandler('focusMessageInComments', async (global, actions, payload): Promise<void> => {
const {
chatId, threadId, messageId, tabId = getCurrentTabId(),
} = payload!;
const chat = selectChat(global, chatId);
if (!chat) {
return;
}
const result = await callApi('requestThreadInfoUpdate', { chat, threadId });
if (!result) {
return;
}
global = getGlobal();
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
setGlobal(global);
actions.focusMessage({
chatId, threadId, messageId, tabId,
});
});
addActionHandler('openSupportChat', async (global, actions, payload): Promise<void> => { addActionHandler('openSupportChat', async (global, actions, payload): Promise<void> => {
const { tabId = getCurrentTabId() } = payload || {}; const { tabId = getCurrentTabId() } = payload || {};
const chat = selectSupportChat(global); const chat = selectSupportChat(global);
@ -1227,20 +1412,16 @@ addActionHandler('openChatByUsername', async (global, actions, payload): Promise
} }
} }
const { chatId, type } = selectCurrentMessageList(global, tabId) || {};
const usernameChat = selectChatByUsername(global, username); const usernameChat = selectChatByUsername(global, username);
if (chatId && commentId && messageId && usernameChat && type === 'thread') { if (commentId && messageId && usernameChat) {
const threadInfo = selectThreadInfo(global, chatId, messageId); actions.openThread({
isComments: true,
if (threadInfo && threadInfo.chatId === chatId) { originChannelId: usernameChat.id,
actions.focusMessage({ originMessageId: messageId,
chatId: threadInfo.chatId, tabId,
threadId: threadInfo.threadId, focusMessageId: commentId,
messageId: commentId, });
tabId, return;
});
return;
}
} }
if (!isWebApp) actions.openChat({ id: TMP_CHAT_ID, tabId }); if (!isWebApp) actions.openChat({ id: TMP_CHAT_ID, tabId });
@ -1249,8 +1430,6 @@ addActionHandler('openChatByUsername', async (global, actions, payload): Promise
if (!chatByUsername) return; if (!chatByUsername) return;
global = getGlobal();
if (isWebApp && chatByUsername) { if (isWebApp && chatByUsername) {
const theme = extractCurrentThemeParams(); const theme = extractCurrentThemeParams();
@ -1266,29 +1445,12 @@ addActionHandler('openChatByUsername', async (global, actions, payload): Promise
if (!messageId) return; if (!messageId) return;
const threadInfo = selectThreadInfo(global, chatByUsername.id, messageId); actions.openThread({
let discussionChatId: string | undefined; isComments: true,
originChannelId: chatByUsername.id,
if (!threadInfo) { originMessageId: messageId,
const result = await callApi('requestThreadInfoUpdate', { chat: chatByUsername, threadId: messageId });
if (!result) return;
global = getGlobal();
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
setGlobal(global);
discussionChatId = result.discussionChatId;
} else {
discussionChatId = threadInfo.chatId;
}
if (!discussionChatId) return;
actions.focusMessage({
chatId: discussionChatId,
threadId: messageId,
messageId: Number(commentId),
tabId, tabId,
focusMessageId: commentId,
}); });
}); });
@ -1971,8 +2133,8 @@ addActionHandler('createTopic', async (global, actions, payload): Promise<void>
chat, title, iconColor, iconEmojiId, chat, title, iconColor, iconEmojiId,
}); });
if (topicId) { if (topicId) {
actions.openChat({ actions.openThread({
id: chatId, threadId: topicId, shouldReplaceHistory: true, tabId, chatId, threadId: topicId, shouldReplaceHistory: true, tabId,
}); });
} }
actions.closeCreateTopicPanel({ tabId }); actions.closeCreateTopicPanel({ tabId });
@ -2673,7 +2835,7 @@ async function openChatByUsername<T extends GlobalState>(
chatId: chat.id, threadId, messageId: channelPostId, tabId, chatId: chat.id, threadId, messageId: channelPostId, tabId,
}); });
} else if (!isCurrentChat) { } else if (!isCurrentChat) {
actions.openChat({ id: chat.id, threadId, tabId }); actions.openThread({ chatId: chat.id, threadId: threadId ?? MAIN_THREAD_ID, tabId });
} }
if (startParam) { if (startParam) {

View File

@ -1,6 +1,6 @@
import type { ApiChat } from '../../../api/types';
import type { SharedMediaType } from '../../../types'; import type { SharedMediaType } from '../../../types';
import type { ActionReturnType, GlobalState, TabArgs } from '../../types'; import type { ActionReturnType, GlobalState, TabArgs } from '../../types';
import { type ApiChat, MAIN_THREAD_ID } from '../../../api/types';
import { MESSAGE_SEARCH_SLICE, SHARED_MEDIA_SLICE } from '../../../config'; import { MESSAGE_SEARCH_SLICE, SHARED_MEDIA_SLICE } from '../../../config';
import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { getCurrentTabId } from '../../../util/establishMultitabRole';
@ -21,7 +21,6 @@ import {
selectCurrentMediaSearch, selectCurrentMediaSearch,
selectCurrentMessageList, selectCurrentMessageList,
selectCurrentTextSearch, selectCurrentTextSearch,
selectThreadInfo,
} from '../../selectors'; } from '../../selectors';
addActionHandler('searchTextMessagesLocal', async (global, actions, payload): Promise<void> => { addActionHandler('searchTextMessagesLocal', async (global, actions, payload): Promise<void> => {
@ -36,12 +35,6 @@ addActionHandler('searchTextMessagesLocal', async (global, actions, payload): Pr
const { query, results } = currentSearch; const { query, results } = currentSearch;
const offsetId = results?.nextOffsetId; const offsetId = results?.nextOffsetId;
let topMessageId: number | undefined;
if (threadId !== MAIN_THREAD_ID) {
const threadInfo = selectThreadInfo(global, chatId!, threadId);
topMessageId = threadInfo?.topMessageId;
}
if (!query) { if (!query) {
return; return;
} }
@ -50,7 +43,7 @@ addActionHandler('searchTextMessagesLocal', async (global, actions, payload): Pr
chat, chat,
type: 'text', type: 'text',
query, query,
topMessageId, threadId,
limit: MESSAGE_SEARCH_SLICE, limit: MESSAGE_SEARCH_SLICE,
offsetId, offsetId,
}); });
@ -147,7 +140,7 @@ async function searchSharedMedia<T extends GlobalState>(
chat, chat,
type, type,
limit: SHARED_MEDIA_SLICE * 2, limit: SHARED_MEDIA_SLICE * 2,
topMessageId: threadId === MAIN_THREAD_ID ? undefined : threadId, threadId,
offsetId, offsetId,
}); });

View File

@ -36,7 +36,7 @@ import {
import { ensureProtocol } from '../../../util/ensureProtocol'; import { ensureProtocol } from '../../../util/ensureProtocol';
import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { import {
areSortedArraysIntersecting, buildCollectionByKey, omit, split, unique, areSortedArraysIntersecting, buildCollectionByKey, omit, partition, split, unique,
} from '../../../util/iteratees'; } from '../../../util/iteratees';
import { translate } from '../../../util/langProvider'; import { translate } from '../../../util/langProvider';
import { debounce, onTickEnd, rafPromise } from '../../../util/schedulers'; import { debounce, onTickEnd, rafPromise } from '../../../util/schedulers';
@ -44,8 +44,11 @@ import { IS_IOS } from '../../../util/windowEnvironment';
import { callApi, cancelApiProgress } from '../../../api/gramjs'; import { callApi, cancelApiProgress } from '../../../api/gramjs';
import { import {
getMessageOriginalId, getMessageOriginalId,
getUserFullName, isChatChannel, getUserFullName,
isDeletedUser, isMessageLocal, isChatChannel,
isDeletedUser,
isLocalMessageId,
isMessageLocal,
isServiceNotificationMessage, isServiceNotificationMessage,
isUserBot, isUserBot,
} from '../../helpers'; } from '../../helpers';
@ -72,7 +75,6 @@ import {
updateRequestedMessageTranslation, updateRequestedMessageTranslation,
updateSponsoredMessage, updateSponsoredMessage,
updateThreadInfo, updateThreadInfo,
updateThreadInfos,
updateThreadUnreadFromForwardedMessage, updateThreadUnreadFromForwardedMessage,
updateTopic, updateTopic,
} from '../../reducers'; } from '../../reducers';
@ -107,8 +109,6 @@ import {
selectSponsoredMessage, selectSponsoredMessage,
selectTabState, selectTabState,
selectThreadIdFromMessage, selectThreadIdFromMessage,
selectThreadOriginChat,
selectThreadTopMessageId,
selectTranslationLanguage, selectTranslationLanguage,
selectUser, selectUser,
selectUserFullInfo, selectUserFullInfo,
@ -127,6 +127,8 @@ addActionHandler('loadViewportMessages', (global, actions, payload): ActionRetur
direction = LoadMoreDirection.Around, direction = LoadMoreDirection.Around,
isBudgetPreload = false, isBudgetPreload = false,
shouldForceRender = false, shouldForceRender = false,
onLoaded,
onError,
tabId = getCurrentTabId(), tabId = getCurrentTabId(),
} = payload || {}; } = payload || {};
@ -135,6 +137,7 @@ addActionHandler('loadViewportMessages', (global, actions, payload): ActionRetur
if (!chatId || !threadId) { if (!chatId || !threadId) {
const currentMessageList = selectCurrentMessageList(global, tabId); const currentMessageList = selectCurrentMessageList(global, tabId);
if (!currentMessageList) { if (!currentMessageList) {
onError?.();
return; return;
} }
@ -145,6 +148,7 @@ addActionHandler('loadViewportMessages', (global, actions, payload): ActionRetur
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
// TODO Revise if `chat.isRestricted` check is needed // TODO Revise if `chat.isRestricted` check is needed
if (!chat || chat.isRestricted) { if (!chat || chat.isRestricted) {
onError?.();
return; return;
} }
@ -168,12 +172,18 @@ addActionHandler('loadViewportMessages', (global, actions, payload): ActionRetur
if (!areAllLocal) { if (!areAllLocal) {
onTickEnd(() => { onTickEnd(() => {
void loadViewportMessages( void loadViewportMessages(
global, chat, threadId!, offsetId, LoadMoreDirection.Around, isOutlying, isBudgetPreload, tabId, global, chat, threadId!, offsetId, LoadMoreDirection.Around, isOutlying, isBudgetPreload, onLoaded, tabId,
); );
}); });
} else {
onLoaded?.();
} }
} else { } else {
const offsetId = direction === LoadMoreDirection.Backwards ? viewportIds[0] : viewportIds[viewportIds.length - 1]; const offsetId = direction === LoadMoreDirection.Backwards ? viewportIds[0] : viewportIds[viewportIds.length - 1];
// Prevent requests with local offsets
if (isLocalMessageId(offsetId)) return;
const isOutlying = Boolean(listedIds && !listedIds.includes(offsetId)); const isOutlying = Boolean(listedIds && !listedIds.includes(offsetId));
const historyIds = (isOutlying const historyIds = (isOutlying
? selectOutlyingListByMessageId(global, chatId, threadId, offsetId) : listedIds)!; ? selectOutlyingListByMessageId(global, chatId, threadId, offsetId) : listedIds)!;
@ -187,7 +197,17 @@ addActionHandler('loadViewportMessages', (global, actions, payload): ActionRetur
onTickEnd(() => { onTickEnd(() => {
void loadWithBudget( void loadWithBudget(
global, actions, areAllLocal, isOutlying, isBudgetPreload, chat, threadId!, direction, offsetId, tabId, global,
actions,
areAllLocal,
isOutlying,
isBudgetPreload,
chat,
threadId!,
direction,
offsetId,
onLoaded,
tabId,
); );
}); });
@ -204,17 +224,18 @@ async function loadWithBudget<T extends GlobalState>(
actions: RequiredGlobalActions, actions: RequiredGlobalActions,
areAllLocal: boolean, isOutlying: boolean, isBudgetPreload: boolean, areAllLocal: boolean, isOutlying: boolean, isBudgetPreload: boolean,
chat: ApiChat, threadId: number, direction: LoadMoreDirection, offsetId?: number, chat: ApiChat, threadId: number, direction: LoadMoreDirection, offsetId?: number,
onLoaded?: NoneToVoidFunction,
...[tabId = getCurrentTabId()]: TabArgs<T> ...[tabId = getCurrentTabId()]: TabArgs<T>
) { ) {
if (!areAllLocal) { if (!areAllLocal) {
await loadViewportMessages( await loadViewportMessages(
global, chat, threadId, offsetId, direction, isOutlying, isBudgetPreload, tabId, global, chat, threadId, offsetId, direction, isOutlying, isBudgetPreload, onLoaded, tabId,
); );
} }
if (!isBudgetPreload) { if (!isBudgetPreload) {
actions.loadViewportMessages({ actions.loadViewportMessages({
chatId: chat.id, threadId, direction, isBudgetPreload: true, tabId, chatId: chat.id, threadId, direction, isBudgetPreload: true, onLoaded, tabId,
}); });
} }
} }
@ -571,8 +592,7 @@ addActionHandler('unpinAllMessages', async (global, actions, payload): Promise<v
return; return;
} }
const topId = selectThreadTopMessageId(global, chatId, threadId); await callApi('unpinAllMessages', { chat, threadId });
await callApi('unpinAllMessages', { chat, threadId: topId });
global = getGlobal(); global = getGlobal();
const pinnedIds = selectPinnedIds(global, chatId, threadId); const pinnedIds = selectPinnedIds(global, chatId, threadId);
@ -734,6 +754,14 @@ addActionHandler('markMessageListRead', (global, actions, payload): ActionReturn
const viewportIds = selectViewportIds(global, chatId, threadId, tabId); const viewportIds = selectViewportIds(global, chatId, threadId, tabId);
const minId = selectFirstUnreadId(global, chatId, threadId); const minId = selectFirstUnreadId(global, chatId, threadId);
if (threadId !== MAIN_THREAD_ID && !chat.isForum) {
global = updateThreadInfo(global, chatId, threadId, {
lastReadInboxMessageId: maxId,
});
return global;
}
if (!viewportIds || !minId || !chat.unreadCount) { if (!viewportIds || !minId || !chat.unreadCount) {
return global; return global;
} }
@ -759,11 +787,6 @@ addActionHandler('markMessageListRead', (global, actions, payload): ActionReturn
}); });
} }
// TODO Support local marking read for comments
if (threadId !== MAIN_THREAD_ID) {
return undefined;
}
return updateChat(global, chatId, { return updateChat(global, chatId, {
lastReadInboxMessageId: maxId, lastReadInboxMessageId: maxId,
unreadCount: Math.max(0, chat.unreadCount - readCount), unreadCount: Math.max(0, chat.unreadCount - readCount),
@ -886,24 +909,28 @@ addActionHandler('forwardMessages', (global, actions, payload): ActionReturnType
} = payload; } = payload;
const { const {
fromChatId, messageIds, toChatId, withMyScore, noAuthors, noCaptions, toThreadId, fromChatId, messageIds, toChatId, withMyScore, noAuthors, noCaptions, toThreadId = MAIN_THREAD_ID,
} = selectTabState(global, tabId).forwardMessages; } = selectTabState(global, tabId).forwardMessages;
const isCurrentUserPremium = selectIsCurrentUserPremium(global); const isCurrentUserPremium = selectIsCurrentUserPremium(global);
const isToMainThread = toThreadId === MAIN_THREAD_ID;
const fromChat = fromChatId ? selectChat(global, fromChatId) : undefined; const fromChat = fromChatId ? selectChat(global, fromChatId) : undefined;
const toChat = toChatId ? selectChat(global, toChatId) : undefined; const toChat = toChatId ? selectChat(global, toChatId) : undefined;
const messages = fromChatId && messageIds const messages = fromChatId && messageIds
? messageIds ? messageIds
.sort((a, b) => a - b) .sort((a, b) => a - b)
.map((id) => selectChatMessage(global, fromChatId, id)).filter(Boolean) .map((id) => selectChatMessage(global, fromChatId, id)).filter(Boolean)
: undefined; : undefined;
if (!fromChat || !toChat || !messages || (toThreadId && !toChat.isForum)) { if (!fromChat || !toChat || !messages || (toThreadId && !isToMainThread && !toChat.isForum)) {
return; return;
} }
const sendAs = selectSendAs(global, toChatId!); const sendAs = selectSendAs(global, toChatId!);
const realMessages = messages.filter((m) => !isServiceNotificationMessage(m)); const [realMessages, serviceMessages] = partition(messages, (m) => !isServiceNotificationMessage(m));
if (realMessages.length) { if (realMessages.length) {
(async () => { (async () => {
await rafPromise(); // Wait one frame for any previous `sendMessage` to be processed await rafPromise(); // Wait one frame for any previous `sendMessage` to be processed
@ -923,8 +950,7 @@ addActionHandler('forwardMessages', (global, actions, payload): ActionReturnType
})(); })();
} }
messages serviceMessages
.filter((m) => isServiceNotificationMessage(m))
.forEach((message) => { .forEach((message) => {
const { text, entities } = message.content.text || {}; const { text, entities } = message.content.text || {};
const { sticker, poll } = message.content; const { sticker, poll } = message.content;
@ -1022,22 +1048,6 @@ addActionHandler('rescheduleMessage', (global, actions, payload): ActionReturnTy
}); });
}); });
addActionHandler('requestThreadInfoUpdate', async (global, actions, payload): Promise<void> => {
const { chatId, threadId } = payload;
const chat = selectChat(global, chatId);
if (!chat) {
return;
}
const originChannelId = selectThreadOriginChat(global, chatId, threadId)?.id;
const result = await callApi('requestThreadInfoUpdate', { chat, threadId, originChannelId });
if (!result) return;
global = getGlobal();
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
setGlobal(global);
});
addActionHandler('transcribeAudio', async (global, actions, payload): Promise<void> => { addActionHandler('transcribeAudio', async (global, actions, payload): Promise<void> => {
const { messageId, chatId } = payload; const { messageId, chatId } = payload;
@ -1093,6 +1103,7 @@ async function loadViewportMessages<T extends GlobalState>(
direction: LoadMoreDirection, direction: LoadMoreDirection,
isOutlying = false, isOutlying = false,
isBudgetPreload = false, isBudgetPreload = false,
onLoaded?: NoneToVoidFunction,
...[tabId = getCurrentTabId()]: TabArgs<T> ...[tabId = getCurrentTabId()]: TabArgs<T>
) { ) {
const chatId = chat.id; const chatId = chat.id;
@ -1133,7 +1144,7 @@ async function loadViewportMessages<T extends GlobalState>(
} }
const { const {
messages, users, chats, repliesThreadInfos, messages, users, chats,
} = result; } = result;
global = getGlobal(); global = getGlobal();
@ -1146,7 +1157,7 @@ async function loadViewportMessages<T extends GlobalState>(
const ids = Object.keys(byId).map(Number); const ids = Object.keys(byId).map(Number);
if (threadId !== MAIN_THREAD_ID) { if (threadId !== MAIN_THREAD_ID) {
const threadFirstMessageId = selectFirstMessageId(global, chatId, threadId) || {}; const threadFirstMessageId = selectFirstMessageId(global, chatId, threadId);
if ((!ids[0] || threadFirstMessageId === ids[0]) && threadFirstMessageId !== threadId) { if ((!ids[0] || threadFirstMessageId === ids[0]) && threadFirstMessageId !== threadId) {
ids.unshift(threadId); ids.unshift(threadId);
} }
@ -1159,7 +1170,6 @@ async function loadViewportMessages<T extends GlobalState>(
global = addUsers(global, buildCollectionByKey(users, 'id')); global = addUsers(global, buildCollectionByKey(users, 'id'));
global = addChats(global, buildCollectionByKey(chats, 'id')); global = addChats(global, buildCollectionByKey(chats, 'id'));
global = updateThreadInfos(global, repliesThreadInfos);
let listedIds = selectListedIds(global, chatId, threadId); let listedIds = selectListedIds(global, chatId, threadId);
const outlyingList = offsetId ? selectOutlyingListByMessageId(global, chatId, threadId, offsetId) : undefined; const outlyingList = offsetId ? selectOutlyingListByMessageId(global, chatId, threadId, offsetId) : undefined;
@ -1174,12 +1184,15 @@ async function loadViewportMessages<T extends GlobalState>(
} }
if (!isBudgetPreload) { if (!isBudgetPreload) {
const historyIds = isOutlying ? outlyingList! : listedIds!; const historyIds = isOutlying && outlyingList ? outlyingList : listedIds;
const { newViewportIds } = getViewportSlice(historyIds, offsetId, direction); if (historyIds) {
global = safeReplaceViewportIds(global, chatId, threadId, newViewportIds!, tabId); const { newViewportIds } = getViewportSlice(historyIds, offsetId, direction);
global = safeReplaceViewportIds(global, chatId, threadId, newViewportIds!, tabId);
}
} }
setGlobal(global); setGlobal(global);
onLoaded?.();
} }
async function loadMessage<T extends GlobalState>( async function loadMessage<T extends GlobalState>(
@ -1570,7 +1583,7 @@ addActionHandler('setForwardChatOrTopic', async (global, actions, payload): Prom
}, tabId); }, tabId);
setGlobal(global); setGlobal(global);
actions.openChat({ id: chatId, threadId: topicId, tabId }); actions.openThread({ chatId, threadId: topicId || MAIN_THREAD_ID, tabId });
actions.closeMediaViewer({ tabId }); actions.closeMediaViewer({ tabId });
actions.exitMessageSelectMode({ tabId }); actions.exitMessageSelectMode({ tabId });
}); });
@ -1732,19 +1745,7 @@ addActionHandler('loadMessageViews', async (global, actions, payload): Promise<v
forwards: update.forwards, forwards: update.forwards,
}); });
const message = selectChatMessage(global, chatId, update.id); global = updateThreadInfo(global, chatId, update.id, update.threadInfo);
if (!message) return;
const repliesChatId = message.repliesThreadInfo?.chatId;
const threadId = message.repliesThreadInfo?.threadId;
if (!repliesChatId || !threadId) return;
global = updateThreadInfo(global, repliesChatId, threadId, {
messagesCount: update.messagesCount,
recentReplierIds: update.recentReplierIds,
lastMessageId: update.maxId,
lastReadInboxMessageId: update.readMaxId,
});
}); });
setGlobal(global); setGlobal(global);

View File

@ -1,13 +1,15 @@
import { addCallback } from '../../../lib/teact/teactn'; import { addCallback } from '../../../lib/teact/teactn';
import type { ApiChat, ApiMessage } from '../../../api/types'; import type { ApiChat } from '../../../api/types';
import type { RequiredGlobalActions } from '../../index'; import type { RequiredGlobalActions } from '../../index';
import type { ActionReturnType, GlobalState, Thread } from '../../types'; import type { ActionReturnType, GlobalState, Thread } from '../../types';
import { MAIN_THREAD_ID } from '../../../api/types'; import { MAIN_THREAD_ID } from '../../../api/types';
import { DEBUG, MESSAGE_LIST_SLICE, SERVICE_NOTIFICATIONS_USER_ID } from '../../../config'; import { DEBUG, MESSAGE_LIST_SLICE, SERVICE_NOTIFICATIONS_USER_ID } from '../../../config';
import { init as initFolderManager } from '../../../util/folderManager'; import { init as initFolderManager } from '../../../util/folderManager';
import { buildCollectionByKey } from '../../../util/iteratees'; import {
buildCollectionByKey, omitUndefined, pick, unique,
} from '../../../util/iteratees';
import { callApi } from '../../../api/gramjs'; import { callApi } from '../../../api/gramjs';
import { import {
addActionHandler, getActions, getGlobal, setGlobal, addActionHandler, getActions, getGlobal, setGlobal,
@ -17,8 +19,8 @@ import {
safeReplaceViewportIds, safeReplaceViewportIds,
updateChats, updateChats,
updateListedIds, updateListedIds,
updateThread, updateThreadInfo, updateThread,
updateThreadInfos, updateThreadInfo,
updateUsers, updateUsers,
} from '../../reducers'; } from '../../reducers';
import { updateTabState } from '../../reducers/tabs'; import { updateTabState } from '../../reducers/tabs';
@ -107,11 +109,11 @@ async function loadAndReplaceMessages<T extends GlobalState>(global: T, actions:
acc[chatId] = Object acc[chatId] = Object
.keys(global.messages.byChatId[chatId].threadsById) .keys(global.messages.byChatId[chatId].threadsById)
.reduce<Record<number, Partial<Thread>>>((acc2, threadId) => { .reduce<Record<number, Partial<Thread>>>((acc2, threadId) => {
acc2[Number(threadId)] = { acc2[Number(threadId)] = omitUndefined({
draft: selectDraft(global, chatId, Number(threadId)), draft: selectDraft(global, chatId, Number(threadId)),
editingId: selectEditingId(global, chatId, Number(threadId)), editingId: selectEditingId(global, chatId, Number(threadId)),
editingDraft: selectEditingDraft(global, chatId, Number(threadId)), editingDraft: selectEditingDraft(global, chatId, Number(threadId)),
}; });
return acc2; return acc2;
}, {}); }, {});
@ -123,11 +125,21 @@ async function loadAndReplaceMessages<T extends GlobalState>(global: T, actions:
global = getGlobal(); global = getGlobal();
const { chatId: currentChatId, threadId: currentThreadId } = selectCurrentMessageList(global, tabId) || {}; const { chatId: currentChatId, threadId: currentThreadId } = selectCurrentMessageList(global, tabId) || {};
const activeThreadId = currentThreadId || MAIN_THREAD_ID; const activeThreadId = currentThreadId || MAIN_THREAD_ID;
const threadInfo = currentThreadId && currentChatId const threadInfo = currentChatId && currentThreadId
? selectThreadInfo(global, currentChatId, currentThreadId) : undefined; ? selectThreadInfo(global, currentChatId, currentThreadId) : undefined;
const currentChat = currentChatId ? global.chats.byId[currentChatId] : undefined; const currentChat = currentChatId ? global.chats.byId[currentChatId] : undefined;
if (currentChatId && currentChat) { if (currentChatId && currentChat) {
const result = await loadTopMessages(currentChat, activeThreadId, threadInfo?.lastReadInboxMessageId); const [result, resultDiscussion] = await Promise.all([
loadTopMessages(
currentChat,
activeThreadId,
activeThreadId !== MAIN_THREAD_ID ? activeThreadId : undefined,
),
activeThreadId !== MAIN_THREAD_ID ? callApi('fetchDiscussionMessage', {
chat: currentChat,
messageId: activeThreadId,
}) : undefined,
]);
global = getGlobal(); global = getGlobal();
const { chatId: newCurrentChatId } = selectCurrentMessageList(global, tabId) || {}; const { chatId: newCurrentChatId } = selectCurrentMessageList(global, tabId) || {};
@ -142,10 +154,12 @@ async function loadAndReplaceMessages<T extends GlobalState>(global: T, actions:
.filter(Boolean) .filter(Boolean)
: []; : [];
const allMessages = ([] as ApiMessage[]).concat(result.messages, localMessages); const isDiscussionStartLoaded = result.messages.some(({ id }) => id === resultDiscussion?.firstMessageId);
const threadStartMessages = (isDiscussionStartLoaded && resultDiscussion?.topMessages) || [];
const allMessages = threadStartMessages.concat(result.messages, localMessages);
const allMessagesWithTopicLastMessages = allMessages.concat(topicLastMessages); const allMessagesWithTopicLastMessages = allMessages.concat(topicLastMessages);
const byId = buildCollectionByKey(allMessagesWithTopicLastMessages, 'id'); const byId = buildCollectionByKey(allMessagesWithTopicLastMessages, 'id');
const listedIds = allMessages.map(({ id }) => id); const listedIds = unique(allMessages.map(({ id }) => id));
if (!wasReset) { if (!wasReset) {
global = { global = {
@ -166,8 +180,16 @@ async function loadAndReplaceMessages<T extends GlobalState>(global: T, actions:
global = addChatMessagesById(global, currentChatId, byId); global = addChatMessagesById(global, currentChatId, byId);
global = updateListedIds(global, currentChatId, activeThreadId, listedIds); global = updateListedIds(global, currentChatId, activeThreadId, listedIds);
if (threadInfo?.originChannelId) { if (resultDiscussion) {
global = updateThreadInfo(global, currentChatId, activeThreadId, threadInfo); // eslint-disable-next-line @typescript-eslint/no-loop-func
resultDiscussion.threadInfoUpdates.forEach((update) => {
global = updateThreadInfo(global, currentChatId, activeThreadId, update);
});
}
if (threadInfo && !threadInfo.isCommentsInfo && activeThreadId !== MAIN_THREAD_ID) {
global = updateThreadInfo(global, currentChatId, activeThreadId, {
...pick(threadInfo, ['fromChannelId', 'fromMessageId']),
});
} }
// eslint-disable-next-line @typescript-eslint/no-loop-func // eslint-disable-next-line @typescript-eslint/no-loop-func
Object.values(global.byTabId).forEach(({ id: otherTabId }) => { Object.values(global.byTabId).forEach(({ id: otherTabId }) => {
@ -178,9 +200,6 @@ async function loadAndReplaceMessages<T extends GlobalState>(global: T, actions:
}); });
global = updateChats(global, buildCollectionByKey(result.chats, 'id')); global = updateChats(global, buildCollectionByKey(result.chats, 'id'));
global = updateUsers(global, buildCollectionByKey(result.users, 'id')); global = updateUsers(global, buildCollectionByKey(result.users, 'id'));
if (result.repliesThreadInfos.length) {
global = updateThreadInfos(global, result.repliesThreadInfos);
}
areMessagesLoaded = true; areMessagesLoaded = true;
} }
@ -235,11 +254,11 @@ async function loadAndReplaceMessages<T extends GlobalState>(global: T, actions:
}); });
} }
function loadTopMessages(chat: ApiChat, threadId: number, lastReadInboxId?: number) { function loadTopMessages(chat: ApiChat, threadId: number, offsetId?: number) {
return callApi('fetchMessages', { return callApi('fetchMessages', {
chat, chat,
threadId, threadId,
offsetId: lastReadInboxId || chat.lastReadInboxMessageId, offsetId: offsetId || chat.lastReadInboxMessageId,
addOffset: -(Math.round(MESSAGE_LIST_SLICE / 2) + 1), addOffset: -(Math.round(MESSAGE_LIST_SLICE / 2) + 1),
limit: MESSAGE_LIST_SLICE, limit: MESSAGE_LIST_SLICE,
}); });

View File

@ -1,5 +1,5 @@
import type { import type {
ApiChat, ApiMessage, ApiPollResult, ApiReactions, ApiThreadInfo, ApiChat, ApiMessage, ApiPollResult, ApiReactions,
} from '../../../api/types'; } from '../../../api/types';
import type { RequiredGlobalActions } from '../../index'; import type { RequiredGlobalActions } from '../../index';
import type { import type {
@ -33,6 +33,7 @@ import {
updateMessageTranslations, updateMessageTranslations,
updateScheduledMessage, updateScheduledMessage,
updateThreadInfo, updateThreadInfo,
updateThreadInfos,
updateThreadUnreadFromForwardedMessage, updateThreadUnreadFromForwardedMessage,
updateTopic, updateTopic,
} from '../../reducers'; } from '../../reducers';
@ -75,15 +76,6 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
global = updateWithLocalMedia(global, chatId, id, message); global = updateWithLocalMedia(global, chatId, id, message);
global = updateListedAndViewportIds(global, actions, message as ApiMessage); global = updateListedAndViewportIds(global, actions, message as ApiMessage);
if (message.repliesThreadInfo) {
global = updateThreadInfo(
global,
message.repliesThreadInfo.chatId,
message.repliesThreadInfo.threadId,
message.repliesThreadInfo,
);
}
const newMessage = selectChatMessage(global, chatId, id)!; const newMessage = selectChatMessage(global, chatId, id)!;
const replyInfo = getMessageReplyInfo(newMessage); const replyInfo = getMessageReplyInfo(newMessage);
const storyReplyInfo = getStoryReplyInfo(newMessage); const storyReplyInfo = getStoryReplyInfo(newMessage);
@ -114,11 +106,6 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
} }
} }
const { threadInfo } = selectThreadByMessage(global, message as ApiMessage) || {};
if (threadInfo && !isLocal) {
actions.requestThreadInfoUpdate({ chatId, threadId: threadInfo.threadId });
}
// @perf Wait until scroll animation finishes or simply rely on delivery status update // @perf Wait until scroll animation finishes or simply rely on delivery status update
// (which is itself delayed) // (which is itself delayed)
if (!isLocal) { if (!isLocal) {
@ -204,14 +191,6 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
global = updateWithLocalMedia(global, chatId, id, message); global = updateWithLocalMedia(global, chatId, id, message);
const newMessage = selectChatMessage(global, chatId, id)!; const newMessage = selectChatMessage(global, chatId, id)!;
if (message.repliesThreadInfo) {
global = updateThreadInfo(
global,
message.repliesThreadInfo.chatId,
message.repliesThreadInfo.threadId,
message.repliesThreadInfo,
);
}
if (currentMessage) { if (currentMessage) {
global = updateChatLastMessage(global, chatId, newMessage); global = updateChatLastMessage(global, chatId, newMessage);
@ -293,7 +272,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
actions.markMessageListRead({ maxId: message.id, tabId }); actions.markMessageListRead({ maxId: message.id, tabId });
}); });
if (thread?.threadInfo) { if (thread?.threadInfo?.threadId) {
global = replaceThreadParam(global, chatId, thread.threadInfo.threadId, 'threadInfo', { global = replaceThreadParam(global, chatId, thread.threadInfo.threadId, 'threadInfo', {
...thread.threadInfo, ...thread.threadInfo,
lastMessageId: message.id, lastMessageId: message.id,
@ -364,43 +343,33 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
break; break;
} }
case 'updateThreadInfo': { case 'updateThreadInfos': {
const { const {
chatId, threadId, threadInfo, firstMessageId, threadInfoUpdates,
} = update; } = update;
const currentThreadInfo = selectThreadInfo(global, chatId, threadId); global = updateThreadInfos(global, threadInfoUpdates);
const newThreadInfo = { threadInfoUpdates.forEach((threadInfo) => {
...currentThreadInfo, const { chatId, threadId } = threadInfo;
...threadInfo, if (!chatId || !threadId) return;
};
if (!newThreadInfo.threadId) { const chat = selectChat(global, chatId);
return; const currentThreadInfo = selectThreadInfo(global, chatId, threadId);
} if (chat?.isForum && threadInfo.lastReadInboxMessageId !== currentThreadInfo?.lastReadInboxMessageId) {
actions.loadTopicById({ chatId, topicId: threadId });
global = updateThreadInfo(global, chatId, threadId, newThreadInfo as ApiThreadInfo);
if (firstMessageId) {
global = replaceThreadParam(global, chatId, threadId, 'firstMessageId', firstMessageId);
}
const chat = selectChat(global, chatId);
if (chat?.isForum && threadInfo.lastReadInboxMessageId !== currentThreadInfo?.lastReadInboxMessageId) {
actions.loadTopicById({ chatId, topicId: threadId });
}
// Update reply thread last read message id if already read in main thread
if (threadInfo.topMessageId === threadId && !chat?.isForum) {
const lastReadInboxMessageId = chat?.lastReadInboxMessageId;
const lastReadInboxMessageIdInThread = newThreadInfo.lastReadInboxMessageId || lastReadInboxMessageId;
if (lastReadInboxMessageId && lastReadInboxMessageIdInThread) {
global = updateThreadInfo(global, chatId, threadId, {
lastReadInboxMessageId: Math.max(lastReadInboxMessageIdInThread, lastReadInboxMessageId),
});
} }
}
// Update reply thread last read message id if already read in main thread
if (!chat?.isForum) {
const lastReadInboxMessageId = chat?.lastReadInboxMessageId;
const lastReadInboxMessageIdInThread = threadInfo.lastReadInboxMessageId || lastReadInboxMessageId;
if (lastReadInboxMessageId && lastReadInboxMessageIdInThread) {
global = updateThreadInfo(global, chatId, threadId, {
lastReadInboxMessageId: Math.max(lastReadInboxMessageIdInThread, lastReadInboxMessageId),
});
}
}
});
setGlobal(global); setGlobal(global);
break; break;
@ -807,35 +776,37 @@ function updateListedAndViewportIds<T extends GlobalState>(
) { ) {
const { id, chatId } = message; const { id, chatId } = message;
const { threadInfo, firstMessageId } = selectThreadByMessage(global, message) || {}; const { threadInfo } = selectThreadByMessage(global, message) || {};
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
const isUnreadChatNotLoaded = chat?.unreadCount && !selectListedIds(global, chatId, MAIN_THREAD_ID); const isUnreadChatNotLoaded = chat?.unreadCount && !selectListedIds(global, chatId, MAIN_THREAD_ID);
global = updateThreadUnread(global, actions, message); global = updateThreadUnread(global, actions, message);
const { threadId } = threadInfo ?? {};
if (threadInfo) { if (threadInfo && threadId) {
if (firstMessageId || !isMessageLocal(message)) { global = updateListedIds(global, chatId, threadId, [id]);
global = updateListedIds(global, chatId, threadInfo.threadId, [id]);
Object.values(global.byTabId).forEach(({ id: tabId }) => { Object.values(global.byTabId).forEach(({ id: tabId }) => {
if (selectIsViewportNewest(global, chatId, threadInfo.threadId, tabId)) { if (selectIsViewportNewest(global, chatId, threadId, tabId)) {
global = addViewportId(global, chatId, threadInfo.threadId, id, tabId); // Always keep the first unread message in the viewport list
const firstUnreadId = selectFirstUnreadId(global, chatId, threadId);
const candidateGlobal = addViewportId(global, chatId, threadId, id, tabId);
const newViewportIds = selectViewportIds(candidateGlobal, chatId, threadId, tabId);
if (!firstMessageId) { if (!firstUnreadId || newViewportIds!.includes(firstUnreadId)) {
global = replaceThreadParam(global, chatId, threadInfo.threadId, 'firstMessageId', message.id); global = candidateGlobal;
}
} }
}); }
} });
global = replaceThreadParam(global, chatId, threadInfo.threadId, 'threadInfo', { global = replaceThreadParam(global, chatId, threadId, 'threadInfo', {
...threadInfo, ...threadInfo,
lastMessageId: message.id, lastMessageId: message.id,
}); });
if (!isMessageLocal(message) && !isActionMessage(message)) { if (!isMessageLocal(message) && !isActionMessage(message)) {
global = updateThreadInfo(global, chatId, threadInfo.threadId, { global = updateThreadInfo(global, chatId, threadId, {
messagesCount: (threadInfo.messagesCount || 0) + 1, messagesCount: (threadInfo.messagesCount || 0) + 1,
}); });
} }
@ -895,9 +866,9 @@ function updateChatLastMessage<T extends GlobalState>(
return global; return global;
} }
function findLastMessage<T extends GlobalState>(global: T, chatId: string) { function findLastMessage<T extends GlobalState>(global: T, chatId: string, threadId = MAIN_THREAD_ID) {
const byId = selectChatMessages(global, chatId); const byId = selectChatMessages(global, chatId);
const listedIds = selectListedIds(global, chatId, MAIN_THREAD_ID); const listedIds = selectListedIds(global, chatId, threadId);
if (!byId || !listedIds) { if (!byId || !listedIds) {
return undefined; return undefined;
@ -906,7 +877,7 @@ function findLastMessage<T extends GlobalState>(global: T, chatId: string) {
let i = listedIds.length; let i = listedIds.length;
while (i--) { while (i--) {
const message = byId[listedIds[i]]; const message = byId[listedIds[i]];
if (!message.isDeleting) { if (message && !message.isDeleting) {
return message; return message;
} }
} }
@ -923,6 +894,9 @@ export function deleteMessages<T extends GlobalState>(
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
if (!chat) return; if (!chat) return;
const threadIdsToUpdate = new Set<number>();
threadIdsToUpdate.add(MAIN_THREAD_ID);
ids.forEach((id) => { ids.forEach((id) => {
global = updateChatMessage(global, chatId, id, { global = updateChatMessage(global, chatId, id, {
isDeleting: true, isDeleting: true,
@ -930,21 +904,10 @@ export function deleteMessages<T extends GlobalState>(
global = clearMessageTranslation(global, chatId, id); global = clearMessageTranslation(global, chatId, id);
const newLastMessage = findLastMessage(global, chatId);
if (newLastMessage) {
global = updateChatLastMessage(global, chatId, newLastMessage, true);
}
if (chat.topics?.[id]) { if (chat.topics?.[id]) {
global = deleteTopic(global, chatId, id); global = deleteTopic(global, chatId, id);
} }
});
actions.requestChatUpdate({ chatId });
const threadIdsToUpdate: number[] = [];
ids.forEach((id) => {
const message = selectChatMessage(global, chatId, id); const message = selectChatMessage(global, chatId, id);
if (!message) { if (!message) {
return; return;
@ -954,20 +917,36 @@ export function deleteMessages<T extends GlobalState>(
const threadId = selectThreadIdFromMessage(global, message); const threadId = selectThreadIdFromMessage(global, message);
if (threadId) { if (threadId) {
threadIdsToUpdate.push(threadId); threadIdsToUpdate.add(threadId);
} }
}); });
actions.requestChatUpdate({ chatId });
const idsSet = new Set(ids);
threadIdsToUpdate.forEach((threadId) => {
const threadInfo = selectThreadInfo(global, chatId, threadId);
if (!threadInfo?.lastMessageId || !idsSet.has(threadInfo.lastMessageId)) return;
const newLastMessage = findLastMessage(global, chatId, threadId);
if (!newLastMessage) return;
if (threadId === MAIN_THREAD_ID) {
global = updateChatLastMessage(global, chatId, newLastMessage, true);
}
global = updateThreadInfo(global, chatId, threadId, {
lastMessageId: newLastMessage.id,
});
});
setGlobal(global); setGlobal(global);
setTimeout(() => { setTimeout(() => {
global = getGlobal(); global = getGlobal();
global = deleteChatMessages(global, chatId, ids); global = deleteChatMessages(global, chatId, ids);
setGlobal(global); setGlobal(global);
unique(threadIdsToUpdate).forEach((threadId) => {
actions.requestThreadInfoUpdate({ chatId, threadId });
});
}, ANIMATION_DELAY); }, ANIMATION_DELAY);
return; return;

View File

@ -14,9 +14,9 @@ import {
} from '../../selectors'; } from '../../selectors';
import { closeLocalTextSearch } from './localSearch'; import { closeLocalTextSearch } from './localSearch';
addActionHandler('openChat', (global, actions, payload): ActionReturnType => { addActionHandler('processOpenChatOrThread', (global, actions, payload): ActionReturnType => {
const { const {
id, chatId,
threadId = MAIN_THREAD_ID, threadId = MAIN_THREAD_ID,
type = 'thread', type = 'thread',
shouldReplaceHistory = false, shouldReplaceHistory = false,
@ -38,12 +38,12 @@ addActionHandler('openChat', (global, actions, payload): ActionReturnType => {
} }
if (!currentMessageList || ( if (!currentMessageList || (
currentMessageList.chatId !== id currentMessageList.chatId !== chatId
|| currentMessageList.threadId !== threadId || currentMessageList.threadId !== threadId
|| currentMessageList.type !== type || currentMessageList.type !== type
)) { )) {
if (id) { if (chatId) {
global = replaceTabThreadParam(global, id, threadId, 'replyStack', [], tabId); global = replaceTabThreadParam(global, chatId, threadId, 'replyStack', [], tabId);
global = updateTabState(global, { global = updateTabState(global, {
activeReactions: {}, activeReactions: {},
@ -57,25 +57,25 @@ addActionHandler('openChat', (global, actions, payload): ActionReturnType => {
isStatisticsShown: false, isStatisticsShown: false,
boostStatistics: undefined, boostStatistics: undefined,
contentToBeScheduled: undefined, contentToBeScheduled: undefined,
...(id !== selectTabState(global, tabId).forwardMessages.toChatId && { ...(chatId !== selectTabState(global, tabId).forwardMessages.toChatId && {
forwardMessages: {}, forwardMessages: {},
}), }),
}, tabId); }, tabId);
} }
if (id) { if (chatId) {
const chat = selectChat(global, id); const chat = selectChat(global, chatId);
if (chat?.isForum && !noForumTopicPanel) { if (chat?.isForum && !noForumTopicPanel) {
actions.openForumPanel({ chatId: id!, tabId }); actions.openForumPanel({ chatId, tabId });
} else if (id !== selectTabState(global, tabId).forumPanelChatId) { } else if (chatId !== selectTabState(global, tabId).forumPanelChatId) {
actions.closeForumPanel({ tabId }); actions.closeForumPanel({ tabId });
} }
} }
actions.updatePageTitle({ tabId }); actions.updatePageTitle({ tabId });
return updateCurrentMessageList(global, id, threadId, type, shouldReplaceHistory, shouldReplaceLast, tabId); return updateCurrentMessageList(global, chatId, threadId, type, shouldReplaceHistory, shouldReplaceLast, tabId);
}); });
addActionHandler('openChatInNewTab', (global, actions, payload): ActionReturnType => { addActionHandler('openChatInNewTab', (global, actions, payload): ActionReturnType => {
@ -110,13 +110,26 @@ addActionHandler('openChatWithInfo', (global, actions, payload): ActionReturnTyp
actions.openChat({ ...payload, tabId }); actions.openChat({ ...payload, tabId });
}); });
addActionHandler('openThreadWithInfo', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload;
global = updateTabState(global, {
...selectTabState(global, tabId),
isChatInfoShown: true,
}, tabId);
global = { ...global, lastIsChatInfoShown: true };
setGlobal(global);
actions.openThread({ ...payload, tabId });
});
addActionHandler('openChatWithDraft', (global, actions, payload): ActionReturnType => { addActionHandler('openChatWithDraft', (global, actions, payload): ActionReturnType => {
const { const {
chatId, text, threadId, files, filter, tabId = getCurrentTabId(), chatId, text, threadId = MAIN_THREAD_ID, files, filter, tabId = getCurrentTabId(),
} = payload; } = payload;
if (chatId) { if (chatId) {
actions.openChat({ id: chatId, threadId, tabId }); actions.openThread({ chatId, threadId, tabId });
} }
return updateTabState(global, { return updateTabState(global, {

View File

@ -440,8 +440,8 @@ addActionHandler('focusMessage', (global, actions, payload): ActionReturnType =>
const viewportIds = selectViewportIds(global, chatId, threadId, tabId); const viewportIds = selectViewportIds(global, chatId, threadId, tabId);
if (viewportIds && viewportIds.includes(messageId)) { if (viewportIds && viewportIds.includes(messageId)) {
setGlobal(global, { forceOnHeavyAnimation: true }); setGlobal(global, { forceOnHeavyAnimation: true });
actions.openChat({ actions.openThread({
id: chatId, chatId,
threadId, threadId,
type: messageListType, type: messageListType,
shouldReplaceHistory, shouldReplaceHistory,
@ -462,8 +462,8 @@ addActionHandler('focusMessage', (global, actions, payload): ActionReturnType =>
setGlobal(global, { forceOnHeavyAnimation: true }); setGlobal(global, { forceOnHeavyAnimation: true });
actions.openChat({ actions.openThread({
id: chatId, chatId,
threadId, threadId,
type: messageListType, type: messageListType,
shouldReplaceHistory, shouldReplaceHistory,
@ -471,6 +471,8 @@ addActionHandler('focusMessage', (global, actions, payload): ActionReturnType =>
tabId, tabId,
}); });
actions.loadViewportMessages({ actions.loadViewportMessages({
chatId,
threadId,
tabId, tabId,
shouldForceRender: true, shouldForceRender: true,
}); });

View File

@ -2,11 +2,9 @@ import { addCallback } from '../../../lib/teact/teactn';
import type { ApiError, ApiNotification } from '../../../api/types'; import type { ApiError, ApiNotification } from '../../../api/types';
import type { ActionReturnType, GlobalState } from '../../types'; import type { ActionReturnType, GlobalState } from '../../types';
import { MAIN_THREAD_ID } from '../../../api/types';
import { import {
DEBUG, GLOBAL_STATE_CACHE_CUSTOM_EMOJI_LIMIT, INACTIVE_MARKER, DEBUG, GLOBAL_STATE_CACHE_CUSTOM_EMOJI_LIMIT, INACTIVE_MARKER, PAGE_TITLE,
PAGE_TITLE,
} from '../../../config'; } from '../../../config';
import { getAllMultitabTokens, getCurrentTabId, reestablishMasterToSelf } from '../../../util/establishMultitabRole'; import { getAllMultitabTokens, getCurrentTabId, reestablishMasterToSelf } from '../../../util/establishMultitabRole';
import { getAllNotificationsCount } from '../../../util/folderManager'; import { getAllNotificationsCount } from '../../../util/folderManager';
@ -134,7 +132,7 @@ addActionHandler('closeManagement', (global, actions, payload): ActionReturnType
}, tabId); }, tabId);
}); });
addActionHandler('openChat', (global, actions, payload): ActionReturnType => { addActionHandler('processOpenChatOrThread', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload; const { tabId = getCurrentTabId() } = payload;
if (!getIsMobile() && !getIsTablet()) { if (!getIsMobile() && !getIsTablet()) {
return undefined; return undefined;
@ -541,7 +539,7 @@ addActionHandler('openCreateTopicPanel', (global, actions, payload): ActionRetur
// Topic panel can be opened only if there is a selected chat // Topic panel can be opened only if there is a selected chat
const currentChat = selectCurrentChat(global, tabId); const currentChat = selectCurrentChat(global, tabId);
if (!currentChat) actions.openChat({ id: chatId, threadId: MAIN_THREAD_ID, tabId }); if (!currentChat) actions.openChat({ id: chatId, tabId });
return updateTabState(global, { return updateTabState(global, {
createTopicPanel: { createTopicPanel: {

View File

@ -5,16 +5,16 @@ import { addActionHandler } from '../../index';
import { updateTabState } from '../../reducers/tabs'; import { updateTabState } from '../../reducers/tabs';
import { selectTabState } from '../../selectors'; import { selectTabState } from '../../selectors';
addActionHandler('openChat', (global, actions, payload): ActionReturnType => { addActionHandler('processOpenChatOrThread', (global, actions, payload): ActionReturnType => {
const { const {
id, chatId,
tabId = getCurrentTabId(), tabId = getCurrentTabId(),
} = payload; } = payload;
if (id) { if (chatId) {
return updateTabState(global, { return updateTabState(global, {
reactionPicker: { reactionPicker: {
chatId: id, chatId,
messageId: undefined, messageId: undefined,
position: undefined, position: undefined,
}, },

View File

@ -33,7 +33,6 @@ import {
selectChat, selectChat,
selectChatMessages, selectChatMessages,
selectCurrentMessageList, selectCurrentMessageList,
selectThreadOriginChat,
selectViewportIds, selectViewportIds,
selectVisibleUsers, selectVisibleUsers,
} from './selectors'; } from './selectors';
@ -335,17 +334,8 @@ function reduceChats<T extends GlobalState>(global: T): GlobalState['chats'] {
const { chats: { byId }, currentUserId } = global; const { chats: { byId }, currentUserId } = global;
const currentChatIds = compact( const currentChatIds = compact(
Object.values(global.byTabId) Object.values(global.byTabId)
.flatMap(({ id: tabId }): MessageList[] | undefined => { .map(({ id: tabId }): MessageList | undefined => {
const messageList = selectCurrentMessageList(global, tabId); return selectCurrentMessageList(global, tabId);
if (!messageList) return undefined;
const { chatId, threadId } = messageList;
const origin = selectThreadOriginChat(global, chatId, threadId);
return origin ? [{
chatId: origin.id,
threadId: MAIN_THREAD_ID,
type: 'thread',
}, messageList] : [messageList];
}), }),
).map(({ chatId }) => chatId); ).map(({ chatId }) => chatId);
@ -407,14 +397,17 @@ function reduceMessages<T extends GlobalState>(global: T): GlobalState['messages
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
const threadIds = compact(Object.values(global.byTabId).map(({ id: tabId }) => { const threadIds = unique(compact(Object.values(global.byTabId).map(({ id: tabId }) => {
const { chatId: tabChatId, threadId } = selectCurrentMessageList(global, tabId) || {}; const { chatId: tabChatId, threadId } = selectCurrentMessageList(global, tabId) || {};
if (!tabChatId || tabChatId !== chatId || !threadId || threadId === MAIN_THREAD_ID) { if (!tabChatId || tabChatId !== chatId || !threadId || threadId === MAIN_THREAD_ID) {
return undefined; return undefined;
} }
return threadId; return threadId;
})); }).concat(
Object.values(global.messages.byChatId[chatId].threadsById || {})
.map(({ threadInfo }) => (threadInfo?.isCommentsInfo ? threadInfo?.originMessageId : undefined)),
)));
const threadIdsToSave = threadIds.length ? [MAIN_THREAD_ID, ...threadIds] : [MAIN_THREAD_ID]; const threadIdsToSave = threadIds.length ? [MAIN_THREAD_ID, ...threadIds] : [MAIN_THREAD_ID];
const threadsToSave = pickTruthy(current.threadsById, threadIdsToSave); const threadsToSave = pickTruthy(current.threadsById, threadIdsToSave);

View File

@ -142,7 +142,7 @@ export function isUserRightBanned(chat: ApiChat, key: keyof ApiChatBannedRights)
); );
} }
export function getCanPostInChat(chat: ApiChat, threadId: number, isComments?: boolean) { export function getCanPostInChat(chat: ApiChat, threadId: number, isMessageThread?: boolean) {
if (threadId !== MAIN_THREAD_ID) { if (threadId !== MAIN_THREAD_ID) {
if (chat.isForum) { if (chat.isForum) {
if (chat.isNotJoined) { if (chat.isNotJoined) {
@ -157,7 +157,7 @@ export function getCanPostInChat(chat: ApiChat, threadId: number, isComments?: b
} }
if (chat.isRestricted || chat.isForbidden || chat.migratedTo if (chat.isRestricted || chat.isForbidden || chat.migratedTo
|| (!isComments && chat.isNotJoined) || isChatWithRepliesBot(chat.id)) { || (!isMessageThread && chat.isNotJoined) || isChatWithRepliesBot(chat.id)) {
return false; return false;
} }

View File

@ -14,7 +14,7 @@ import {
} from '../../config'; } from '../../config';
import { getCurrentTabId } from '../../util/establishMultitabRole'; import { getCurrentTabId } from '../../util/establishMultitabRole';
import { import {
areSortedArraysEqual, omit, pickTruthy, unique, areSortedArraysEqual, excludeSortedArray, omit, pick, pickTruthy, unique,
} from '../../util/iteratees'; } from '../../util/iteratees';
import { import {
isLocalMessageId, mergeIdRanges, orderHistoryIds, orderPinnedIds, isLocalMessageId, mergeIdRanges, orderHistoryIds, orderPinnedIds,
@ -31,7 +31,7 @@ import {
selectOutlyingLists, selectOutlyingLists,
selectPinnedIds, selectPinnedIds,
selectScheduledIds, selectScheduledIds,
selectTabState, selectThreadInfo, selectTabState, selectThreadIdFromMessage, selectThreadInfo,
selectViewportIds, selectViewportIds,
} from '../selectors'; } from '../selectors';
import { updateTabState } from './tabs'; import { updateTabState } from './tabs';
@ -100,17 +100,16 @@ export function updateTabThread<T extends GlobalState>(
} }
export function updateThread<T extends GlobalState>( export function updateThread<T extends GlobalState>(
global: T, chatId: string, threadId: number, threadUpdate: Partial<Thread>, global: T, chatId: string, threadId: number, threadUpdate: Partial<Thread> | undefined,
): T { ): T {
const current = global.messages.byChatId[chatId]; if (!threadUpdate) {
return updateMessageStore(global, chatId, {
if (threadUpdate.listedIds?.length) { threadsById: omit(global.messages.byChatId[chatId]?.threadsById, [threadId]),
const lastListedId = threadUpdate.listedIds[threadUpdate.listedIds.length - 1]; });
if (lastListedId) {
global = updateTopicLastMessageId(global, chatId, threadId, lastListedId);
}
} }
const current = global.messages.byChatId[chatId];
return updateMessageStore(global, chatId, { return updateMessageStore(global, chatId, {
threadsById: { threadsById: {
...(current?.threadsById), ...(current?.threadsById),
@ -243,40 +242,49 @@ export function deleteChatMessages<T extends GlobalState>(
if (!byId) { if (!byId) {
return global; return global;
} }
const newById = omit(byId, messageIds);
orderHistoryIds(messageIds);
const updatedThreads = new Map<number, number[]>();
updatedThreads.set(MAIN_THREAD_ID, messageIds);
messageIds.forEach((messageId) => {
const message = byId[messageId];
if (!message) return;
const threadId = selectThreadIdFromMessage(global, message);
if (!threadId || threadId === MAIN_THREAD_ID) return;
const threadMessages = updatedThreads.get(threadId) || [];
threadMessages.push(messageId);
updatedThreads.set(threadId, threadMessages);
});
const deletedForwardedPosts = Object.values(pickTruthy(byId, messageIds)).filter( const deletedForwardedPosts = Object.values(pickTruthy(byId, messageIds)).filter(
({ forwardInfo }) => forwardInfo?.isLinkedChannelPost, ({ forwardInfo }) => forwardInfo?.isLinkedChannelPost,
); );
const threadIds = Object.keys(global.messages.byChatId[chatId].threadsById).map(Number); updatedThreads.forEach((threadMessageIds, threadId) => {
threadIds.forEach((threadId) => {
const threadInfo = selectThreadInfo(global, chatId, threadId); const threadInfo = selectThreadInfo(global, chatId, threadId);
let listedIds = selectListedIds(global, chatId, threadId); let listedIds = selectListedIds(global, chatId, threadId);
let pinnedIds = selectPinnedIds(global, chatId, threadId); let pinnedIds = selectPinnedIds(global, chatId, threadId);
let outlyingLists = selectOutlyingLists(global, chatId, threadId); let outlyingLists = selectOutlyingLists(global, chatId, threadId);
let mainPinnedIds = selectPinnedIds(global, chatId, MAIN_THREAD_ID);
let newMessageCount = threadInfo?.messagesCount; let newMessageCount = threadInfo?.messagesCount;
messageIds.forEach((messageId) => { if (listedIds) {
if (listedIds?.includes(messageId)) { listedIds = excludeSortedArray(listedIds, threadMessageIds);
listedIds = listedIds.filter((id) => id !== messageId); }
if (newMessageCount !== undefined && !isLocalMessageId(messageId)) newMessageCount -= 1;
}
outlyingLists = outlyingLists?.map((list) => { if (outlyingLists) {
if (!list.includes(messageId)) return list; outlyingLists = outlyingLists.map((list) => excludeSortedArray(list, threadMessageIds));
return list.filter((id) => id !== messageId); }
});
if (pinnedIds?.includes(messageId)) { if (pinnedIds) {
pinnedIds = pinnedIds.filter((id) => id !== messageId); pinnedIds = excludeSortedArray(pinnedIds, orderPinnedIds(threadMessageIds));
} }
if (mainPinnedIds?.includes(messageId)) { const nonLocalMessageCount = threadMessageIds.filter((id) => !isLocalMessageId(id)).length;
mainPinnedIds = mainPinnedIds.filter((id) => id !== messageId); if (newMessageCount !== undefined) {
} newMessageCount -= nonLocalMessageCount;
}); }
Object.values(global.byTabId).forEach(({ id: tabId }) => { Object.values(global.byTabId).forEach(({ id: tabId }) => {
let viewportIds = selectViewportIds(global, chatId, threadId, tabId); let viewportIds = selectViewportIds(global, chatId, threadId, tabId);
@ -293,7 +301,6 @@ export function deleteChatMessages<T extends GlobalState>(
global = replaceThreadParam(global, chatId, threadId, 'listedIds', listedIds); global = replaceThreadParam(global, chatId, threadId, 'listedIds', listedIds);
global = replaceThreadParam(global, chatId, threadId, 'outlyingLists', outlyingLists); global = replaceThreadParam(global, chatId, threadId, 'outlyingLists', outlyingLists);
global = replaceThreadParam(global, chatId, threadId, 'pinnedIds', pinnedIds); global = replaceThreadParam(global, chatId, threadId, 'pinnedIds', pinnedIds);
global = replaceThreadParam(global, chatId, MAIN_THREAD_ID, 'pinnedIds', mainPinnedIds);
if (threadInfo && newMessageCount !== undefined) { if (threadInfo && newMessageCount !== undefined) {
global = updateThreadInfo(global, chatId, threadId, { global = updateThreadInfo(global, chatId, threadId, {
@ -313,16 +320,17 @@ export function deleteChatMessages<T extends GlobalState>(
const { fromChatId, fromMessageId } = message.forwardInfo!; const { fromChatId, fromMessageId } = message.forwardInfo!;
const originalPost = selectChatMessage(global, fromChatId!, fromMessageId!); const originalPost = selectChatMessage(global, fromChatId!, fromMessageId!);
if (canDeleteCurrentThread && currentThreadId === fromMessageId) { if (canDeleteCurrentThread && currentThreadId === message.id) {
global = updateCurrentMessageList(global, chatId, undefined, undefined, undefined, undefined, tabId); global = updateCurrentMessageList(global, chatId, undefined, undefined, undefined, undefined, tabId);
} }
if (originalPost) { if (originalPost) {
global = updateChatMessage(global, fromChatId!, fromMessageId!, { repliesThreadInfo: undefined }); global = updateThread(global, fromChatId!, fromMessageId!, undefined);
} }
}); });
}); });
} }
const newById = omit(byId, messageIds);
global = replaceChatMessages(global, chatId, newById); global = replaceChatMessages(global, chatId, newById);
return global; return global;
@ -477,14 +485,26 @@ export function safeReplacePinnedIds<T extends GlobalState>(
export function updateThreadInfo<T extends GlobalState>( export function updateThreadInfo<T extends GlobalState>(
global: T, chatId: string, threadId: number, update: Partial<ApiThreadInfo> | undefined, global: T, chatId: string, threadId: number, update: Partial<ApiThreadInfo> | undefined,
doNotUpdateLinked?: boolean,
): T { ): T {
const newThreadInfo = { const newThreadInfo = {
...(selectThreadInfo(global, chatId, threadId) as ApiThreadInfo), ...(selectThreadInfo(global, chatId, threadId) as ApiThreadInfo),
...update, ...update,
}; } as ApiThreadInfo;
if (!newThreadInfo.threadId) { if (!doNotUpdateLinked) {
return global; const linkedUpdate = pick(newThreadInfo, ['messagesCount', 'lastMessageId', 'lastReadInboxMessageId']);
if (newThreadInfo.isCommentsInfo) {
if (newThreadInfo.threadId) {
global = updateThreadInfo(
global, newThreadInfo.chatId, newThreadInfo.threadId, linkedUpdate, true,
);
}
} else if (newThreadInfo.fromChannelId && newThreadInfo.fromMessageId) {
global = updateThreadInfo(
global, newThreadInfo.fromChannelId, newThreadInfo.fromMessageId, linkedUpdate, true,
);
}
} }
return replaceThreadParam(global, chatId, threadId, 'threadInfo', newThreadInfo); return replaceThreadParam(global, chatId, threadId, 'threadInfo', newThreadInfo);
@ -494,7 +514,10 @@ export function updateThreadInfos<T extends GlobalState>(
global: T, updates: Partial<ApiThreadInfo>[], global: T, updates: Partial<ApiThreadInfo>[],
): T { ): T {
updates.forEach((update) => { updates.forEach((update) => {
global = updateThreadInfo(global, update.chatId!, update.threadId!, update); global = updateThreadInfo(global,
update.isCommentsInfo ? update.originChannelId! : update.chatId!,
update.isCommentsInfo ? update.originMessageId! : update.threadId!,
update);
}); });
return global; return global;

View File

@ -248,34 +248,6 @@ export function selectThreadMessagesCount(global: GlobalState, chatId: string, t
return threadInfo.messagesCount; return threadInfo.messagesCount;
} }
export function selectThreadOriginChat<T extends GlobalState>(global: T, chatId: string, threadId: number) {
if (threadId === MAIN_THREAD_ID) {
return selectChat(global, chatId);
}
const threadInfo = selectThreadInfo(global, chatId, threadId);
return selectChat(global, threadInfo?.originChannelId || chatId);
}
export function selectThreadTopMessageId<T extends GlobalState>(global: T, chatId: string, threadId: number) {
if (threadId === MAIN_THREAD_ID) {
return undefined;
}
const chat = selectChat(global, chatId);
if (chat?.isForum) {
return threadId;
}
const threadInfo = selectThreadInfo(global, chatId, threadId);
if (!threadInfo) {
return undefined;
}
return threadInfo.topMessageId;
}
export function selectThreadByMessage<T extends GlobalState>(global: T, message: ApiMessage) { export function selectThreadByMessage<T extends GlobalState>(global: T, message: ApiMessage) {
const threadId = selectThreadIdFromMessage(global, message); const threadId = selectThreadIdFromMessage(global, message);
if (!threadId || threadId === MAIN_THREAD_ID) { if (!threadId || threadId === MAIN_THREAD_ID) {
@ -325,10 +297,12 @@ export function selectIsViewportNewest<T extends GlobalState>(
} else { } else {
const threadInfo = selectThreadInfo(global, chatId, threadId); const threadInfo = selectThreadInfo(global, chatId, threadId);
if (!threadInfo || !threadInfo.lastMessageId) { if (!threadInfo || !threadInfo.lastMessageId) {
return undefined; if (!threadInfo?.threadId) return undefined;
// No messages in thread, except for the thread message itself
lastMessageId = threadInfo?.threadId;
} else {
lastMessageId = threadInfo.lastMessageId;
} }
lastMessageId = threadInfo.lastMessageId;
} }
// Edge case: outgoing `lastMessage` is updated with a delay to optimize animation // Edge case: outgoing `lastMessage` is updated with a delay to optimize animation
@ -580,9 +554,9 @@ export function selectAllowedMessageActions<T extends GlobalState>(global: T, me
); );
const threadInfo = selectThreadInfo(global, message.chatId, threadId); const threadInfo = selectThreadInfo(global, message.chatId, threadId);
const isComments = Boolean(threadInfo?.originChannelId); const isMessageThread = Boolean(!threadInfo?.isCommentsInfo && threadInfo?.fromChannelId);
const canReply = !isLocal && !isServiceNotification && !chat.isForbidden const canReply = !isLocal && !isServiceNotification && !chat.isForbidden
&& getCanPostInChat(chat, threadId, isComments) && getCanPostInChat(chat, threadId, isMessageThread)
&& (!messageTopic || !messageTopic.isClosed || messageTopic.isOwner || getHasAdminRight(chat, 'manageTopics')); && (!messageTopic || !messageTopic.isClosed || messageTopic.isOwner || getHasAdminRight(chat, 'manageTopics'));
const hasPinPermission = isPrivate || ( const hasPinPermission = isPrivate || (
@ -799,7 +773,7 @@ export function selectRealLastReadId<T extends GlobalState>(global: T, chatId: s
} }
if (!threadInfo.lastReadInboxMessageId) { if (!threadInfo.lastReadInboxMessageId) {
return threadInfo.topMessageId; return threadInfo.threadId;
} }
// Some previously read messages may be deleted // Some previously read messages may be deleted
@ -977,9 +951,15 @@ export function selectNewestMessageWithBotKeyboardButtons<T extends GlobalState>
return undefined; return undefined;
} }
const messageId = findLast(viewportIds, (id) => selectShouldDisplayReplyKeyboard(global, chatMessages[id])); const messageId = findLast(viewportIds, (id) => {
const message = chatMessages[id];
return message && selectShouldDisplayReplyKeyboard(global, message);
});
const replyHideMessageId = findLast(viewportIds, (id) => selectShouldHideReplyKeyboard(global, chatMessages[id])); const replyHideMessageId = findLast(viewportIds, (id) => {
const message = chatMessages[id];
return message && selectShouldHideReplyKeyboard(global, message);
});
if (messageId && replyHideMessageId && replyHideMessageId > messageId) { if (messageId && replyHideMessageId && replyHideMessageId > messageId) {
return undefined; return undefined;
@ -1391,20 +1371,18 @@ export function selectTopicLink<T extends GlobalState>(
} }
export function selectMessageReplyInfo<T extends GlobalState>( export function selectMessageReplyInfo<T extends GlobalState>(
global: T, chatId: string, threadId: number = MAIN_THREAD_ID, additionalReplyInfo?: ApiInputMessageReplyInfo, global: T, chatId: string, threadId: number, additionalReplyInfo?: ApiInputMessageReplyInfo,
) { ) {
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
if (!chat) return undefined; if (!chat) return undefined;
const isMainThread = threadId === MAIN_THREAD_ID;
const replyingToTopId = selectThreadTopMessageId(global, chatId, threadId); if (!additionalReplyInfo && isMainThread) return undefined;
if (!additionalReplyInfo && !replyingToTopId) return undefined;
const replyInfo: ApiInputMessageReplyInfo = { const replyInfo: ApiInputMessageReplyInfo = {
type: 'message', type: 'message',
...additionalReplyInfo, ...additionalReplyInfo,
replyToMsgId: additionalReplyInfo?.replyToMsgId || replyingToTopId!, replyToMsgId: additionalReplyInfo?.replyToMsgId || threadId,
replyToTopId: additionalReplyInfo?.replyToTopId || replyingToTopId, replyToTopId: additionalReplyInfo?.replyToTopId || (!isMainThread ? threadId : undefined),
}; };
return replyInfo; return replyInfo;

View File

@ -400,6 +400,11 @@ export type TabState = {
webPagePreview?: ApiWebPage; webPagePreview?: ApiWebPage;
loadingThread?: {
loadingChatId: string;
loadingMessageId: number;
};
forwardMessages: { forwardMessages: {
isModalShown?: boolean; isModalShown?: boolean;
fromChatId?: string; fromChatId?: string;
@ -1176,6 +1181,7 @@ export interface ActionPayloads {
shouldReplace?: boolean; shouldReplace?: boolean;
}; };
openChatWithInfo: ActionPayloads['openChat'] & { profileTab?: ProfileTabType } & WithTabId; openChatWithInfo: ActionPayloads['openChat'] & { profileTab?: ProfileTabType } & WithTabId;
openThreadWithInfo: ActionPayloads['openThread'] & WithTabId;
openLinkedChat: { id: string } & WithTabId; openLinkedChat: { id: string } & WithTabId;
loadMoreMembers: WithTabId | undefined; loadMoreMembers: WithTabId | undefined;
setActiveChatFolder: { setActiveChatFolder: {
@ -1211,11 +1217,6 @@ export interface ActionPayloads {
id: number; id: number;
}; };
openSupportChat: WithTabId | undefined; openSupportChat: WithTabId | undefined;
focusMessageInComments: {
chatId: string;
threadId: number;
messageId: number;
} & WithTabId;
openChatByPhoneNumber: { openChatByPhoneNumber: {
phoneNumber: string; phoneNumber: string;
startAttach?: string | boolean; startAttach?: string | boolean;
@ -1267,6 +1268,8 @@ export interface ActionPayloads {
chatId?: string; chatId?: string;
threadId?: number; threadId?: number;
shouldForceRender?: boolean; shouldForceRender?: boolean;
onLoaded?: NoneToVoidFunction;
onError?: NoneToVoidFunction;
} & WithTabId; } & WithTabId;
sendMessage: { sendMessage: {
text?: string; text?: string;
@ -1400,10 +1403,6 @@ export interface ActionPayloads {
usernameOrId: string; usernameOrId: string;
isPrivate?: boolean; isPrivate?: boolean;
} & WithTabId; } & WithTabId;
requestThreadInfoUpdate: {
chatId: string;
threadId: number;
};
setScrollOffset: { setScrollOffset: {
chatId: string; chatId: string;
threadId: number; threadId: number;
@ -1769,17 +1768,36 @@ export interface ActionPayloads {
openChat: { openChat: {
id: string | undefined; id: string | undefined;
threadId?: number;
type?: MessageListType; type?: MessageListType;
shouldReplaceHistory?: boolean; shouldReplaceHistory?: boolean;
shouldReplaceLast?: boolean; shouldReplaceLast?: boolean;
noForumTopicPanel?: boolean; noForumTopicPanel?: boolean;
noRequestThreadInfoUpdate?: boolean;
} & WithTabId; } & WithTabId;
openComments: { openThread: {
id: string; type?: MessageListType;
shouldReplaceHistory?: boolean;
shouldReplaceLast?: boolean;
noForumTopicPanel?: boolean;
focusMessageId?: number;
} & ({
isComments: true;
chatId?: string;
originMessageId: number;
originChannelId: string;
} | {
isComments?: false;
chatId: string;
threadId: number; threadId: number;
originChannelId?: string; }) & WithTabId;
// Used by both openThread & openChat
processOpenChatOrThread: {
chatId: string | undefined;
threadId: number;
type?: MessageListType;
shouldReplaceHistory?: boolean;
shouldReplaceLast?: boolean;
noForumTopicPanel?: boolean;
isComments?: boolean;
} & WithTabId; } & WithTabId;
loadFullChat: { loadFullChat: {
chatId: string; chatId: string;

View File

@ -63,6 +63,16 @@ export function omit<T extends object, K extends keyof T>(object: T, keys: K[]):
return pick(object, savedKeys); return pick(object, savedKeys);
} }
export function omitUndefined<T extends object>(object: T): T {
return Object.keys(object).reduce((result, stringKey) => {
const key = stringKey as keyof T;
if (object[key] !== undefined) {
result[key as keyof T] = object[key];
}
return result;
}, {} as T);
}
export function orderBy<T>( export function orderBy<T>(
collection: T[], collection: T[],
orderRule: (keyof T) | OrderCallback<T> | ((keyof T) | OrderCallback<T>)[], orderRule: (keyof T) | OrderCallback<T> | ((keyof T) | OrderCallback<T>)[],
@ -119,6 +129,29 @@ export function areSortedArraysIntersecting(array1: any[], array2: any[]) {
export function findIntersectionWithSet<T>(array: T[], set: Set<T>): T[] { export function findIntersectionWithSet<T>(array: T[], set: Set<T>): T[] {
return array.filter((a) => set.has(a)); return array.filter((a) => set.has(a));
} }
/**
* Exlude elements from base array. Both arrays should be sorted in same order
* @param base
* @param toExclude
* @returns New array without excluded elements
*/
export function excludeSortedArray<T extends any>(base: T[], toExclude: T[]) {
if (!base?.length) return base;
const result: T[] = [];
let excludeIndex = 0;
for (let i = 0; i < base.length; i++) {
if (toExclude[excludeIndex] === base[i]) {
excludeIndex += 1;
} else {
result.push(base[i]);
}
}
return result;
}
export function split<T extends any>(array: T[], chunkSize: number) { export function split<T extends any>(array: T[], chunkSize: number) {
const result: T[][] = []; const result: T[][] = [];

View File

@ -7,7 +7,7 @@ import type { MethodArgs, Methods } from '../api/gramjs/methods/types';
import type { ApiInitialArgs } from '../api/types'; import type { ApiInitialArgs } from '../api/types';
import type { GlobalState } from '../global/types'; import type { GlobalState } from '../global/types';
import { DATA_BROADCAST_CHANNEL_NAME, DEBUG, MULTITAB_LOCALSTORAGE_KEY } from '../config'; import { DATA_BROADCAST_CHANNEL_NAME, MULTITAB_LOCALSTORAGE_KEY } from '../config';
import { selectTabState } from '../global/selectors'; import { selectTabState } from '../global/selectors';
import { import {
callApiLocal, callApiLocal,