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,
} from '../../types';
import {
ApiMessageEntityTypes,
ApiMessageEntityTypes, MAIN_THREAD_ID,
} from '../../types';
import {
@ -41,7 +41,7 @@ import {
SUPPORTED_VIDEO_CONTENT_TYPES,
} from '../../../config';
import { getEmojiOnlyCountForMessage } from '../../../global/helpers/getEmojiOnlyCountForMessage';
import { pick } from '../../../util/iteratees';
import { omitUndefined, pick } from '../../../util/iteratees';
import { getServerTime, getServerTimeOffset } from '../../../util/serverTime';
import { interpolateArray } from '../../../util/waveform';
import { buildPeer } from '../gramjsBuilders';
@ -178,12 +178,12 @@ export function buildApiMessageWithChatId(
const isInvoiceMedia = mtpMessage.media instanceof GramJs.MessageMediaInvoice
&& Boolean(mtpMessage.media.extendedMedia);
const isEdited = mtpMessage.editDate && !mtpMessage.editHide;
const isEdited = Boolean(mtpMessage.editDate) && !mtpMessage.editHide;
const {
inlineButtons, keyboardButtons, keyboardPlaceholder, isKeyboardSingleUse, isKeyboardSelective,
} = buildReplyButtons(mtpMessage, isInvoiceMedia) || {};
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 isInAlbum = Boolean(groupedId) && !(content.document || content.audio || content.sticker);
const shouldHideKeyboardButtons = mtpMessage.replyMarkup instanceof GramJs.ReplyKeyboardHide;
@ -192,8 +192,9 @@ export function buildApiMessageWithChatId(
const isProtected = mtpMessage.noforwards || isInvoiceMedia;
const isForwardingAllowed = !mtpMessage.noforwards;
const emojiOnlyCount = getEmojiOnlyCountForMessage(content, groupedId);
const hasComments = mtpMessage.replies?.comments;
return {
return omitUndefined({
id: mtpMessage.id,
chatId,
isOutgoing,
@ -209,12 +210,12 @@ export function buildApiMessageWithChatId(
reactions: mtpMessage.reactions && buildMessageReactions(mtpMessage.reactions),
emojiOnlyCount,
...(mtpMessage.replyTo && { replyInfo: buildApiReplyInfo(mtpMessage.replyTo) }),
...(forwardInfo && { forwardInfo }),
...(isEdited && { isEdited }),
...(mtpMessage.editDate && { editDate: mtpMessage.editDate }),
...(isMediaUnread && { isMediaUnread }),
...(mtpMessage.mentioned && isMediaUnread && { hasUnreadMention: true }),
...(mtpMessage.mentioned && { isMentioned: true }),
forwardInfo,
isEdited,
editDate: mtpMessage.editDate,
isMediaUnread,
hasUnreadMention: mtpMessage.mentioned && isMediaUnread,
isMentioned: mtpMessage.mentioned,
...(groupedId && {
groupedId,
isInAlbum,
@ -225,11 +226,11 @@ export function buildApiMessageWithChatId(
}),
...(shouldHideKeyboardButtons && { shouldHideKeyboardButtons, isHideKeyboardSelective }),
...(mtpMessage.viaBotId && { viaBotId: buildApiPeerId(mtpMessage.viaBotId, 'user') }),
...(replies && { repliesThreadInfo: buildThreadInfo(replies, mtpMessage.id, chatId) }),
...(postAuthor && { postAuthorTitle: postAuthor }),
postAuthorTitle: postAuthor,
isProtected,
isForwardingAllowed,
};
hasComments,
} satisfies ApiMessage);
}
export function buildMessageDraft(draft: GramJs.TypeDraftMessage): ApiDraft | undefined {
@ -830,8 +831,11 @@ export function buildLocalForwardedMessage({
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',
replyToMsgId: toThreadId,
replyToTopId: toThreadId,
isForumTopic: toChat.isForum || 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,
): ApiThreadInfo | undefined {
const {
@ -980,21 +998,28 @@ function buildThreadInfo(
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 {
isComments: comments,
...baseThreadInfo,
isCommentsInfo: false,
chatId,
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 { scheduleMutedChatUpdate } from '../scheduleUnmute';
import { applyState, processUpdate, updateChannelState } from '../updateManager';
import { dispatchThreadInfoUpdates } from '../updater';
import { invokeRequest, uploadFile } from './client';
type FullChatData = {
@ -130,6 +131,8 @@ export async function fetchChats({
'chatId',
);
dispatchThreadInfoUpdates(result.messages);
const peersByKey = preparePeers(result);
if (resultPinned) {
Object.assign(peersByKey, preparePeers(resultPinned, peersByKey));
@ -340,6 +343,8 @@ export async function requestChatUpdate({
updateLocalDb(result);
const lastRemoteMessage = buildApiMessage(result.messages[0]);
dispatchThreadInfoUpdates(result.messages);
const lastMessage = lastLocalMessage && (!lastRemoteMessage || (lastLocalMessage.date > lastRemoteMessage.date))
? lastLocalMessage
: lastRemoteMessage;
@ -542,7 +547,7 @@ async function getFullChannelInfo(
kickedMembers,
adminMembersById: adminMembers ? buildCollectionByKey(adminMembers, 'userId') : undefined,
groupCallId: call ? String(call.id) : undefined,
linkedChatId: linkedChatId ? buildApiPeerId(linkedChatId, 'chat') : undefined,
linkedChatId: linkedChatId ? buildApiPeerId(linkedChatId, 'channel') : undefined,
botCommands,
enabledReactions: buildApiChatReactions(availableReactions),
sendAsId: defaultSendAs ? getApiChatIdFromMtpPeer(defaultSendAs) : undefined,
@ -1584,6 +1589,7 @@ export async function fetchTopics({
const topics = result.topics.map(buildApiTopic).filter(Boolean);
const messages = result.messages.map(buildApiMessage).filter(Boolean);
dispatchThreadInfoUpdates(result.messages);
const users = result.users.map(buildApiUser).filter(Boolean);
const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean);
const draftsById = result.topics.reduce((acc, topic) => {
@ -1637,6 +1643,7 @@ export async function fetchTopicById({
updateLocalDb(result);
const messages = result.messages.map(buildApiMessage).filter(Boolean);
dispatchThreadInfoUpdates(result.messages);
const users = result.users.map(buildApiUser).filter(Boolean);
const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean);

View File

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

View File

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

View File

@ -7,7 +7,7 @@ import type {
} from '../types';
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 { buildApiBotMenuButton } from './apiBuilders/bots';
import {
@ -38,6 +38,7 @@ import {
buildApiMessageFromNotification,
buildApiMessageFromShort,
buildApiMessageFromShortChat,
buildApiThreadInfoFromMessage,
buildMessageDraft,
} from './apiBuilders/messages';
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) {
onUpdate(update);
}
@ -199,6 +210,8 @@ export function updater(update: Update) {
}
message = buildApiMessage(update.message)!;
dispatchThreadInfoUpdates([update.message]);
shouldForceReply = 'replyMarkup' in update.message
&& update.message?.replyMarkup instanceof GramJs.ReplyKeyboardForceReply
&& (!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
const message = omit(buildApiMessage(update.message)!, ['isOutgoing']);
dispatchThreadInfoUpdates([update.message]);
onUpdate({
'@type': 'updateMessage',
@ -548,12 +562,12 @@ export function updater(update: Update) {
});
} else if (update instanceof GramJs.UpdateReadChannelDiscussionInbox) {
onUpdate({
'@type': 'updateThreadInfo',
chatId: buildApiPeerId(update.channelId, 'channel'),
threadId: update.topMsgId,
threadInfo: {
'@type': 'updateThreadInfos',
threadInfoUpdates: [{
chatId: buildApiPeerId(update.channelId, 'channel'),
threadId: update.topMsgId,
lastReadInboxMessageId: update.readMaxId,
},
}],
});
} else if (update instanceof GramJs.UpdateReadChannelDiscussionOutbox) {
onUpdate({

View File

@ -480,7 +480,6 @@ export interface ApiMessage {
isKeyboardSingleUse?: boolean;
isKeyboardSelective?: boolean;
viaBotId?: string;
repliesThreadInfo?: ApiThreadInfo;
postAuthorTitle?: string;
isScheduled?: boolean;
shouldHideKeyboardButtons?: boolean;
@ -500,6 +499,7 @@ export interface ApiMessage {
reactions: ApiPeerReaction[];
};
reactions?: ApiReactions;
hasComments?: boolean;
}
export interface ApiReactions {
@ -559,18 +559,31 @@ export type ApiReactionCustomEmoji = {
export type ApiReaction = ApiReactionEmoji | ApiReactionCustomEmoji;
export interface ApiThreadInfo {
isComments?: boolean;
threadId: number;
interface ApiBaseThreadInfo {
chatId: string;
topMessageId?: number;
originChannelId?: string;
messagesCount: number;
lastMessageId?: number;
lastReadInboxMessageId?: number;
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 ApiSponsoredMessage = {

View File

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

View File

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

View File

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

View File

@ -260,9 +260,9 @@ export default memo(withGlobal<OwnProps>(
const chat = chatId && selectChat(global, chatId);
const sendOptions = chat ? getAllowedAttachmentOptions(chat) : 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(
chat && threadId && getCanPostInChat(chat, threadId, isComments) && sendOptions?.canSendStickers,
chat && threadId && getCanPostInChat(chat, threadId, isMessageThread) && sendOptions?.canSendStickers,
);
const isSavedMessages = Boolean(chatId) && selectIsChatWithSelf(global, chatId);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,7 +18,9 @@ export function isAlbum(messageOrAlbum: ApiMessage | IAlbum): messageOrAlbum is
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 currentDateGroup = {
originalDate: messages[0].date,
@ -39,7 +41,7 @@ export function groupMessages(messages: ApiMessage[], firstUnreadId?: number, is
};
} else {
currentAlbum.messages.push(message);
if (message.content.text) {
if (message.hasComments || (message.content.text && !currentAlbum.mainMessage.hasComments)) {
currentAlbum.mainMessage = message;
}
}
@ -56,6 +58,7 @@ export function groupMessages(messages: ApiMessage[], firstUnreadId?: number, is
currentSenderGroup.push(currentAlbum);
currentAlbum = undefined;
}
const lastSenderGroupItem = currentSenderGroup[currentSenderGroup.length - 1];
if (nextMessage) {
const nextMessageDayStartsAt = getDayStartAt(nextMessage.date * 1000);
if (currentDateGroup.datetime !== nextMessageDayStartsAt) {
@ -77,6 +80,11 @@ export function groupMessages(messages: ApiMessage[], firstUnreadId?: number, is
|| message.inlineButtons
|| nextMessage.inlineButtons
|| (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)
) {
currentSenderGroup = [];

View File

@ -7,7 +7,6 @@ import type { Signal } from '../../../util/signals';
import { LoadMoreDirection } from '../../../types';
import { requestMeasure } from '../../../lib/fasterdom/fasterdom';
import { isLocalMessageId } from '../../../global/helpers';
import { debounce } from '../../../util/schedulers';
import { MESSAGE_LIST_SENSITIVE_AREA } from '../../../util/windowEnvironment';
@ -92,12 +91,6 @@ export default function useScrollHooks(
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 }) => {
if (!isIntersecting) return;

View File

@ -20,6 +20,11 @@
transition: background-color 0.15s, color 0.15s;
user-select: none;
.label {
overflow: hidden;
text-overflow: ellipsis;
}
.Message .has-appendix &::before {
content: "";
display: block;
@ -45,7 +50,7 @@
bottom: 3rem;
height: 3.375rem;
border-radius: 1.375rem;
padding: 0.375rem 0.3125rem 0.25rem;
padding: 0.375rem;
align-items: flex-start;
color: white;
background-color: var(--pattern-color);
@ -66,7 +71,7 @@
}
}
.Message:hover & {
.Message:hover &, &.loading {
opacity: 1;
}
@ -97,7 +102,7 @@
.recent-repliers,
.icon-comments,
.label,
.icon-next {
.CommentButton_icon-open {
display: none;
}
}
@ -153,8 +158,8 @@
margin-inline-end: 0.875rem;
}
.icon-next {
margin-inline-start: auto;
.CommentButton_icon-open {
position: absolute;
font-size: 1.5rem;
}
@ -210,3 +215,34 @@
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 { getActions, getGlobal } from '../../../global';
import type {
ApiThreadInfo,
} from '../../../api/types';
import type { ApiCommentsInfo } from '../../../api/types';
import { selectPeer } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
@ -12,30 +10,42 @@ import { formatIntegerCompact } from '../../../util/textFormat';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useAsyncRendering from '../../right/hooks/useAsyncRendering';
import AnimatedCounter from '../../common/AnimatedCounter';
import Avatar from '../../common/Avatar';
import Spinner from '../../ui/Spinner';
import './CommentButton.scss';
type OwnProps = {
threadInfo: ApiThreadInfo;
threadInfo: ApiCommentsInfo;
disabled?: boolean;
isLoading?: boolean;
isCustomShape?: boolean;
};
const SHOW_LOADER_DELAY = 450;
const CommentButton: FC<OwnProps> = ({
isCustomShape,
threadInfo,
disabled,
isLoading,
}) => {
const { openComments } = getActions();
const { openThread } = getActions();
const shouldRenderLoading = useAsyncRendering([isLoading], SHOW_LOADER_DELAY);
const lang = useLang();
const {
threadId, chatId, messagesCount, lastMessageId, lastReadInboxMessageId, recentReplierIds, originChannelId,
originMessageId, chatId, messagesCount, lastMessageId, lastReadInboxMessageId, recentReplierIds, originChannelId,
} = threadInfo;
const handleClick = useLastCallback(() => {
openComments({ id: chatId, threadId, originChannelId });
openThread({
isComments: true, chatId, originMessageId, originChannelId,
});
});
const recentRepliers = useMemo(() => {
@ -73,7 +83,7 @@ const CommentButton: FC<OwnProps> = ({
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('%')
.map((s) => {
return (s === 'COMMENTS_COUNT' ? <AnimatedCounter text={formatIntegerCompact(messagesCount)} /> : s);
@ -83,17 +93,48 @@ const CommentButton: FC<OwnProps> = ({
return (
<div
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'}
onClick={handleClick}
role="button"
tabIndex={0}
>
<i className="icon icon-comments-sticker" />
{(!recentRepliers || recentRepliers.length === 0) && <i className="icon icon-comments" />}
<i
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()}
<div className="label" dir="auto">
{messagesCount ? commentsText : lang('LeaveAComment')}
</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>
);
};

View File

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

View File

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

View File

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

View File

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

View File

@ -169,8 +169,8 @@ export default memo(withGlobal(
const isChatWithBot = chat ? selectIsChatWithBot(global, chat) : undefined;
const isSavedMessages = Boolean(chatId) && selectIsChatWithSelf(global, chatId);
const threadInfo = chatId && threadId ? selectThreadInfo(global, chatId, threadId) : undefined;
const isComments = Boolean(threadInfo?.originChannelId);
const canPostInChat = Boolean(chat) && Boolean(threadId) && getCanPostInChat(chat, threadId, isComments);
const isMessageThread = Boolean(!threadInfo?.isCommentsInfo && threadInfo?.fromChannelId);
const canPostInChat = Boolean(chat) && Boolean(threadId) && getCanPostInChat(chat, threadId, isMessageThread);
return {
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 { RequiredGlobalActions } from '../../index';
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 { getCurrentTabId } from '../../../util/establishMultitabRole';
@ -793,8 +794,8 @@ addActionHandler('callAttachBot', (global, actions, payload): ActionReturnType =
}
if ('chatId' in payload) {
const { chatId, threadId, url } = payload;
actions.openChat({ id: chatId, threadId, tabId });
const { chatId, threadId = MAIN_THREAD_ID, url } = payload;
actions.openThread({ chatId, threadId, tabId });
actions.requestWebView({
url,
peerId: chatId!,

View File

@ -30,7 +30,7 @@ import {
import { formatShareText, parseChooseParameter, processDeepLink } from '../../../util/deeplink';
import { getCurrentTabId } from '../../../util/establishMultitabRole';
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 { debounce, pause, throttle } from '../../../util/schedulers';
import { extractCurrentThemeParams } from '../../../util/themeStyle';
@ -70,6 +70,7 @@ import {
updateListedTopicIds,
updateManagementProgress,
updatePeerFullInfo,
updateThread,
updateThreadInfo,
updateTopic,
updateTopics,
@ -78,13 +79,24 @@ import {
import { updateGroupCall } from '../../reducers/calls';
import { updateTabState } from '../../reducers/tabs';
import {
selectChat, selectChatByUsername,
selectChatFolder, selectChatFullInfo, selectChatListType, selectCurrentChat, selectCurrentMessageList, selectDraft,
selectChat,
selectChatByUsername,
selectChatFolder,
selectChatFullInfo,
selectChatListType,
selectCurrentChat,
selectCurrentMessageList,
selectDraft,
selectIsChatPinned,
selectLastServiceNotification,
selectStickerSet,
selectSupportChat, selectTabState, selectThread, selectThreadInfo, selectThreadOriginChat, selectThreadTopMessageId,
selectUser, selectUserByPhoneNumber, selectVisibleUsers,
selectSupportChat,
selectTabState,
selectThread,
selectThreadInfo,
selectUser,
selectUserByPhoneNumber,
selectVisibleUsers,
} from '../../selectors';
import { selectGroupCall } from '../../selectors/calls';
import { selectCurrentLimit } from '../../selectors/limits';
@ -132,16 +144,19 @@ addActionHandler('preloadTopChatMessages', async (global, actions): Promise<void
}
});
addActionHandler('openChat', (global, actions, payload): ActionReturnType => {
const {
id, threadId = MAIN_THREAD_ID, noRequestThreadInfoUpdate, tabId = getCurrentTabId(),
} = payload;
function abortChatRequests(chatId: string, threadId?: number) {
callApi('abortChatRequests', { chatId, threadId });
}
function abortChatRequestsForCurrentChat<T extends GlobalState>(
global: T, newChatId?: string, newThreadId?: number,
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
const currentMessageList = selectCurrentMessageList(global, tabId);
const currentChatId = currentMessageList?.chatId;
const currentThreadId = currentMessageList?.threadId;
if (currentChatId && (currentChatId !== id || currentThreadId !== threadId)) {
if (currentChatId && (currentChatId !== newChatId || currentThreadId !== newThreadId)) {
const [isChatOpened, isThreadOpened] = Object.values(global.byTabId)
.reduce(([accHasChatOpened, accHasThreadOpened], { id: otherTabId }) => {
if (otherTabId === tabId || (accHasChatOpened && accHasThreadOpened)) {
@ -153,14 +168,33 @@ addActionHandler('openChat', (global, actions, payload): ActionReturnType => {
const isSameThread = isSameChat && otherMessageList?.threadId === currentThreadId;
return [accHasChatOpened || isSameChat, accHasThreadOpened || isSameThread];
}, [currentChatId === id, false]);
}, [currentChatId === newChatId, false]);
const shouldAbortChatRequests = !isChatOpened || !isThreadOpened;
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) {
return;
@ -186,54 +220,227 @@ addActionHandler('openChat', (global, actions, payload): ActionReturnType => {
actions.requestChatUpdate({ chatId: id });
}
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 {
id, threadId, originChannelId, tabId = getCurrentTabId(),
type, isComments, noForumTopicPanel, shouldReplaceHistory, shouldReplaceLast,
focusMessageId,
tabId = getCurrentTabId(),
} = payload;
let { chatId } = payload;
let threadId: number | undefined;
let loadingChatId: string;
let loadingThreadId: number;
if (threadId !== MAIN_THREAD_ID) {
const topMessageId = selectThreadTopMessageId(global, id, threadId);
if (!topMessageId) {
const chat = selectThreadOriginChat(global, id, threadId);
if (!chat) {
return;
}
if (!isComments) {
loadingChatId = payload.chatId;
threadId = payload.threadId;
loadingThreadId = threadId;
const originalChat = selectChat(global, loadingChatId);
if (threadId === MAIN_THREAD_ID) {
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 });
if (!result) {
actions.openPreviousChat({ tabId });
return;
}
loadingChatId = originChannelId;
loadingThreadId = originMessageId;
}
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 = addUsers(global, buildCollectionByKey(result.users, 'id'));
global = updateTabState(global, {
loadingThread: undefined,
}, tabId);
setGlobal(global);
actions.openChat({
id,
threadId: result.topMessageId,
actions.showNotification({
message: langProvider.translate('Group.ErrorAccessDenied'),
tabId,
shouldReplaceLast: true,
noRequestThreadInfoUpdate: true,
});
} else {
actions.openChat({
id,
threadId: topMessageId,
},
onLoaded: () => {
global = getGlobal();
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,
noRequestThreadInfoUpdate: true,
isComments,
noForumTopicPanel,
shouldReplaceHistory,
shouldReplaceLast,
});
}
}
},
});
});
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> => {
const { tabId = getCurrentTabId() } = payload || {};
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);
if (chatId && commentId && messageId && usernameChat && type === 'thread') {
const threadInfo = selectThreadInfo(global, chatId, messageId);
if (threadInfo && threadInfo.chatId === chatId) {
actions.focusMessage({
chatId: threadInfo.chatId,
threadId: threadInfo.threadId,
messageId: commentId,
tabId,
});
return;
}
if (commentId && messageId && usernameChat) {
actions.openThread({
isComments: true,
originChannelId: usernameChat.id,
originMessageId: messageId,
tabId,
focusMessageId: commentId,
});
return;
}
if (!isWebApp) actions.openChat({ id: TMP_CHAT_ID, tabId });
@ -1249,8 +1430,6 @@ addActionHandler('openChatByUsername', async (global, actions, payload): Promise
if (!chatByUsername) return;
global = getGlobal();
if (isWebApp && chatByUsername) {
const theme = extractCurrentThemeParams();
@ -1266,29 +1445,12 @@ addActionHandler('openChatByUsername', async (global, actions, payload): Promise
if (!messageId) return;
const threadInfo = selectThreadInfo(global, chatByUsername.id, messageId);
let discussionChatId: string | undefined;
if (!threadInfo) {
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),
actions.openThread({
isComments: true,
originChannelId: chatByUsername.id,
originMessageId: messageId,
tabId,
focusMessageId: commentId,
});
});
@ -1971,8 +2133,8 @@ addActionHandler('createTopic', async (global, actions, payload): Promise<void>
chat, title, iconColor, iconEmojiId,
});
if (topicId) {
actions.openChat({
id: chatId, threadId: topicId, shouldReplaceHistory: true, tabId,
actions.openThread({
chatId, threadId: topicId, shouldReplaceHistory: true, tabId,
});
}
actions.closeCreateTopicPanel({ tabId });
@ -2673,7 +2835,7 @@ async function openChatByUsername<T extends GlobalState>(
chatId: chat.id, threadId, messageId: channelPostId, tabId,
});
} else if (!isCurrentChat) {
actions.openChat({ id: chat.id, threadId, tabId });
actions.openThread({ chatId: chat.id, threadId: threadId ?? MAIN_THREAD_ID, tabId });
}
if (startParam) {

View File

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

View File

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

View File

@ -1,13 +1,15 @@
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 { ActionReturnType, GlobalState, Thread } from '../../types';
import { MAIN_THREAD_ID } from '../../../api/types';
import { DEBUG, MESSAGE_LIST_SLICE, SERVICE_NOTIFICATIONS_USER_ID } from '../../../config';
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 {
addActionHandler, getActions, getGlobal, setGlobal,
@ -17,8 +19,8 @@ import {
safeReplaceViewportIds,
updateChats,
updateListedIds,
updateThread, updateThreadInfo,
updateThreadInfos,
updateThread,
updateThreadInfo,
updateUsers,
} from '../../reducers';
import { updateTabState } from '../../reducers/tabs';
@ -107,11 +109,11 @@ async function loadAndReplaceMessages<T extends GlobalState>(global: T, actions:
acc[chatId] = Object
.keys(global.messages.byChatId[chatId].threadsById)
.reduce<Record<number, Partial<Thread>>>((acc2, threadId) => {
acc2[Number(threadId)] = {
acc2[Number(threadId)] = omitUndefined({
draft: selectDraft(global, chatId, Number(threadId)),
editingId: selectEditingId(global, chatId, Number(threadId)),
editingDraft: selectEditingDraft(global, chatId, Number(threadId)),
};
});
return acc2;
}, {});
@ -123,11 +125,21 @@ async function loadAndReplaceMessages<T extends GlobalState>(global: T, actions:
global = getGlobal();
const { chatId: currentChatId, threadId: currentThreadId } = selectCurrentMessageList(global, tabId) || {};
const activeThreadId = currentThreadId || MAIN_THREAD_ID;
const threadInfo = currentThreadId && currentChatId
const threadInfo = currentChatId && currentThreadId
? selectThreadInfo(global, currentChatId, currentThreadId) : undefined;
const currentChat = currentChatId ? global.chats.byId[currentChatId] : undefined;
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();
const { chatId: newCurrentChatId } = selectCurrentMessageList(global, tabId) || {};
@ -142,10 +154,12 @@ async function loadAndReplaceMessages<T extends GlobalState>(global: T, actions:
.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 byId = buildCollectionByKey(allMessagesWithTopicLastMessages, 'id');
const listedIds = allMessages.map(({ id }) => id);
const listedIds = unique(allMessages.map(({ id }) => id));
if (!wasReset) {
global = {
@ -166,8 +180,16 @@ async function loadAndReplaceMessages<T extends GlobalState>(global: T, actions:
global = addChatMessagesById(global, currentChatId, byId);
global = updateListedIds(global, currentChatId, activeThreadId, listedIds);
if (threadInfo?.originChannelId) {
global = updateThreadInfo(global, currentChatId, activeThreadId, threadInfo);
if (resultDiscussion) {
// 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
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 = updateUsers(global, buildCollectionByKey(result.users, 'id'));
if (result.repliesThreadInfos.length) {
global = updateThreadInfos(global, result.repliesThreadInfos);
}
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', {
chat,
threadId,
offsetId: lastReadInboxId || chat.lastReadInboxMessageId,
offsetId: offsetId || chat.lastReadInboxMessageId,
addOffset: -(Math.round(MESSAGE_LIST_SLICE / 2) + 1),
limit: MESSAGE_LIST_SLICE,
});

View File

@ -1,5 +1,5 @@
import type {
ApiChat, ApiMessage, ApiPollResult, ApiReactions, ApiThreadInfo,
ApiChat, ApiMessage, ApiPollResult, ApiReactions,
} from '../../../api/types';
import type { RequiredGlobalActions } from '../../index';
import type {
@ -33,6 +33,7 @@ import {
updateMessageTranslations,
updateScheduledMessage,
updateThreadInfo,
updateThreadInfos,
updateThreadUnreadFromForwardedMessage,
updateTopic,
} from '../../reducers';
@ -75,15 +76,6 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
global = updateWithLocalMedia(global, chatId, id, message);
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 replyInfo = getMessageReplyInfo(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
// (which is itself delayed)
if (!isLocal) {
@ -204,14 +191,6 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
global = updateWithLocalMedia(global, chatId, id, message);
const newMessage = selectChatMessage(global, chatId, id)!;
if (message.repliesThreadInfo) {
global = updateThreadInfo(
global,
message.repliesThreadInfo.chatId,
message.repliesThreadInfo.threadId,
message.repliesThreadInfo,
);
}
if (currentMessage) {
global = updateChatLastMessage(global, chatId, newMessage);
@ -293,7 +272,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
actions.markMessageListRead({ maxId: message.id, tabId });
});
if (thread?.threadInfo) {
if (thread?.threadInfo?.threadId) {
global = replaceThreadParam(global, chatId, thread.threadInfo.threadId, 'threadInfo', {
...thread.threadInfo,
lastMessageId: message.id,
@ -364,43 +343,33 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
break;
}
case 'updateThreadInfo': {
case 'updateThreadInfos': {
const {
chatId, threadId, threadInfo, firstMessageId,
threadInfoUpdates,
} = update;
const currentThreadInfo = selectThreadInfo(global, chatId, threadId);
const newThreadInfo = {
...currentThreadInfo,
...threadInfo,
};
global = updateThreadInfos(global, threadInfoUpdates);
threadInfoUpdates.forEach((threadInfo) => {
const { chatId, threadId } = threadInfo;
if (!chatId || !threadId) return;
if (!newThreadInfo.threadId) {
return;
}
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),
});
const chat = selectChat(global, chatId);
const currentThreadInfo = selectThreadInfo(global, chatId, threadId);
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 (!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);
break;
@ -807,35 +776,37 @@ function updateListedAndViewportIds<T extends GlobalState>(
) {
const { id, chatId } = message;
const { threadInfo, firstMessageId } = selectThreadByMessage(global, message) || {};
const { threadInfo } = selectThreadByMessage(global, message) || {};
const chat = selectChat(global, chatId);
const isUnreadChatNotLoaded = chat?.unreadCount && !selectListedIds(global, chatId, MAIN_THREAD_ID);
global = updateThreadUnread(global, actions, message);
const { threadId } = threadInfo ?? {};
if (threadInfo) {
if (firstMessageId || !isMessageLocal(message)) {
global = updateListedIds(global, chatId, threadInfo.threadId, [id]);
if (threadInfo && threadId) {
global = updateListedIds(global, chatId, threadId, [id]);
Object.values(global.byTabId).forEach(({ id: tabId }) => {
if (selectIsViewportNewest(global, chatId, threadInfo.threadId, tabId)) {
global = addViewportId(global, chatId, threadInfo.threadId, id, tabId);
Object.values(global.byTabId).forEach(({ id: tabId }) => {
if (selectIsViewportNewest(global, chatId, threadId, 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) {
global = replaceThreadParam(global, chatId, threadInfo.threadId, 'firstMessageId', message.id);
}
if (!firstUnreadId || newViewportIds!.includes(firstUnreadId)) {
global = candidateGlobal;
}
});
}
}
});
global = replaceThreadParam(global, chatId, threadInfo.threadId, 'threadInfo', {
global = replaceThreadParam(global, chatId, threadId, 'threadInfo', {
...threadInfo,
lastMessageId: message.id,
});
if (!isMessageLocal(message) && !isActionMessage(message)) {
global = updateThreadInfo(global, chatId, threadInfo.threadId, {
global = updateThreadInfo(global, chatId, threadId, {
messagesCount: (threadInfo.messagesCount || 0) + 1,
});
}
@ -895,9 +866,9 @@ function updateChatLastMessage<T extends GlobalState>(
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 listedIds = selectListedIds(global, chatId, MAIN_THREAD_ID);
const listedIds = selectListedIds(global, chatId, threadId);
if (!byId || !listedIds) {
return undefined;
@ -906,7 +877,7 @@ function findLastMessage<T extends GlobalState>(global: T, chatId: string) {
let i = listedIds.length;
while (i--) {
const message = byId[listedIds[i]];
if (!message.isDeleting) {
if (message && !message.isDeleting) {
return message;
}
}
@ -923,6 +894,9 @@ export function deleteMessages<T extends GlobalState>(
const chat = selectChat(global, chatId);
if (!chat) return;
const threadIdsToUpdate = new Set<number>();
threadIdsToUpdate.add(MAIN_THREAD_ID);
ids.forEach((id) => {
global = updateChatMessage(global, chatId, id, {
isDeleting: true,
@ -930,21 +904,10 @@ export function deleteMessages<T extends GlobalState>(
global = clearMessageTranslation(global, chatId, id);
const newLastMessage = findLastMessage(global, chatId);
if (newLastMessage) {
global = updateChatLastMessage(global, chatId, newLastMessage, true);
}
if (chat.topics?.[id]) {
global = deleteTopic(global, chatId, id);
}
});
actions.requestChatUpdate({ chatId });
const threadIdsToUpdate: number[] = [];
ids.forEach((id) => {
const message = selectChatMessage(global, chatId, id);
if (!message) {
return;
@ -954,20 +917,36 @@ export function deleteMessages<T extends GlobalState>(
const threadId = selectThreadIdFromMessage(global, message);
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);
setTimeout(() => {
global = getGlobal();
global = deleteChatMessages(global, chatId, ids);
setGlobal(global);
unique(threadIdsToUpdate).forEach((threadId) => {
actions.requestThreadInfoUpdate({ chatId, threadId });
});
}, ANIMATION_DELAY);
return;

View File

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

View File

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

View File

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

View File

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

View File

@ -33,7 +33,6 @@ import {
selectChat,
selectChatMessages,
selectCurrentMessageList,
selectThreadOriginChat,
selectViewportIds,
selectVisibleUsers,
} from './selectors';
@ -335,17 +334,8 @@ function reduceChats<T extends GlobalState>(global: T): GlobalState['chats'] {
const { chats: { byId }, currentUserId } = global;
const currentChatIds = compact(
Object.values(global.byTabId)
.flatMap(({ id: tabId }): MessageList[] | undefined => {
const messageList = 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(({ id: tabId }): MessageList | undefined => {
return selectCurrentMessageList(global, tabId);
}),
).map(({ chatId }) => chatId);
@ -407,14 +397,17 @@ function reduceMessages<T extends GlobalState>(global: T): GlobalState['messages
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) || {};
if (!tabChatId || tabChatId !== chatId || !threadId || threadId === MAIN_THREAD_ID) {
return undefined;
}
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 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 (chat.isForum) {
if (chat.isNotJoined) {
@ -157,7 +157,7 @@ export function getCanPostInChat(chat: ApiChat, threadId: number, isComments?: b
}
if (chat.isRestricted || chat.isForbidden || chat.migratedTo
|| (!isComments && chat.isNotJoined) || isChatWithRepliesBot(chat.id)) {
|| (!isMessageThread && chat.isNotJoined) || isChatWithRepliesBot(chat.id)) {
return false;
}

View File

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

View File

@ -248,34 +248,6 @@ export function selectThreadMessagesCount(global: GlobalState, chatId: string, t
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) {
const threadId = selectThreadIdFromMessage(global, message);
if (!threadId || threadId === MAIN_THREAD_ID) {
@ -325,10 +297,12 @@ export function selectIsViewportNewest<T extends GlobalState>(
} else {
const threadInfo = selectThreadInfo(global, chatId, threadId);
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
@ -580,9 +554,9 @@ export function selectAllowedMessageActions<T extends GlobalState>(global: T, me
);
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
&& getCanPostInChat(chat, threadId, isComments)
&& getCanPostInChat(chat, threadId, isMessageThread)
&& (!messageTopic || !messageTopic.isClosed || messageTopic.isOwner || getHasAdminRight(chat, 'manageTopics'));
const hasPinPermission = isPrivate || (
@ -799,7 +773,7 @@ export function selectRealLastReadId<T extends GlobalState>(global: T, chatId: s
}
if (!threadInfo.lastReadInboxMessageId) {
return threadInfo.topMessageId;
return threadInfo.threadId;
}
// Some previously read messages may be deleted
@ -977,9 +951,15 @@ export function selectNewestMessageWithBotKeyboardButtons<T extends GlobalState>
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) {
return undefined;
@ -1391,20 +1371,18 @@ export function selectTopicLink<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);
if (!chat) return undefined;
const replyingToTopId = selectThreadTopMessageId(global, chatId, threadId);
if (!additionalReplyInfo && !replyingToTopId) return undefined;
const isMainThread = threadId === MAIN_THREAD_ID;
if (!additionalReplyInfo && isMainThread) return undefined;
const replyInfo: ApiInputMessageReplyInfo = {
type: 'message',
...additionalReplyInfo,
replyToMsgId: additionalReplyInfo?.replyToMsgId || replyingToTopId!,
replyToTopId: additionalReplyInfo?.replyToTopId || replyingToTopId,
replyToMsgId: additionalReplyInfo?.replyToMsgId || threadId,
replyToTopId: additionalReplyInfo?.replyToTopId || (!isMainThread ? threadId : undefined),
};
return replyInfo;

View File

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