Fix various problems for comments and threads (#3809)
This commit is contained in:
parent
4571745654
commit
dcba75a11a
@ -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) }),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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({
|
||||
|
||||
@ -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 = {
|
||||
|
||||
@ -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 |
|
||||
|
||||
@ -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(),
|
||||
|
||||
@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -208,7 +208,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
||||
});
|
||||
|
||||
const handleAsMessagesClick = useLastCallback(() => {
|
||||
openChat({ id: chatId, threadId: MAIN_THREAD_ID });
|
||||
openChat({ id: chatId });
|
||||
});
|
||||
|
||||
function handleRequestCall() {
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
@ -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 }),
|
||||
};
|
||||
},
|
||||
|
||||
@ -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>
|
||||
),
|
||||
]);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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 = [];
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -714,6 +714,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.message-action-button-shown {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&.own .message-action-button {
|
||||
left: -3rem;
|
||||
}
|
||||
|
||||
@ -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 }),
|
||||
|
||||
@ -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,
|
||||
});
|
||||
});
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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!,
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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, {
|
||||
|
||||
@ -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,
|
||||
});
|
||||
|
||||
@ -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: {
|
||||
|
||||
@ -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,
|
||||
},
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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[][] = [];
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user