Forum: Support mentions and reactions (#6489)
This commit is contained in:
parent
00f7da84a3
commit
b68ae94f3a
@ -304,7 +304,7 @@ Global State is our single, app-wide store, similar to Redux or Zustand. All its
|
||||
### 1. Accessing Global in Components
|
||||
|
||||
* **Use** `withGlobal` (a `mapStateToProps` helper) to pull in state.
|
||||
* **Avoid** the experimental `useSelector` API.
|
||||
* There is an experimental `useSelector` API available.
|
||||
* **Use** `getGlobal` **only** inside hooks for one-off reads (it's non-reactive).
|
||||
|
||||
### 2. Performance
|
||||
|
||||
@ -1,28 +1,31 @@
|
||||
import { Api as GramJs } from '../../../lib/gramjs';
|
||||
import type { Entity } from '../../../lib/gramjs/types';
|
||||
|
||||
import type {
|
||||
ApiBotCommand,
|
||||
ApiChat,
|
||||
ApiChatAdminRights,
|
||||
ApiChatBannedRights,
|
||||
ApiChatFolder,
|
||||
ApiChatInviteImporter,
|
||||
ApiChatInviteInfo,
|
||||
ApiChatlistExportedInvite,
|
||||
ApiChatlistInvite,
|
||||
ApiChatMember,
|
||||
ApiChatReactions,
|
||||
ApiExportedInvite,
|
||||
ApiMissingInvitedUser,
|
||||
ApiRestrictionReason,
|
||||
ApiSendAsPeerId,
|
||||
ApiSponsoredMessageReportResult,
|
||||
ApiSponsoredPeer,
|
||||
ApiStarsSubscriptionPricing,
|
||||
import type { ThreadReadState } from '../../../types';
|
||||
import {
|
||||
type ApiBotCommand,
|
||||
type ApiChat,
|
||||
type ApiChatAdminRights,
|
||||
type ApiChatBannedRights,
|
||||
type ApiChatFolder,
|
||||
type ApiChatInviteImporter,
|
||||
type ApiChatInviteInfo,
|
||||
type ApiChatlistExportedInvite,
|
||||
type ApiChatlistInvite,
|
||||
type ApiChatMember,
|
||||
type ApiChatReactions,
|
||||
type ApiExportedInvite,
|
||||
type ApiMissingInvitedUser,
|
||||
type ApiRestrictionReason,
|
||||
type ApiSendAsPeerId,
|
||||
type ApiSponsoredMessageReportResult,
|
||||
type ApiSponsoredPeer,
|
||||
type ApiStarsSubscriptionPricing,
|
||||
type ApiThreadInfo,
|
||||
MAIN_THREAD_ID,
|
||||
} from '../../types';
|
||||
|
||||
import { pickTruthy } from '../../../util/iteratees';
|
||||
import { omitUndefined, pickTruthy } from '../../../util/iteratees';
|
||||
import { toJSNumber } from '../../../util/numbers';
|
||||
import { getServerTimeOffset } from '../../../util/serverTime';
|
||||
import { addPhotoToLocalDb, addUserToLocalDb } from '../helpers/localDb';
|
||||
@ -144,23 +147,15 @@ export function buildApiChatFromDialog(
|
||||
peerEntity: GramJs.TypeUser | GramJs.TypeChat,
|
||||
): ApiChat {
|
||||
const {
|
||||
peer, folderId, unreadMark, unreadCount, unreadMentionsCount, unreadReactionsCount,
|
||||
readOutboxMaxId, readInboxMaxId, draft, viewForumAsMessages,
|
||||
peer, folderId, viewForumAsMessages,
|
||||
} = dialog;
|
||||
|
||||
return {
|
||||
id: getApiChatIdFromMtpPeer(peer),
|
||||
...(folderId && { folderId }),
|
||||
folderId,
|
||||
type: getApiChatTypeFromPeerEntity(peerEntity),
|
||||
title: getApiChatTitleFromMtpPeer(peer, peerEntity),
|
||||
lastReadOutboxMessageId: readOutboxMaxId,
|
||||
lastReadInboxMessageId: readInboxMaxId,
|
||||
unreadCount,
|
||||
unreadMentionsCount,
|
||||
unreadReactionsCount,
|
||||
...(unreadMark && { hasUnreadMark: true }),
|
||||
...(draft instanceof GramJs.DraftMessage && { draftDate: draft.date }),
|
||||
...(viewForumAsMessages && { isForumAsMessages: true }),
|
||||
isForumAsMessages: viewForumAsMessages,
|
||||
...buildApiChatFieldsFromPeerEntity(peerEntity),
|
||||
};
|
||||
}
|
||||
@ -705,3 +700,38 @@ export function buildApiSponsoredPeer(sponsoredPeer: GramJs.SponsoredPeer): ApiS
|
||||
sponsorInfo,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildThreadReadState(
|
||||
input: GramJs.Dialog | GramJs.MonoForumDialog | GramJs.ForumTopic | GramJs.messages.DiscussionMessage,
|
||||
): ThreadReadState {
|
||||
const { unreadCount, readInboxMaxId, readOutboxMaxId } = input;
|
||||
const dialog = input instanceof GramJs.Dialog ? input : undefined;
|
||||
const monoForumDialog = input instanceof GramJs.MonoForumDialog ? input : undefined;
|
||||
const forumTopic = input instanceof GramJs.ForumTopic ? input : undefined;
|
||||
|
||||
const { unreadReactionsCount } = dialog || monoForumDialog || forumTopic || {};
|
||||
const { unreadMentionsCount } = dialog || forumTopic || {};
|
||||
const { unreadMark } = dialog || monoForumDialog || {};
|
||||
|
||||
return omitUndefined<ThreadReadState>({
|
||||
unreadCount,
|
||||
lastReadInboxMessageId: readInboxMaxId,
|
||||
lastReadOutboxMessageId: readOutboxMaxId,
|
||||
unreadReactionsCount,
|
||||
unreadMentionsCount,
|
||||
hasUnreadMark: unreadMark,
|
||||
});
|
||||
}
|
||||
|
||||
export function buildApiThreadInfoFromDialog(
|
||||
chatId: string, dialog: GramJs.Dialog | GramJs.SavedDialog,
|
||||
): ApiThreadInfo {
|
||||
const isSavedDialog = dialog instanceof GramJs.SavedDialog;
|
||||
const { topMessage } = dialog;
|
||||
return {
|
||||
isCommentsInfo: false,
|
||||
chatId,
|
||||
threadId: isSavedDialog ? getApiChatIdFromMtpPeer(dialog.peer) : MAIN_THREAD_ID,
|
||||
lastMessageId: topMessage,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import { Api as GramJs } from '../../../lib/gramjs';
|
||||
|
||||
import type { ApiTopic } from '../../types';
|
||||
import type { ApiTopic, ApiTopicWithState } from '../../types';
|
||||
|
||||
import { buildThreadReadState } from './chats';
|
||||
import { buildApiPeerNotifySettings, getApiChatIdFromMtpPeer } from './peers';
|
||||
|
||||
export function buildApiTopic(forumTopic: GramJs.TypeForumTopic): ApiTopic | undefined {
|
||||
function buildApiTopic(forumTopic: GramJs.TypeForumTopic): ApiTopic | undefined {
|
||||
if (forumTopic instanceof GramJs.ForumTopicDeleted) {
|
||||
return undefined;
|
||||
}
|
||||
@ -20,10 +21,6 @@ export function buildApiTopic(forumTopic: GramJs.TypeForumTopic): ApiTopic | und
|
||||
title,
|
||||
iconColor,
|
||||
iconEmojiId,
|
||||
topMessage,
|
||||
unreadCount,
|
||||
unreadMentionsCount,
|
||||
unreadReactionsCount,
|
||||
fromId,
|
||||
notifySettings,
|
||||
titleMissing,
|
||||
@ -40,12 +37,24 @@ export function buildApiTopic(forumTopic: GramJs.TypeForumTopic): ApiTopic | und
|
||||
title,
|
||||
iconColor,
|
||||
iconEmojiId: iconEmojiId?.toString(),
|
||||
lastMessageId: topMessage,
|
||||
unreadCount,
|
||||
unreadMentionsCount,
|
||||
unreadReactionsCount,
|
||||
fromId: getApiChatIdFromMtpPeer(fromId),
|
||||
notifySettings: buildApiPeerNotifySettings(notifySettings),
|
||||
isTitleMissing: titleMissing,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildApiTopicWithState(forumTopic: GramJs.TypeForumTopic): ApiTopicWithState | undefined {
|
||||
if (forumTopic instanceof GramJs.ForumTopicDeleted) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const topic = buildApiTopic(forumTopic);
|
||||
if (!topic) return undefined;
|
||||
const isMin = topic.isMin;
|
||||
|
||||
return {
|
||||
topic,
|
||||
readState: !isMin ? buildThreadReadState(forumTopic) : undefined,
|
||||
lastMessageId: !isMin ? forumTopic.topMessage : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
@ -1,24 +1,26 @@
|
||||
import { Api as GramJs } from '../../../lib/gramjs';
|
||||
import { RPCError } from '../../../lib/gramjs/errors';
|
||||
|
||||
import type { ChatListType } from '../../../types';
|
||||
import type {
|
||||
ApiChat,
|
||||
ApiChatAdminRights,
|
||||
ApiChatBannedRights,
|
||||
ApiChatFolder,
|
||||
ApiChatFullInfo,
|
||||
ApiChatReactions,
|
||||
ApiDraft,
|
||||
ApiGroupCall,
|
||||
ApiMessage,
|
||||
ApiMissingInvitedUser,
|
||||
ApiPeer,
|
||||
ApiPeerNotifySettings,
|
||||
ApiPhoto,
|
||||
ApiProfileTab,
|
||||
ApiUser,
|
||||
ApiUserStatus,
|
||||
import type { ChatListType, ThreadReadState } from '../../../types';
|
||||
import {
|
||||
type ApiChat,
|
||||
type ApiChatAdminRights,
|
||||
type ApiChatBannedRights,
|
||||
type ApiChatFolder,
|
||||
type ApiChatFullInfo,
|
||||
type ApiChatReactions,
|
||||
type ApiDraft,
|
||||
type ApiGroupCall,
|
||||
type ApiMessage,
|
||||
type ApiMissingInvitedUser,
|
||||
type ApiPeer,
|
||||
type ApiPeerNotifySettings,
|
||||
type ApiPhoto,
|
||||
type ApiProfileTab,
|
||||
type ApiThreadInfo,
|
||||
type ApiUser,
|
||||
type ApiUserStatus,
|
||||
MAIN_THREAD_ID,
|
||||
} from '../../types';
|
||||
|
||||
import {
|
||||
@ -43,8 +45,10 @@ import {
|
||||
buildApiChatReactions,
|
||||
buildApiMissingInvitedUser,
|
||||
buildApiSponsoredPeer,
|
||||
buildApiThreadInfoFromDialog,
|
||||
buildChatMember,
|
||||
buildChatMembers,
|
||||
buildThreadReadState,
|
||||
getPeerKey,
|
||||
} from '../apiBuilders/chats';
|
||||
import { buildApiPhoto } from '../apiBuilders/common';
|
||||
@ -101,6 +105,8 @@ type ChatListData = {
|
||||
users: ApiUser[];
|
||||
userStatusesById: Record<string, ApiUserStatus>;
|
||||
draftsById: Record<string, ApiDraft>;
|
||||
threadReadStatesById?: Record<string, ThreadReadState>;
|
||||
threadInfos: ApiThreadInfo[];
|
||||
orderedPinnedIds: string[] | undefined;
|
||||
totalChatCount: number;
|
||||
messages: ApiMessage[];
|
||||
@ -161,6 +167,8 @@ export async function fetchChats({
|
||||
const chats: ApiChat[] = [];
|
||||
const draftsById: Record<string, ApiDraft> = {};
|
||||
const notifyExceptionById: Record<string, ApiPeerNotifySettings> = {};
|
||||
const threadReadStatesById: Record<string, ThreadReadState> = {};
|
||||
const threadInfos: ApiThreadInfo[] = [];
|
||||
|
||||
const dialogs = (resultPinned?.dialogs || []).concat(result.dialogs);
|
||||
|
||||
@ -216,6 +224,12 @@ export async function fetchChats({
|
||||
draftsById[chat.id] = draft;
|
||||
}
|
||||
}
|
||||
|
||||
const readState = buildThreadReadState(dialog);
|
||||
threadReadStatesById[chat.id] = readState;
|
||||
|
||||
const threadInfo = buildApiThreadInfoFromDialog(chat.id, dialog);
|
||||
threadInfos.push(threadInfo);
|
||||
});
|
||||
|
||||
const chatIds = chats.map((chat) => chat.id);
|
||||
@ -251,26 +265,33 @@ export async function fetchChats({
|
||||
nextOffsetId,
|
||||
nextOffsetPeerId,
|
||||
nextOffsetDate,
|
||||
threadReadStatesById,
|
||||
threadInfos,
|
||||
};
|
||||
}
|
||||
|
||||
export async function fetchSavedChats({
|
||||
parentPeer,
|
||||
limit,
|
||||
offsetDate,
|
||||
offsetPeer,
|
||||
offsetId,
|
||||
withPinned,
|
||||
}: {
|
||||
parentPeer: ApiPeer;
|
||||
limit: number;
|
||||
offsetDate?: number;
|
||||
offsetPeer?: ApiPeer;
|
||||
offsetId?: number;
|
||||
withPinned?: boolean;
|
||||
}): Promise<ChatListData | undefined> {
|
||||
const isMonoforum = 'title' in parentPeer;
|
||||
const inputParentPeer = isMonoforum ? buildInputPeer(parentPeer.id, parentPeer.accessHash) : undefined;
|
||||
const peer = (offsetPeer && buildInputPeer(offsetPeer.id, offsetPeer.accessHash)) || new GramJs.InputPeerEmpty();
|
||||
const result = await invokeRequest(new GramJs.messages.GetSavedDialogs({
|
||||
offsetPeer: peer,
|
||||
offsetId: offsetId ?? DEFAULT_PRIMITIVES.INT,
|
||||
parentPeer: inputParentPeer,
|
||||
limit,
|
||||
offsetDate: offsetDate ?? DEFAULT_PRIMITIVES.INT,
|
||||
hash: DEFAULT_PRIMITIVES.BIGINT,
|
||||
@ -301,6 +322,7 @@ export async function fetchSavedChats({
|
||||
const chatIds: string[] = [];
|
||||
const orderedPinnedIds: string[] = [];
|
||||
const lastMessageByChatId: Record<string, number> = {};
|
||||
const threadInfos: ApiThreadInfo[] = [];
|
||||
|
||||
const chats: ApiChat[] = [];
|
||||
|
||||
@ -321,6 +343,9 @@ export async function fetchSavedChats({
|
||||
lastMessageByChatId[chatId] = dialog.topMessage;
|
||||
|
||||
chats.push(chat);
|
||||
|
||||
const threadInfo = buildApiThreadInfoFromDialog(parentPeer.id, dialog);
|
||||
threadInfos.push(threadInfo);
|
||||
});
|
||||
|
||||
const users = result.users.map(buildApiUser).filter(Boolean);
|
||||
@ -355,6 +380,7 @@ export async function fetchSavedChats({
|
||||
nextOffsetId,
|
||||
nextOffsetPeerId,
|
||||
nextOffsetDate,
|
||||
threadInfos,
|
||||
};
|
||||
}
|
||||
|
||||
@ -496,6 +522,14 @@ export async function requestChatUpdate({
|
||||
|
||||
const chatUpdate = buildApiChatFromDialog(dialog, peerEntity);
|
||||
|
||||
const readState = buildThreadReadState(dialog);
|
||||
sendApiUpdate({
|
||||
'@type': 'updateThreadReadState',
|
||||
chatId: id,
|
||||
threadId: MAIN_THREAD_ID,
|
||||
readState,
|
||||
});
|
||||
|
||||
sendApiUpdate({
|
||||
'@type': 'updateChat',
|
||||
id,
|
||||
@ -1195,7 +1229,7 @@ export function toggleDialogFilterTags(isEnabled: boolean) {
|
||||
export async function toggleDialogUnread({
|
||||
chat, hasUnreadMark,
|
||||
}: {
|
||||
chat: ApiChat; hasUnreadMark: boolean | undefined;
|
||||
chat: ApiChat; hasUnreadMark?: true;
|
||||
}) {
|
||||
const { id, accessHash } = chat;
|
||||
|
||||
@ -1203,14 +1237,17 @@ export async function toggleDialogUnread({
|
||||
peer: new GramJs.InputDialogPeer({
|
||||
peer: buildInputPeer(id, accessHash),
|
||||
}),
|
||||
unread: hasUnreadMark || undefined,
|
||||
unread: hasUnreadMark,
|
||||
}));
|
||||
|
||||
if (isActionSuccessful) {
|
||||
sendApiUpdate({
|
||||
'@type': 'updateChat',
|
||||
id: chat.id,
|
||||
chat: { hasUnreadMark },
|
||||
'@type': 'updateThreadReadState',
|
||||
chatId: chat.id,
|
||||
threadId: MAIN_THREAD_ID,
|
||||
readState: {
|
||||
hasUnreadMark,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Api as GramJs } from '../../../lib/gramjs';
|
||||
import { generateRandomBigInt } from '../../../lib/gramjs/Helpers';
|
||||
|
||||
import type { ApiMessage, ApiTopic } from '../../types';
|
||||
import type { ApiMessage, ApiTopicWithState } from '../../types';
|
||||
import type {
|
||||
ApiChat,
|
||||
ApiDraft,
|
||||
@ -9,7 +9,7 @@ import type {
|
||||
} from '../../types/chats';
|
||||
|
||||
import { GENERAL_TOPIC_ID, TOPICS_SLICE } from '../../../config';
|
||||
import { buildApiTopic } from '../apiBuilders/forums';
|
||||
import { buildApiTopicWithState } from '../apiBuilders/forums';
|
||||
import { buildApiMessage, buildMessageDraft } from '../apiBuilders/messages';
|
||||
import { buildInputPeer, DEFAULT_PRIMITIVES } from '../gramjsBuilders';
|
||||
import { processAffectedHistory } from '../updates/updateManager';
|
||||
@ -57,12 +57,11 @@ export async function fetchTopics({
|
||||
offsetDate?: number;
|
||||
limit?: number;
|
||||
}): Promise<{
|
||||
topics: ApiTopic[];
|
||||
topics: ApiTopicWithState[];
|
||||
messages: ApiMessage[];
|
||||
count: number;
|
||||
shouldOrderByCreateDate?: boolean;
|
||||
draftsById: Record<number, ApiDraft | undefined>;
|
||||
readInboxMessageIdByTopicId: Record<number, number>;
|
||||
} | undefined> {
|
||||
const { id, accessHash } = chat;
|
||||
|
||||
@ -79,7 +78,7 @@ export async function fetchTopics({
|
||||
|
||||
const { orderByCreateDate } = result;
|
||||
|
||||
const topics = result.topics.map(buildApiTopic).filter(Boolean);
|
||||
const topics = result.topics.map(buildApiTopicWithState).filter(Boolean);
|
||||
const count = result.count === 0 ? topics.length : result.count; // Sometimes count is 0 in result, but we have topics
|
||||
const messages = result.messages.map(buildApiMessage).filter(Boolean);
|
||||
const draftsById = result.topics.reduce((acc, topic) => {
|
||||
@ -88,12 +87,6 @@ export async function fetchTopics({
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<number, ReturnType<typeof buildMessageDraft>>);
|
||||
const readInboxMessageIdByTopicId = result.topics.reduce((acc, topic) => {
|
||||
if (topic instanceof GramJs.ForumTopic && topic.readInboxMaxId) {
|
||||
acc[topic.id] = topic.readInboxMaxId;
|
||||
}
|
||||
return acc;
|
||||
}, {} as Record<number, number>);
|
||||
|
||||
return {
|
||||
topics,
|
||||
@ -102,7 +95,6 @@ export async function fetchTopics({
|
||||
count: count + 1,
|
||||
shouldOrderByCreateDate: orderByCreateDate,
|
||||
draftsById,
|
||||
readInboxMessageIdByTopicId,
|
||||
};
|
||||
}
|
||||
|
||||
@ -112,7 +104,7 @@ export async function fetchTopicById({
|
||||
chat: ApiChat;
|
||||
topicId: number;
|
||||
}): Promise<{
|
||||
topic: ApiTopic;
|
||||
topic: ApiTopicWithState;
|
||||
messages: ApiMessage[];
|
||||
} | undefined> {
|
||||
const { id, accessHash } = chat;
|
||||
@ -129,7 +121,7 @@ export async function fetchTopicById({
|
||||
const messages = result.messages.map(buildApiMessage).filter(Boolean);
|
||||
|
||||
return {
|
||||
topic: buildApiTopic(result.topics[0])!,
|
||||
topic: buildApiTopicWithState(result.topics[0])!,
|
||||
messages,
|
||||
};
|
||||
}
|
||||
|
||||
@ -27,6 +27,7 @@ import type {
|
||||
ApiSearchPostsFlood,
|
||||
ApiSendMessageAction,
|
||||
ApiTodoItem,
|
||||
ApiTopicWithState,
|
||||
ApiUser,
|
||||
ApiUserStatus,
|
||||
ApiWebPage,
|
||||
@ -57,8 +58,10 @@ import {
|
||||
buildApiChatFromPreview,
|
||||
buildApiSendAsPeerId,
|
||||
buildApiSponsoredMessageReportResult,
|
||||
buildThreadReadState,
|
||||
} from '../apiBuilders/chats';
|
||||
import { buildApiFormattedText } from '../apiBuilders/common';
|
||||
import { buildApiTopicWithState } from '../apiBuilders/forums';
|
||||
import {
|
||||
buildMessageMediaContent, buildMessageTextContent, buildPollFromMedia,
|
||||
buildWebPageFromMedia,
|
||||
@ -126,6 +129,7 @@ type TranslateTextParams = ({
|
||||
|
||||
type SearchResults = {
|
||||
messages: ApiMessage[];
|
||||
topics: ApiTopicWithState[];
|
||||
userStatusesById: Record<number, ApiUserStatus>;
|
||||
totalCount: number;
|
||||
nextOffsetRate?: number;
|
||||
@ -196,13 +200,15 @@ 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 count = !(result instanceof GramJs.messages.Messages) && result.count;
|
||||
const count = !(result instanceof GramJs.messages.Messages) ? result.count : undefined;
|
||||
const topics = result.topics.map(buildApiTopicWithState).filter(Boolean);
|
||||
|
||||
return {
|
||||
messages,
|
||||
users,
|
||||
chats,
|
||||
count,
|
||||
topics,
|
||||
};
|
||||
}
|
||||
|
||||
@ -371,6 +377,7 @@ export function sendApiMessage(
|
||||
message: {
|
||||
sendingState: 'messageSendingStatePending',
|
||||
},
|
||||
isFull: false,
|
||||
});
|
||||
}, FAST_SEND_TIMEOUT);
|
||||
|
||||
@ -740,7 +747,7 @@ export async function editMessage({
|
||||
}),
|
||||
};
|
||||
|
||||
const messageUpdate: Partial<ApiMessage> = {
|
||||
const messageUpdate: ApiMessage = {
|
||||
...message,
|
||||
content: newContent,
|
||||
isInvertedMedia,
|
||||
@ -751,6 +758,7 @@ export async function editMessage({
|
||||
id: message.id,
|
||||
chatId: chat.id,
|
||||
message: messageUpdate,
|
||||
isFull: true,
|
||||
});
|
||||
|
||||
try {
|
||||
@ -794,6 +802,7 @@ export async function editMessage({
|
||||
id: message.id,
|
||||
chatId: chat.id,
|
||||
message,
|
||||
isFull: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -818,7 +827,7 @@ export async function editTodo({
|
||||
},
|
||||
};
|
||||
|
||||
const messageUpdate: Partial<ApiMessage> = {
|
||||
const messageUpdate: ApiMessage = {
|
||||
...message,
|
||||
content: newContent,
|
||||
};
|
||||
@ -828,6 +837,7 @@ export async function editTodo({
|
||||
id: message.id,
|
||||
chatId: chat.id,
|
||||
message: messageUpdate,
|
||||
isFull: true,
|
||||
});
|
||||
|
||||
try {
|
||||
@ -858,6 +868,7 @@ export async function editTodo({
|
||||
id: message.id,
|
||||
chatId: chat.id,
|
||||
message,
|
||||
isFull: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -1447,17 +1458,14 @@ export async function fetchDiscussionMessage({
|
||||
|
||||
if (!threadId) return undefined;
|
||||
|
||||
const {
|
||||
unreadCount, maxId, readInboxMaxId, readOutboxMaxId,
|
||||
} = result;
|
||||
const { maxId } = result;
|
||||
const threadReadState = buildThreadReadState(result);
|
||||
|
||||
return {
|
||||
messages,
|
||||
topMessages,
|
||||
unreadCount,
|
||||
threadId,
|
||||
lastReadInboxMessageId: readInboxMaxId,
|
||||
lastReadOutboxMessageId: readOutboxMaxId,
|
||||
threadReadState,
|
||||
lastMessageId: maxId,
|
||||
chatId: topMessages[0]?.chatId,
|
||||
firstMessageId: replies.messages[0]?.id,
|
||||
@ -1554,6 +1562,7 @@ export async function searchMessagesInChat({
|
||||
|
||||
const userStatusesById = buildApiUserStatuses(result.users);
|
||||
const messages = result.messages.map(buildApiMessage).filter(Boolean);
|
||||
const topics = result.topics.map(buildApiTopicWithState).filter(Boolean);
|
||||
|
||||
let totalCount = messages.length;
|
||||
let nextOffsetId: number | undefined;
|
||||
@ -1568,6 +1577,7 @@ export async function searchMessagesInChat({
|
||||
return {
|
||||
userStatusesById,
|
||||
messages,
|
||||
topics,
|
||||
totalCount,
|
||||
nextOffsetId,
|
||||
};
|
||||
@ -1649,6 +1659,7 @@ export async function searchMessagesGlobal({
|
||||
|
||||
const userStatusesById = buildApiUserStatuses(result.users);
|
||||
const messages = result.messages.map(buildApiMessage).filter(Boolean);
|
||||
const topics = result.topics.map(buildApiTopicWithState).filter(Boolean);
|
||||
|
||||
let totalCount = messages.length;
|
||||
if (result instanceof GramJs.messages.MessagesSlice || result instanceof GramJs.messages.ChannelMessages) {
|
||||
@ -1664,6 +1675,7 @@ export async function searchMessagesGlobal({
|
||||
|
||||
return {
|
||||
messages,
|
||||
topics,
|
||||
userStatusesById,
|
||||
totalCount,
|
||||
nextOffsetRate,
|
||||
@ -1706,6 +1718,7 @@ export async function searchPublicPosts({
|
||||
|
||||
const userStatusesById = buildApiUserStatuses(result.users);
|
||||
const messages = result.messages.map(buildApiMessage).filter(Boolean);
|
||||
const topics = result.topics.map(buildApiTopicWithState).filter(Boolean);
|
||||
|
||||
let totalCount = messages.length;
|
||||
if (result instanceof GramJs.messages.MessagesSlice || result instanceof GramJs.messages.ChannelMessages) {
|
||||
@ -1725,6 +1738,7 @@ export async function searchPublicPosts({
|
||||
|
||||
return {
|
||||
messages,
|
||||
topics,
|
||||
userStatusesById,
|
||||
totalCount,
|
||||
nextOffsetRate,
|
||||
@ -2248,10 +2262,14 @@ export async function fetchUnreadMentions({
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const totalCount = 'count' in result ? result.count : result.messages.length;
|
||||
const messages = result.messages.map(buildApiMessage).filter(Boolean);
|
||||
const topics = result.topics.map(buildApiTopicWithState).filter(Boolean);
|
||||
|
||||
return {
|
||||
totalCount,
|
||||
messages,
|
||||
topics,
|
||||
};
|
||||
}
|
||||
|
||||
@ -2283,10 +2301,14 @@ export async function fetchUnreadReactions({
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const totalCount = 'count' in result ? result.count : result.messages.length;
|
||||
const messages = result.messages.map(buildApiMessage).filter(Boolean);
|
||||
const topics = result.topics.map(buildApiTopicWithState).filter(Boolean);
|
||||
|
||||
return {
|
||||
totalCount,
|
||||
messages,
|
||||
topics,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -2,10 +2,11 @@ import { Api as GramJs, type Update } from '../../../lib/gramjs';
|
||||
import { UpdateConnectionState, UpdateServerTimeOffset } from '../../../lib/gramjs/network';
|
||||
|
||||
import type { GroupCallConnectionData } from '../../../lib/secret-sauce';
|
||||
import type {
|
||||
ApiMessage, ApiPoll, ApiStory, ApiStorySkipped,
|
||||
ApiUpdateConnectionStateType,
|
||||
ApiWebPage,
|
||||
import {
|
||||
type ApiMessage, type ApiPoll, type ApiStory, type ApiStorySkipped,
|
||||
type ApiUpdateConnectionStateType,
|
||||
type ApiWebPage,
|
||||
MAIN_THREAD_ID,
|
||||
} from '../../types';
|
||||
|
||||
import { DEBUG, GENERAL_TOPIC_ID } from '../../../config';
|
||||
@ -176,6 +177,7 @@ export function updater(update: Update) {
|
||||
poll,
|
||||
webPage,
|
||||
isFromNew: true,
|
||||
isFull: true,
|
||||
});
|
||||
} else {
|
||||
sendApiUpdate({
|
||||
@ -187,6 +189,7 @@ export function updater(update: Update) {
|
||||
poll,
|
||||
webPage,
|
||||
isFromNew: true,
|
||||
isFull: true,
|
||||
});
|
||||
}
|
||||
|
||||
@ -352,7 +355,7 @@ export function updater(update: Update) {
|
||||
processMessageAndUpdateThreadInfo(mtpMessage);
|
||||
|
||||
// Workaround for a weird server behavior when own message is marked as incoming
|
||||
const message = omit(buildApiMessage(mtpMessage)!, ['isOutgoing']);
|
||||
const message = omit(buildApiMessage(mtpMessage)!, ['isOutgoing']) as ApiMessage;
|
||||
|
||||
const poll = mtpMessage instanceof GramJs.Message && mtpMessage.media
|
||||
? buildPollFromMedia(mtpMessage.media) : undefined;
|
||||
@ -367,11 +370,13 @@ export function updater(update: Update) {
|
||||
message,
|
||||
poll,
|
||||
webPage,
|
||||
isFull: true,
|
||||
});
|
||||
} else if (update instanceof GramJs.UpdateMessageReactions) {
|
||||
sendApiUpdate({
|
||||
'@type': 'updateMessageReactions',
|
||||
id: update.msgId,
|
||||
threadId: update.topMsgId,
|
||||
chatId: getApiChatIdFromMtpPeer(update.peer),
|
||||
reactions: buildMessageReactions(update.reactions),
|
||||
});
|
||||
@ -508,51 +513,57 @@ export function updater(update: Update) {
|
||||
// Chats
|
||||
} else if (update instanceof GramJs.UpdateReadHistoryInbox) {
|
||||
sendApiUpdate({
|
||||
'@type': 'updateChatInbox',
|
||||
id: getApiChatIdFromMtpPeer(update.peer),
|
||||
lastReadInboxMessageId: update.maxId,
|
||||
unreadCount: update.stillUnreadCount,
|
||||
threadId: update.topMsgId,
|
||||
'@type': 'updateThreadReadState',
|
||||
chatId: getApiChatIdFromMtpPeer(update.peer),
|
||||
threadId: update.topMsgId || MAIN_THREAD_ID,
|
||||
readState: {
|
||||
lastReadInboxMessageId: update.maxId,
|
||||
unreadCount: update.stillUnreadCount,
|
||||
},
|
||||
});
|
||||
} else if (update instanceof GramJs.UpdateReadHistoryOutbox) {
|
||||
sendApiUpdate({
|
||||
'@type': 'updateChat',
|
||||
id: getApiChatIdFromMtpPeer(update.peer),
|
||||
chat: {
|
||||
'@type': 'updateThreadReadState',
|
||||
chatId: getApiChatIdFromMtpPeer(update.peer),
|
||||
threadId: MAIN_THREAD_ID,
|
||||
readState: {
|
||||
lastReadOutboxMessageId: update.maxId,
|
||||
},
|
||||
});
|
||||
} else if (update instanceof GramJs.UpdateReadChannelInbox) {
|
||||
sendApiUpdate({
|
||||
'@type': 'updateChat',
|
||||
id: buildApiPeerId(update.channelId, 'channel'),
|
||||
chat: {
|
||||
'@type': 'updateThreadReadState',
|
||||
chatId: buildApiPeerId(update.channelId, 'channel'),
|
||||
threadId: MAIN_THREAD_ID,
|
||||
readState: {
|
||||
lastReadInboxMessageId: update.maxId,
|
||||
unreadCount: update.stillUnreadCount,
|
||||
},
|
||||
});
|
||||
} else if (update instanceof GramJs.UpdateReadChannelOutbox) {
|
||||
sendApiUpdate({
|
||||
'@type': 'updateChat',
|
||||
id: buildApiPeerId(update.channelId, 'channel'),
|
||||
chat: {
|
||||
'@type': 'updateThreadReadState',
|
||||
chatId: buildApiPeerId(update.channelId, 'channel'),
|
||||
threadId: MAIN_THREAD_ID,
|
||||
readState: {
|
||||
lastReadOutboxMessageId: update.maxId,
|
||||
},
|
||||
});
|
||||
} else if (update instanceof GramJs.UpdateReadChannelDiscussionInbox) {
|
||||
sendApiUpdate({
|
||||
'@type': 'updateThreadInfo',
|
||||
threadInfo: {
|
||||
chatId: buildApiPeerId(update.channelId, 'channel'),
|
||||
threadId: update.topMsgId,
|
||||
'@type': 'updateThreadReadState',
|
||||
chatId: buildApiPeerId(update.channelId, 'channel'),
|
||||
threadId: update.topMsgId,
|
||||
readState: {
|
||||
lastReadInboxMessageId: update.readMaxId,
|
||||
},
|
||||
});
|
||||
} else if (update instanceof GramJs.UpdateReadChannelDiscussionOutbox) {
|
||||
sendApiUpdate({
|
||||
'@type': 'updateChat',
|
||||
id: buildApiPeerId(update.channelId, 'channel'),
|
||||
chat: {
|
||||
'@type': 'updateThreadReadState',
|
||||
chatId: buildApiPeerId(update.channelId, 'channel'),
|
||||
threadId: update.topMsgId,
|
||||
readState: {
|
||||
lastReadOutboxMessageId: update.readMaxId,
|
||||
},
|
||||
});
|
||||
@ -752,9 +763,10 @@ export function updater(update: Update) {
|
||||
&& update.peer instanceof GramJs.DialogPeer
|
||||
) {
|
||||
sendApiUpdate({
|
||||
'@type': 'updateChat',
|
||||
id: getApiChatIdFromMtpPeer(update.peer.peer),
|
||||
chat: {
|
||||
'@type': 'updateThreadReadState',
|
||||
chatId: getApiChatIdFromMtpPeer(update.peer.peer),
|
||||
threadId: MAIN_THREAD_ID,
|
||||
readState: {
|
||||
hasUnreadMark: update.unread,
|
||||
},
|
||||
});
|
||||
|
||||
@ -28,12 +28,6 @@ export interface ApiChat {
|
||||
folderId?: number;
|
||||
type: ApiChatType;
|
||||
title: string;
|
||||
hasUnreadMark?: boolean;
|
||||
lastReadOutboxMessageId?: number;
|
||||
lastReadInboxMessageId?: number;
|
||||
unreadCount?: number;
|
||||
unreadMentionsCount?: number;
|
||||
unreadReactionsCount?: number;
|
||||
isVerified?: true;
|
||||
areSignaturesShown?: boolean;
|
||||
areProfilesShown?: boolean;
|
||||
@ -48,7 +42,6 @@ export interface ApiChat {
|
||||
membersCount?: number;
|
||||
creationDate?: number;
|
||||
isSupport?: true;
|
||||
draftDate?: number;
|
||||
isProtected?: boolean;
|
||||
fakeType?: ApiFakeType;
|
||||
color?: ApiTypePeerColor;
|
||||
@ -93,9 +86,6 @@ export interface ApiChat {
|
||||
sendPaidReactionsAsPeerIds?: ApiSendAsPeerId[];
|
||||
sendPaidReactionsPeer?: ApiSendAsPeerId;
|
||||
|
||||
unreadReactions?: number[];
|
||||
unreadMentions?: number[];
|
||||
|
||||
// Stories
|
||||
areStoriesHidden?: boolean;
|
||||
hasStories?: boolean;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { ThreadId, WebPageMediaSize } from '../../types';
|
||||
import type { ThreadId, ThreadReadState, WebPageMediaSize } from '../../types';
|
||||
import type {
|
||||
ApiBotInlineMediaResult,
|
||||
ApiBotInlineResult,
|
||||
@ -813,11 +813,10 @@ export type PaidReactionPrivacyPeer = {
|
||||
peerId: string;
|
||||
};
|
||||
|
||||
interface ApiBaseThreadInfo {
|
||||
export interface ApiBaseThreadInfo {
|
||||
chatId: string;
|
||||
messagesCount: number;
|
||||
messagesCount?: number;
|
||||
lastMessageId?: number;
|
||||
lastReadInboxMessageId?: number;
|
||||
recentReplierIds?: string[];
|
||||
}
|
||||
|
||||
@ -1085,22 +1084,22 @@ export interface ApiTopic {
|
||||
isPinned?: boolean;
|
||||
isHidden?: boolean;
|
||||
isOwner?: boolean;
|
||||
|
||||
// TODO[forums] https://github.com/telegramdesktop/tdesktop/blob/1aece79a471d99a8b63d826b1bce1f36a04d7293/Telegram/SourceFiles/data/data_forum_topic.cpp#L318
|
||||
isMin?: boolean;
|
||||
date: number;
|
||||
title: string;
|
||||
iconColor: number;
|
||||
iconEmojiId?: string;
|
||||
lastMessageId: number;
|
||||
unreadCount: number;
|
||||
unreadMentionsCount: number;
|
||||
unreadReactionsCount: number;
|
||||
fromId: string;
|
||||
notifySettings: ApiPeerNotifySettings;
|
||||
isTitleMissing?: boolean;
|
||||
}
|
||||
|
||||
export interface ApiTopicWithState {
|
||||
topic: ApiTopic;
|
||||
readState?: ThreadReadState;
|
||||
lastMessageId?: number;
|
||||
}
|
||||
|
||||
export const MAIN_THREAD_ID = -1;
|
||||
|
||||
// `Symbol` can not be transferred from worker
|
||||
|
||||
@ -6,7 +6,7 @@ import type {
|
||||
VideoRotation,
|
||||
VideoState,
|
||||
} from '../../lib/secret-sauce';
|
||||
import type { ThreadId } from '../../types';
|
||||
import type { ThreadId, ThreadReadState } from '../../types';
|
||||
import type { RegularLangFnParameters } from '../../util/localization';
|
||||
import type { ApiBotCommand, ApiBotMenuButton } from './bots';
|
||||
import type {
|
||||
@ -122,6 +122,7 @@ export type ApiUpdateChat = {
|
||||
'@type': 'updateChat';
|
||||
id: string;
|
||||
chat: Partial<ApiChat>;
|
||||
readState?: Partial<ThreadReadState>;
|
||||
noTopChatsRequest?: boolean;
|
||||
};
|
||||
|
||||
@ -141,14 +142,6 @@ export type ApiUpdateChatLeave = {
|
||||
id: string;
|
||||
};
|
||||
|
||||
export type ApiUpdateChatInbox = {
|
||||
'@type': 'updateChatInbox';
|
||||
id: string;
|
||||
threadId?: ThreadId;
|
||||
lastReadInboxMessageId: number;
|
||||
unreadCount: number;
|
||||
};
|
||||
|
||||
export type ApiUpdateChatTypingStatus = {
|
||||
'@type': 'updateChatTypingStatus';
|
||||
id: string;
|
||||
@ -245,7 +238,7 @@ export type ApiUpdateNewMessage = {
|
||||
'@type': 'newMessage';
|
||||
chatId: string;
|
||||
id: number;
|
||||
message: Partial<ApiMessage>;
|
||||
message: ApiMessage;
|
||||
shouldForceReply?: boolean;
|
||||
wasDrafted?: boolean;
|
||||
poll?: ApiPoll;
|
||||
@ -256,22 +249,36 @@ export type ApiUpdateMessage = {
|
||||
'@type': 'updateMessage';
|
||||
chatId: string;
|
||||
id: number;
|
||||
message: Partial<ApiMessage>;
|
||||
poll?: ApiPoll;
|
||||
webPage?: ApiWebPage;
|
||||
shouldForceReply?: boolean;
|
||||
isFromNew?: true;
|
||||
};
|
||||
} & (
|
||||
{
|
||||
isFull: true;
|
||||
message: ApiMessage;
|
||||
} | {
|
||||
isFull?: false;
|
||||
message: Partial<ApiMessage>;
|
||||
}
|
||||
);
|
||||
|
||||
export type ApiUpdateScheduledMessage = {
|
||||
'@type': 'updateScheduledMessage';
|
||||
chatId: string;
|
||||
id: number;
|
||||
message: Partial<ApiMessage>;
|
||||
poll?: ApiPoll;
|
||||
webPage?: ApiWebPage;
|
||||
isFromNew?: true;
|
||||
};
|
||||
} & (
|
||||
{
|
||||
isFull: true;
|
||||
message: ApiMessage;
|
||||
} | {
|
||||
isFull?: false;
|
||||
message: Partial<ApiMessage>;
|
||||
}
|
||||
);
|
||||
|
||||
export type ApiUpdateQuickReplyMessage = {
|
||||
'@type': 'updateQuickReplyMessage';
|
||||
@ -306,7 +313,14 @@ export type ApiUpdatePinnedMessageIds = {
|
||||
|
||||
export type ApiUpdateThreadInfo = {
|
||||
'@type': 'updateThreadInfo';
|
||||
threadInfo: Partial<ApiThreadInfo>;
|
||||
threadInfo: ApiThreadInfo;
|
||||
};
|
||||
|
||||
export type ApiUpdateThreadReadState = {
|
||||
'@type': 'updateThreadReadState';
|
||||
chatId: string;
|
||||
threadId: ThreadId;
|
||||
readState: Partial<ThreadReadState>;
|
||||
};
|
||||
|
||||
export type ApiUpdateScheduledMessageSendSucceeded = {
|
||||
@ -424,6 +438,7 @@ export type ApiUpdateMessageReactions = {
|
||||
'@type': 'updateMessageReactions';
|
||||
id: number;
|
||||
chatId: string;
|
||||
threadId?: ThreadId;
|
||||
reactions: ApiReactions;
|
||||
};
|
||||
|
||||
@ -898,7 +913,7 @@ export type ApiUpdateWebPage = {
|
||||
export type ApiUpdate = (
|
||||
ApiUpdateReady | ApiUpdateSession | ApiUpdateWebAuthTokenFailed | ApiUpdateRequestUserUpdate |
|
||||
ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser |
|
||||
ApiUpdateChat | ApiUpdateChatInbox | ApiUpdateChatTypingStatus | ApiUpdateChatFullInfo | ApiUpdatePinnedChatIds |
|
||||
ApiUpdateChat | ApiUpdateChatTypingStatus | ApiUpdateChatFullInfo | ApiUpdatePinnedChatIds |
|
||||
ApiUpdateChatMembers | ApiUpdateChatJoin | ApiUpdateChatLeave | ApiUpdateChatPinned | ApiUpdatePinnedMessageIds |
|
||||
ApiUpdateChatListType | ApiUpdateChatFolder | ApiUpdateChatFoldersOrder | ApiUpdateRecommendedChatFolders |
|
||||
ApiUpdateNewMessage | ApiUpdateMessage | ApiUpdateThreadInfo | ApiUpdateCommonBoxMessages | ApiUpdatePasskeyOption |
|
||||
@ -907,7 +922,7 @@ export type ApiUpdate = (
|
||||
ApiUpdateServiceNotification | ApiDeleteContact | ApiUpdateUser | ApiUpdateUserStatus |
|
||||
ApiUpdateUserFullInfo | ApiUpdateVideoProcessingPending | ApiUpdatePeerSettings | ApiUpdateUserAlreadyAuthorized |
|
||||
ApiUpdateAvatar | ApiUpdateMessageImage | ApiUpdateDraftMessage | ApiUpdateScheduledMessageSendFailed |
|
||||
ApiUpdateError | ApiUpdateResetContacts | ApiUpdateStartEmojiInteraction |
|
||||
ApiUpdateError | ApiUpdateResetContacts | ApiUpdateStartEmojiInteraction | ApiUpdateThreadReadState |
|
||||
ApiUpdateFavoriteStickers | ApiUpdateStickerSet | ApiUpdateStickerSets | ApiUpdateStickerSetsOrder |
|
||||
ApiUpdateRecentStickers | ApiUpdateSavedGifs | ApiUpdateNewScheduledMessage | ApiUpdateMoveStickerSetToTop |
|
||||
ApiUpdateScheduledMessageSendSucceeded | ApiUpdateScheduledMessage | ApiUpdateStarPaymentStateCompleted |
|
||||
|
||||
@ -1234,8 +1234,8 @@
|
||||
"ChatListPinToTop" = "Pin to Top";
|
||||
"ChatListOpenInNewWindow" = "Open in New Window";
|
||||
"ChatListOpenInNewTab" = "Open in New Tab";
|
||||
"ChatListContextMaskAsRead" = "Mark as Read";
|
||||
"ChatListContextMaskAsUnread" = "Mark as Unread";
|
||||
"ChatListContextMarkAsRead" = "Mark as Read";
|
||||
"ChatListContextMarkAsUnread" = "Mark as Unread";
|
||||
"ChatListContextAddToFolder" = "Add to Folder";
|
||||
"Unarchive" = "Unarchive";
|
||||
"Archive" = "Archive";
|
||||
@ -2558,6 +2558,7 @@
|
||||
"BotReadTextFromClipboardTitle" = "Clipboard Access";
|
||||
"BotReadTextFromClipboardDescription" = "{bot} wants to read the contents of your clipboard. Do you want to continue?";
|
||||
"BotReadTextFromClipboardConfirm" = "Allow";
|
||||
"ChatInfoForumTopic" = "Topic";
|
||||
"DiceToast" = "Send a {emoji} emoji to try your luck.";
|
||||
"DiceToastSend" = "Send";
|
||||
"ChatTypePrivate" = "Private Chat";
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { TeactNode } from '../../lib/teact/teact';
|
||||
import {
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
@ -8,14 +9,19 @@ import {
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions } from '../../global';
|
||||
|
||||
import type { ApiChat, ApiTopic } from '../../api/types';
|
||||
import type { ApiChat } from '../../api/types';
|
||||
import type { GlobalState } from '../../global/types';
|
||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
|
||||
import { getOrderedTopics } from '../../global/helpers';
|
||||
import { selectTopic } from '../../global/selectors';
|
||||
import { selectThread } from '../../global/selectors/threads';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { buildCollectionByCallback, mapTruthyValues } from '../../util/iteratees';
|
||||
import { REM } from './helpers/mediaDimensions';
|
||||
import renderText from './helpers/renderText';
|
||||
|
||||
import { useShallowSelector } from '../../hooks/data/useSelector';
|
||||
import { getIsMobile } from '../../hooks/useAppLayout';
|
||||
import { useFastClick } from '../../hooks/useFastClick';
|
||||
import useLang from '../../hooks/useLang';
|
||||
@ -26,7 +32,7 @@ import styles from './ChatForumLastMessage.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
chat: ApiChat;
|
||||
topics?: Record<number, ApiTopic>;
|
||||
topicIds?: number[];
|
||||
hasTags?: boolean;
|
||||
renderLastMessage: () => TeactNode | undefined;
|
||||
observeIntersection?: ObserveFn;
|
||||
@ -37,7 +43,7 @@ const MAX_TOPICS = 3;
|
||||
|
||||
const ChatForumLastMessage = ({
|
||||
chat,
|
||||
topics,
|
||||
topicIds,
|
||||
hasTags,
|
||||
renderLastMessage,
|
||||
observeIntersection,
|
||||
@ -49,13 +55,29 @@ const ChatForumLastMessage = ({
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
const topicsThreadSelector = useCallback((global: GlobalState) => {
|
||||
return buildCollectionByCallback(topicIds || [], (tId) => (
|
||||
[tId, selectThread(global, chat.id, tId)]
|
||||
));
|
||||
}, [chat.id, topicIds]);
|
||||
const topicsThreads = useShallowSelector(topicsThreadSelector);
|
||||
|
||||
const topicsSelector = useCallback((global: GlobalState) => {
|
||||
return topicIds?.map((tId) => selectTopic(global, chat.id, tId)).filter(Boolean);
|
||||
}, [chat.id, topicIds]);
|
||||
const topics = useShallowSelector(topicsSelector);
|
||||
|
||||
const [lastActiveTopic, ...otherTopics] = useMemo(() => {
|
||||
if (!topics) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return getOrderedTopics(Object.values(topics), undefined, true).slice(0, MAX_TOPICS);
|
||||
}, [topics]);
|
||||
const topicsThreadInfos = mapTruthyValues(topicsThreads, (t) => t?.threadInfo);
|
||||
|
||||
return getOrderedTopics(topics, topicsThreadInfos, undefined, true).slice(0, MAX_TOPICS);
|
||||
}, [topics, topicsThreads]);
|
||||
|
||||
const lastActiveTopicReadState = lastActiveTopic ? topicsThreads[lastActiveTopic.id]?.readState : undefined;
|
||||
|
||||
const [isReversedCorner, setIsReversedCorner] = useState(false);
|
||||
const [overwrittenWidth, setOverwrittenWidth] = useState<number | undefined>(undefined);
|
||||
@ -64,7 +86,8 @@ const ChatForumLastMessage = ({
|
||||
handleClick: handleOpenTopicClick,
|
||||
handleMouseDown: handleOpenTopicMouseDown,
|
||||
} = useFastClick((e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (lastActiveTopic.unreadCount === 0 || (chat.isForumAsMessages && !chat.isBotForum)) return;
|
||||
if (!lastActiveTopic) return;
|
||||
if (lastActiveTopicReadState?.unreadCount === 0 || (chat.isForumAsMessages && !chat.isBotForum)) return;
|
||||
|
||||
e.stopPropagation();
|
||||
e.preventDefault();
|
||||
@ -111,7 +134,7 @@ const ChatForumLastMessage = ({
|
||||
<div
|
||||
className={buildClassName(
|
||||
styles.mainColumn,
|
||||
lastActiveTopic.unreadCount && styles.unread,
|
||||
lastActiveTopicReadState?.unreadCount && styles.unread,
|
||||
)}
|
||||
ref={mainColumnRef}
|
||||
onClick={handleOpenTopicClick}
|
||||
@ -133,7 +156,7 @@ const ChatForumLastMessage = ({
|
||||
{otherTopics.map((topic) => (
|
||||
<div
|
||||
className={buildClassName(
|
||||
styles.otherColumn, topic.unreadCount && styles.unread,
|
||||
styles.otherColumn, topicsThreads[topic.id]?.readState?.unreadCount && styles.unread,
|
||||
)}
|
||||
key={topic.id}
|
||||
>
|
||||
@ -159,7 +182,10 @@ const ChatForumLastMessage = ({
|
||||
)
|
||||
}
|
||||
<div
|
||||
className={buildClassName(styles.lastMessage, lastActiveTopic?.unreadCount && !hasTags && styles.unread)}
|
||||
className={buildClassName(
|
||||
styles.lastMessage,
|
||||
lastActiveTopicReadState?.unreadCount && !hasTags && styles.unread,
|
||||
)}
|
||||
ref={lastMessageRef}
|
||||
onClick={handleOpenTopicClick}
|
||||
onMouseDown={handleOpenTopicMouseDown}
|
||||
|
||||
@ -77,10 +77,7 @@ import {
|
||||
selectChatType,
|
||||
selectCurrentMessageList,
|
||||
selectCustomEmoji,
|
||||
selectDraft,
|
||||
selectEditingDraft,
|
||||
selectEditingMessage,
|
||||
selectEditingScheduledDraft,
|
||||
selectIsChatWithSelf,
|
||||
selectIsCurrentUserFrozen,
|
||||
selectIsCurrentUserPremium,
|
||||
@ -91,7 +88,6 @@ import {
|
||||
selectNewestMessageWithBotKeyboardButtons,
|
||||
selectNotifyDefaults,
|
||||
selectNotifyException,
|
||||
selectNoWebPage,
|
||||
selectPeer,
|
||||
selectPeerPaidMessagesStars,
|
||||
selectPeerStory,
|
||||
@ -107,6 +103,12 @@ import {
|
||||
} from '../../global/selectors';
|
||||
import { selectCurrentLimit } from '../../global/selectors/limits';
|
||||
import { selectSharedSettings } from '../../global/selectors/sharedState';
|
||||
import {
|
||||
selectDraft,
|
||||
selectEditingDraft,
|
||||
selectEditingScheduledDraft,
|
||||
selectNoWebPage,
|
||||
} from '../../global/selectors/threads';
|
||||
import { IS_IOS, IS_VOICE_RECORDING_SUPPORTED } from '../../util/browser/windowEnvironment';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { formatMediaDuration, formatVoiceRecordDuration } from '../../util/dates/dateFormat';
|
||||
|
||||
@ -19,10 +19,10 @@ import {
|
||||
selectChatOnlineCount,
|
||||
selectIsChatRestricted,
|
||||
selectMonoforumChannel,
|
||||
selectThreadMessagesCount,
|
||||
selectTopic,
|
||||
selectUser,
|
||||
} from '../../global/selectors';
|
||||
import { selectThreadMessagesCount } from '../../global/selectors/threads';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { REM } from './helpers/mediaDimensions';
|
||||
import renderText from './helpers/renderText';
|
||||
|
||||
@ -13,11 +13,11 @@ import {
|
||||
} from '../../global/helpers';
|
||||
import {
|
||||
selectChatMessages,
|
||||
selectThreadMessagesCount,
|
||||
selectTopic,
|
||||
selectUser,
|
||||
selectUserStatus,
|
||||
} from '../../global/selectors';
|
||||
import { selectThreadMessagesCount } from '../../global/selectors/threads';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { REM } from './helpers/mediaDimensions';
|
||||
import renderText from './helpers/renderText';
|
||||
@ -214,8 +214,10 @@ const PrivateChatInfo = ({
|
||||
className="message-count-transition"
|
||||
>
|
||||
{messagesCount !== undefined
|
||||
? lang('Messages', { count: messagesCount }, { pluralValue: messagesCount })
|
||||
: lang('ChatInfoNoMessages')}
|
||||
? (messagesCount > 0
|
||||
? lang('Messages', { count: messagesCount }, { pluralValue: messagesCount })
|
||||
: lang('ChatInfoNoMessages')
|
||||
) : lang('ChatInfoForumTopic')}
|
||||
</Transition>
|
||||
</span>
|
||||
);
|
||||
|
||||
@ -20,9 +20,9 @@ import {
|
||||
selectPeerPaidMessagesStars,
|
||||
selectShouldSchedule,
|
||||
selectStickerSet,
|
||||
selectThreadInfo,
|
||||
selectTopic,
|
||||
} from '../../global/selectors';
|
||||
import { selectThreadInfo } from '../../global/selectors/threads';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { copyTextToClipboard } from '../../util/clipboard';
|
||||
import renderText from './helpers/renderText';
|
||||
|
||||
@ -4,6 +4,7 @@ import {
|
||||
|
||||
import type { ApiBusinessWorkHours } from '../../../api/types';
|
||||
|
||||
import { selectTimezones } from '../../../global/selectors';
|
||||
import {
|
||||
VTT_PROFILE_BUSINESS_HOURS,
|
||||
VTT_PROFILE_BUSINESS_HOURS_COLLAPSE,
|
||||
@ -18,7 +19,7 @@ import {
|
||||
|
||||
import { useViewTransition } from '../../../hooks/animations/useViewTransition';
|
||||
import { useVtn } from '../../../hooks/animations/useVtn';
|
||||
import useSelectorSignal from '../../../hooks/data/useSelectorSignal';
|
||||
import { useSelectorSignal } from '../../../hooks/data/useSelector';
|
||||
import useInterval from '../../../hooks/schedulers/useInterval';
|
||||
import useDerivedState from '../../../hooks/useDerivedState';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
@ -52,7 +53,7 @@ const BusinessHours = ({
|
||||
|
||||
useInterval(forceUpdate, 60 * 1000);
|
||||
|
||||
const timezoneSignal = useSelectorSignal((global) => global.timezones?.byId);
|
||||
const timezoneSignal = useSelectorSignal(selectTimezones);
|
||||
const timezones = useDerivedState(timezoneSignal, [timezoneSignal]);
|
||||
const timezoneMinuteDifference = useMemo(() => {
|
||||
if (!timezones) return 0;
|
||||
|
||||
@ -33,13 +33,13 @@ import {
|
||||
selectPeerSavedGifts,
|
||||
selectTabState,
|
||||
selectTheme,
|
||||
selectThreadMessagesCount,
|
||||
selectTopic,
|
||||
selectUser,
|
||||
selectUserFullInfo,
|
||||
selectUserStatus,
|
||||
} from '../../../global/selectors/index';
|
||||
import { selectSharedSettings } from '../../../global/selectors/sharedState';
|
||||
import { selectThreadMessagesCount } from '../../../global/selectors/threads.ts';
|
||||
import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import buildStyle from '../../../util/buildStyle';
|
||||
@ -654,7 +654,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
emojiStatus,
|
||||
profilePhotos,
|
||||
topic,
|
||||
messagesCount: topic ? selectThreadMessagesCount(global, peerId, currentTopicId!) : undefined,
|
||||
messagesCount: topic ? selectThreadMessagesCount(global, peerId, topic.id) : undefined,
|
||||
profileColorOption: profileColor,
|
||||
theme,
|
||||
isPlain: !hasBackground,
|
||||
|
||||
@ -3,10 +3,13 @@ import { getActions, getGlobal } from '../../../global';
|
||||
|
||||
import type { GlobalState } from '../../../global/types';
|
||||
import type { CustomPeer } from '../../../types';
|
||||
import { MAIN_THREAD_ID } from '../../../api/types';
|
||||
|
||||
import { ANIMATION_LEVEL_MIN, ARCHIVED_FOLDER_ID } from '../../../config';
|
||||
import { getChatTitle } from '../../../global/helpers';
|
||||
import { selectChat } from '../../../global/selectors';
|
||||
import { selectAnimationLevel } from '../../../global/selectors/sharedState';
|
||||
import { selectThreadReadState } from '../../../global/selectors/threads';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import buildStyle from '../../../util/buildStyle';
|
||||
import { waitForTransitionEnd } from '../../../util/cssAnimationEndListeners';
|
||||
@ -95,11 +98,12 @@ const Archive = ({
|
||||
const previewItems = useMemo(() => {
|
||||
if (!orderedChatIds?.length) return lang('Loading');
|
||||
|
||||
const chatsById = getGlobal().chats.byId;
|
||||
const global = getGlobal();
|
||||
|
||||
return orderedChatIds.slice(0, PREVIEW_SLICE).map((chatId, i, arr) => {
|
||||
const isLast = i === arr.length - 1;
|
||||
const chat = chatsById[chatId];
|
||||
const chat = selectChat(global, chatId);
|
||||
const readState = selectThreadReadState(global, chatId, MAIN_THREAD_ID);
|
||||
if (!chat) {
|
||||
return undefined;
|
||||
}
|
||||
@ -108,7 +112,7 @@ const Archive = ({
|
||||
|
||||
return (
|
||||
<>
|
||||
<span className={buildClassName(styles.chat, archiveUnreadCount && chat.unreadCount && styles.unread)}>
|
||||
<span className={buildClassName(styles.chat, archiveUnreadCount && readState?.unreadCount && styles.unread)}>
|
||||
{renderText(title)}
|
||||
</span>
|
||||
{isLast ? '' : ', '}
|
||||
|
||||
@ -33,7 +33,6 @@ import {
|
||||
selectChatLastMessageId,
|
||||
selectChatMessage,
|
||||
selectCurrentMessageList,
|
||||
selectDraft,
|
||||
selectIsCurrentUserFrozen,
|
||||
selectIsCurrentUserPremium,
|
||||
selectIsForumPanelClosed,
|
||||
@ -46,19 +45,19 @@ import {
|
||||
selectPeerStory,
|
||||
selectSender,
|
||||
selectTabState,
|
||||
selectThreadParam,
|
||||
selectTopicFromMessage,
|
||||
selectTopicsInfo,
|
||||
selectUser,
|
||||
selectUserStatus,
|
||||
} from '../../../global/selectors';
|
||||
import { selectDraft, selectThreadLocalStateParam } from '../../../global/selectors/threads';
|
||||
import { IS_OPEN_IN_NEW_TAB_SUPPORTED } from '../../../util/browser/windowEnvironment';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { isUserId } from '../../../util/entities/ids';
|
||||
import { getChatFolderIds } from '../../../util/folderManager';
|
||||
import { createLocationHash } from '../../../util/routing';
|
||||
|
||||
import useSelectorSignal from '../../../hooks/data/useSelectorSignal';
|
||||
import { useSelectorSignal } from '../../../hooks/data/useSelector';
|
||||
import useAppLayout from '../../../hooks/useAppLayout';
|
||||
import useChatContextActions from '../../../hooks/useChatContextActions';
|
||||
import useEnsureMessage from '../../../hooks/useEnsureMessage';
|
||||
@ -96,11 +95,11 @@ type OwnProps = {
|
||||
previewMessageId?: number;
|
||||
className?: string;
|
||||
withTags?: boolean;
|
||||
isFoldersSidebarShown?: boolean;
|
||||
observeIntersection?: ObserveFn;
|
||||
onDragEnter?: (chatId: string) => void;
|
||||
onDragLeave?: NoneToVoidFunction;
|
||||
onReorderAnimationEnd?: NoneToVoidFunction;
|
||||
isFoldersSidebarShown?: boolean;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
@ -108,7 +107,6 @@ type StateProps = {
|
||||
monoforumChannel?: ApiChat;
|
||||
lastMessageStory?: ApiTypeStory;
|
||||
listedTopicIds?: number[];
|
||||
topics?: Record<number, ApiTopic>;
|
||||
isMuted?: boolean;
|
||||
user?: ApiUser;
|
||||
userStatus?: ApiUserStatus;
|
||||
@ -142,7 +140,6 @@ const Chat: FC<OwnProps & StateProps> = ({
|
||||
animationType,
|
||||
isPinned,
|
||||
listedTopicIds,
|
||||
topics,
|
||||
observeIntersection,
|
||||
chat,
|
||||
monoforumChannel,
|
||||
@ -170,16 +167,16 @@ const Chat: FC<OwnProps & StateProps> = ({
|
||||
previewMessageId,
|
||||
className,
|
||||
isSynced,
|
||||
onDragEnter,
|
||||
onDragLeave,
|
||||
isAccountFrozen,
|
||||
chatFolderIds,
|
||||
orderedFolderIds,
|
||||
chatFoldersById,
|
||||
areTagsEnabled,
|
||||
withTags,
|
||||
onReorderAnimationEnd,
|
||||
isFoldersSidebarShown,
|
||||
onDragEnter,
|
||||
onDragLeave,
|
||||
onReorderAnimationEnd,
|
||||
}) => {
|
||||
const {
|
||||
openChat,
|
||||
@ -245,7 +242,7 @@ const Chat: FC<OwnProps & StateProps> = ({
|
||||
isSavedDialog,
|
||||
isPreview,
|
||||
onReorderAnimationEnd,
|
||||
topics,
|
||||
topicIds: listedTopicIds,
|
||||
hasTags: shouldRenderTags,
|
||||
});
|
||||
|
||||
@ -366,7 +363,7 @@ const Chat: FC<OwnProps & StateProps> = ({
|
||||
isSavedDialog,
|
||||
currentUserId,
|
||||
isPreview,
|
||||
topics,
|
||||
topicIds: listedTopicIds,
|
||||
});
|
||||
|
||||
const isIntersecting = useIsIntersecting(ref, chat ? observeIntersection : undefined);
|
||||
@ -445,7 +442,6 @@ const Chat: FC<OwnProps & StateProps> = ({
|
||||
isMuted={isMuted}
|
||||
shouldShowOnlyMostImportant
|
||||
forceHidden={getIsForumPanelClosed}
|
||||
topics={topics}
|
||||
isSelected={isSelected}
|
||||
isOnAvatar
|
||||
/>
|
||||
@ -485,7 +481,6 @@ const Chat: FC<OwnProps & StateProps> = ({
|
||||
isMuted={isMuted}
|
||||
isSavedDialog={isSavedDialog}
|
||||
hasMiniApp={user?.hasMainMiniApp}
|
||||
topics={topics}
|
||||
isSelected={isSelected}
|
||||
transitionClassName="chat-badge-transition"
|
||||
/>
|
||||
@ -557,7 +552,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
const {
|
||||
chatId: currentChatId,
|
||||
threadId: currentThreadId,
|
||||
type: messageListType,
|
||||
type: currentMessageListType,
|
||||
} = selectCurrentMessageList(global) || {};
|
||||
const isSelected = !isPreview && chatId === currentChatId && (isSavedDialog
|
||||
? chatId === currentThreadId : currentThreadId === MAIN_THREAD_ID);
|
||||
@ -567,7 +562,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
const userStatus = selectUserStatus(global, chatId);
|
||||
const lastMessageTopic = lastMessage && selectTopicFromMessage(global, lastMessage);
|
||||
|
||||
const typingStatus = selectThreadParam(global, chatId, MAIN_THREAD_ID, 'typingStatus');
|
||||
const typingStatus = selectThreadLocalStateParam(global, chatId, MAIN_THREAD_ID, 'typingStatus');
|
||||
|
||||
const topicsInfo = selectTopicsInfo(global, chatId);
|
||||
|
||||
@ -585,9 +580,11 @@ export default memo(withGlobal<OwnProps>(
|
||||
isSelected,
|
||||
isSelectedForum,
|
||||
isForumPanelOpen: selectIsForumPanelOpen(global),
|
||||
canScrollDown: isSelected && messageListType === 'thread',
|
||||
canScrollDown: isSelected && currentMessageListType === 'thread',
|
||||
canChangeFolder: (global.chatFolders.orderedIds?.length || 0) > 1,
|
||||
lastMessageOutgoingStatus: isOutgoing && lastMessage ? selectOutgoingStatus(global, lastMessage) : undefined,
|
||||
lastMessageOutgoingStatus: isOutgoing && lastMessage && !isSavedDialog
|
||||
? selectOutgoingStatus(global, chatId, MAIN_THREAD_ID, lastMessage.id, 'thread')
|
||||
: undefined,
|
||||
user,
|
||||
userStatus,
|
||||
lastMessageTopic,
|
||||
@ -597,7 +594,6 @@ export default memo(withGlobal<OwnProps>(
|
||||
lastMessageId,
|
||||
currentUserId: global.currentUserId!,
|
||||
listedTopicIds: topicsInfo?.listedTopicIds,
|
||||
topics: topicsInfo?.topicsById,
|
||||
isSynced: global.isSynced,
|
||||
lastMessageStory,
|
||||
isAccountFrozen,
|
||||
|
||||
@ -1,15 +1,20 @@
|
||||
import { memo, useMemo } from '../../../lib/teact/teact';
|
||||
import { memo, useCallback, useMemo } from '../../../lib/teact/teact';
|
||||
import { getActions } from '../../../global';
|
||||
|
||||
import type { ApiChat, ApiTopic } from '../../../api/types';
|
||||
import type { GlobalState } from '../../../global/types';
|
||||
import type { Signal } from '../../../util/signals';
|
||||
import { type ApiChat, type ApiTopic, MAIN_THREAD_ID } from '../../../api/types';
|
||||
|
||||
import { selectTopicsInfo } from '../../../global/selectors';
|
||||
import { selectThreadReadState } from '../../../global/selectors/threads';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { buildCollectionByCallback } from '../../../util/iteratees';
|
||||
import { getServerTime } from '../../../util/serverTime';
|
||||
import { isSignal } from '../../../util/signals';
|
||||
import { formatIntegerCompact } from '../../../util/textFormat';
|
||||
import { extractCurrentThemeParams } from '../../../util/themeStyle';
|
||||
|
||||
import useSelector, { useShallowSelector } from '../../../hooks/data/useSelector';
|
||||
import useDerivedState from '../../../hooks/useDerivedState';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
@ -31,7 +36,6 @@ type OwnProps = {
|
||||
shouldShowOnlyMostImportant?: boolean;
|
||||
hasMiniApp?: boolean;
|
||||
forceHidden?: boolean | Signal<boolean>;
|
||||
topics?: Record<number, ApiTopic>;
|
||||
isSelected?: boolean;
|
||||
isOnAvatar?: boolean;
|
||||
transitionClassName?: string;
|
||||
@ -40,7 +44,6 @@ type OwnProps = {
|
||||
|
||||
const ChatBadge = ({
|
||||
topic,
|
||||
topics,
|
||||
chat,
|
||||
isPinned,
|
||||
isMuted,
|
||||
@ -58,23 +61,47 @@ const ChatBadge = ({
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
const readStateSelector = useCallback((global: GlobalState) => {
|
||||
return selectThreadReadState(global, chat.id, topic?.id || MAIN_THREAD_ID);
|
||||
}, [chat.id, topic?.id]);
|
||||
|
||||
const readState = useSelector(readStateSelector);
|
||||
|
||||
const {
|
||||
unreadMentionsCount = 0, unreadReactionsCount = 0,
|
||||
} = !chat.isForum ? chat : {}; // TODO[forums] Unread mentions and reactions temporarily disabled for forums
|
||||
unreadMentionsCount: stateUnreadMentionsCount = 0,
|
||||
unreadReactionsCount: stateUnreadReactionsCount = 0,
|
||||
unreadCount: stateUnreadCount = 0,
|
||||
hasUnreadMark,
|
||||
} = readState || {};
|
||||
|
||||
const topicsInfoSelector = useCallback((global: GlobalState) => {
|
||||
return selectTopicsInfo(global, chat.id);
|
||||
}, [chat.id]);
|
||||
const topicsInfo = useShallowSelector(topicsInfoSelector);
|
||||
const { listedTopicIds, topicsById } = topicsInfo || {};
|
||||
|
||||
const topicsReadStateSelector = useCallback((global: GlobalState) => {
|
||||
return buildCollectionByCallback(listedTopicIds || [], (tId) => (
|
||||
[tId, selectThreadReadState(global, chat.id, tId)]
|
||||
));
|
||||
}, [chat.id, listedTopicIds]);
|
||||
const topicsReadStates = useShallowSelector(topicsReadStateSelector);
|
||||
|
||||
const isTopicUnopened = !isPinned && topic && !wasTopicOpened;
|
||||
const isForum = chat.isForum && !topic;
|
||||
const topicsWithUnread = useMemo(() => (
|
||||
isForum && topics ? Object.values(topics).filter(({ unreadCount }) => unreadCount) : undefined
|
||||
), [topics, isForum]);
|
||||
const topicsWithUnreadIds = useMemo(() => (
|
||||
isForum && listedTopicIds ? listedTopicIds.filter((tId) => topicsReadStates[tId]?.unreadCount) : undefined
|
||||
), [listedTopicIds, isForum, topicsReadStates]);
|
||||
const topicsWithUnreadMentionsIds = useMemo(() => (
|
||||
isForum && listedTopicIds ? listedTopicIds.filter((tId) => topicsReadStates[tId]?.unreadMentionsCount) : undefined
|
||||
), [listedTopicIds, isForum, topicsReadStates]);
|
||||
const topicsWithUnreadReactionsIds = useMemo(() => (
|
||||
isForum && listedTopicIds ? listedTopicIds.filter((tId) => topicsReadStates[tId]?.unreadReactionsCount) : undefined
|
||||
), [listedTopicIds, isForum, topicsReadStates]);
|
||||
|
||||
const unreadCount = useMemo(() => {
|
||||
if (!isForum) {
|
||||
return (topic || chat).unreadCount;
|
||||
}
|
||||
|
||||
return topicsWithUnread?.length;
|
||||
}, [chat, topic, topicsWithUnread, isForum]);
|
||||
const unreadCount = isForum ? topicsWithUnreadIds?.length : stateUnreadCount;
|
||||
const unreadMentionsCount = isForum ? topicsWithUnreadMentionsIds?.length : stateUnreadMentionsCount;
|
||||
const unreadReactionsCount = isForum ? topicsWithUnreadReactionsIds?.length : stateUnreadReactionsCount;
|
||||
|
||||
const shouldBeUnMuted = useMemo(() => {
|
||||
if (!isForum) {
|
||||
@ -82,17 +109,17 @@ const ChatBadge = ({
|
||||
}
|
||||
|
||||
if (isMuted) {
|
||||
return topicsWithUnread?.some((acc) => acc.notifySettings.mutedUntil === 0);
|
||||
return topicsWithUnreadIds?.some((tId) => topicsById?.[tId]?.notifySettings.mutedUntil === 0);
|
||||
}
|
||||
|
||||
const isEveryUnreadMuted = topicsWithUnread?.every((acc) => (
|
||||
acc.notifySettings.mutedUntil && acc.notifySettings.mutedUntil > getServerTime()
|
||||
));
|
||||
const isEveryUnreadMuted = topicsWithUnreadIds?.every((tId) => {
|
||||
const mutedUntil = topicsById?.[tId]?.notifySettings.mutedUntil;
|
||||
return mutedUntil && mutedUntil > getServerTime();
|
||||
});
|
||||
|
||||
return !isEveryUnreadMuted;
|
||||
}, [isForum, isMuted, topicsWithUnread, topic?.notifySettings.mutedUntil]);
|
||||
}, [isForum, isMuted, topicsWithUnreadIds, topicsById, topic?.notifySettings.mutedUntil]);
|
||||
|
||||
const hasUnreadMark = topic ? false : chat.hasUnreadMark;
|
||||
const isUnread = Boolean((unreadCount || hasUnreadMark) && !isSavedDialog);
|
||||
|
||||
const resolvedForceHidden = useDerivedState(
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import {
|
||||
beginHeavyAnimation,
|
||||
memo, useEffect, useMemo, useRef, useState,
|
||||
memo, useCallback, useEffect, useMemo, useRef, useState,
|
||||
} from '../../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../../global';
|
||||
|
||||
import type { ApiChat } from '../../../../api/types';
|
||||
import type { GlobalState } from '../../../../global/types';
|
||||
import type { TopicsInfo } from '../../../../types';
|
||||
import { MAIN_THREAD_ID } from '../../../../api/types';
|
||||
|
||||
@ -21,13 +22,16 @@ import {
|
||||
selectTabState,
|
||||
selectTopicsInfo,
|
||||
} from '../../../../global/selectors';
|
||||
import { selectThread } from '../../../../global/selectors/threads';
|
||||
import { IS_TOUCH_ENV } from '../../../../util/browser/windowEnvironment';
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
import captureEscKeyListener from '../../../../util/captureEscKeyListener';
|
||||
import { captureEvents, SwipeDirection } from '../../../../util/captureEvents';
|
||||
import { waitForTransitionEnd } from '../../../../util/cssAnimationEndListeners';
|
||||
import { isUserId } from '../../../../util/entities/ids';
|
||||
import { mapTruthyValues, mapValues } from '../../../../util/iteratees';
|
||||
|
||||
import useSelector from '../../../../hooks/data/useSelector';
|
||||
import useAppLayout from '../../../../hooks/useAppLayout';
|
||||
import useHistoryBack from '../../../../hooks/useHistoryBack';
|
||||
import useInfiniteScroll from '../../../../hooks/useInfiniteScroll';
|
||||
@ -122,10 +126,18 @@ const ForumPanel = ({
|
||||
setIsScrolled(!isIntersecting);
|
||||
});
|
||||
|
||||
const topicsThreadSelector = useCallback((global: GlobalState) => {
|
||||
if (!chat?.id) return undefined;
|
||||
return mapTruthyValues(topicsInfo?.topicsById || {}, (t) => selectThread(global, chat.id, t.id));
|
||||
}, [chat?.id, topicsInfo?.topicsById]);
|
||||
const topicsThreads = useSelector(topicsThreadSelector);
|
||||
|
||||
const orderedIds = useMemo(() => {
|
||||
const ids = topicsInfo
|
||||
const topicsThreadInfos = topicsThreads && mapValues(topicsThreads, (t) => t.threadInfo);
|
||||
const ids = topicsInfo && topicsThreads
|
||||
? getOrderedTopics(
|
||||
Object.values(topicsInfo.topicsById),
|
||||
topicsThreadInfos,
|
||||
topicsInfo.orderedPinnedTopicIds,
|
||||
).map(({ id }) => id)
|
||||
: [];
|
||||
@ -133,7 +145,7 @@ const ForumPanel = ({
|
||||
if (!chat?.isBotForum) return ids;
|
||||
|
||||
return [MAIN_THREAD_ID, ...ids];
|
||||
}, [chat?.isBotForum, topicsInfo]);
|
||||
}, [chat?.isBotForum, topicsInfo, topicsThreads]);
|
||||
|
||||
const { orderDiffById, shiftDiff, getAnimationType, onReorderAnimationEnd } = useOrderDiff(orderedIds, 0, chat?.id);
|
||||
|
||||
|
||||
@ -17,16 +17,19 @@ import {
|
||||
selectChat,
|
||||
selectChatMessage,
|
||||
selectCurrentMessageList,
|
||||
selectDraft,
|
||||
selectNotifyDefaults,
|
||||
selectNotifyException,
|
||||
selectOutgoingStatus,
|
||||
selectPeerStory,
|
||||
selectSender,
|
||||
selectThreadInfo,
|
||||
selectThreadParam,
|
||||
selectTopics,
|
||||
selectTopicsInfo,
|
||||
} from '../../../../global/selectors';
|
||||
import {
|
||||
selectDraft,
|
||||
selectThreadInfo,
|
||||
selectThreadLocalStateParam,
|
||||
selectThreadReadState,
|
||||
} from '../../../../global/selectors/threads';
|
||||
import { IS_OPEN_IN_NEW_TAB_SUPPORTED } from '../../../../util/browser/windowEnvironment';
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
import { createLocationHash } from '../../../../util/routing';
|
||||
@ -73,7 +76,8 @@ type StateProps = {
|
||||
canScrollDown?: boolean;
|
||||
wasTopicOpened?: boolean;
|
||||
withInterfaceAnimations?: boolean;
|
||||
topics?: Record<number, ApiTopic>;
|
||||
topicIds?: number[];
|
||||
unreadCount?: number;
|
||||
};
|
||||
|
||||
const Topic = ({
|
||||
@ -97,7 +101,8 @@ const Topic = ({
|
||||
typingStatus,
|
||||
draft,
|
||||
wasTopicOpened,
|
||||
topics,
|
||||
topicIds,
|
||||
unreadCount,
|
||||
onReorderAnimationEnd,
|
||||
}: OwnProps & StateProps) => {
|
||||
const {
|
||||
@ -149,7 +154,7 @@ const Topic = ({
|
||||
observeIntersection,
|
||||
isTopic: true,
|
||||
typingStatus,
|
||||
topics,
|
||||
topicIds,
|
||||
statefulMediaContent: groupStatefulContent({ story: lastMessageStory }),
|
||||
|
||||
animationType,
|
||||
@ -180,6 +185,7 @@ const Topic = ({
|
||||
isChatMuted,
|
||||
wasOpened: wasTopicOpened,
|
||||
canDelete,
|
||||
unreadCount,
|
||||
handleDelete: handleOpenDeleteModal,
|
||||
handleMute,
|
||||
handleUnmute,
|
||||
@ -226,7 +232,6 @@ const Topic = ({
|
||||
isMuted={isMuted}
|
||||
topic={topic}
|
||||
wasTopicOpened={wasTopicOpened}
|
||||
topics={topics}
|
||||
isSelected={isSelected}
|
||||
/>
|
||||
</div>
|
||||
@ -259,14 +264,17 @@ export default memo(withGlobal<OwnProps>(
|
||||
(global, { chatId, topic, isSelected }) => {
|
||||
const chat = selectChat(global, chatId);
|
||||
|
||||
const lastMessage = selectChatMessage(global, chatId, topic.lastMessageId);
|
||||
const threadInfo = selectThreadInfo(global, chatId, topic.id);
|
||||
const lastMessage = threadInfo?.lastMessageId
|
||||
? selectChatMessage(global, chatId, threadInfo.lastMessageId) : undefined;
|
||||
const { isOutgoing } = lastMessage || {};
|
||||
const lastMessageSender = lastMessage && selectSender(global, lastMessage);
|
||||
const typingStatus = selectThreadParam(global, chatId, topic.id, 'typingStatus');
|
||||
const typingStatus = selectThreadLocalStateParam(global, chatId, topic.id, 'typingStatus');
|
||||
const draft = selectDraft(global, chatId, topic.id);
|
||||
const threadInfo = selectThreadInfo(global, chatId, topic.id);
|
||||
const wasTopicOpened = chat?.isBotForum || Boolean(threadInfo?.lastReadInboxMessageId);
|
||||
const topics = selectTopics(global, chatId);
|
||||
|
||||
const readState = selectThreadReadState(global, chatId, topic.id);
|
||||
const wasTopicOpened = chat?.isBotForum || Boolean(readState?.lastReadInboxMessageId);
|
||||
const topicIds = selectTopicsInfo(global, chatId)?.listedTopicIds;
|
||||
|
||||
const { chatId: currentChatId, threadId: currentThreadId } = selectCurrentMessageList(global) || {};
|
||||
|
||||
@ -287,12 +295,13 @@ export default memo(withGlobal<OwnProps>(
|
||||
withInterfaceAnimations: selectCanAnimateInterface(global),
|
||||
draft,
|
||||
...(isOutgoing && lastMessage && {
|
||||
lastMessageOutgoingStatus: selectOutgoingStatus(global, lastMessage),
|
||||
lastMessageOutgoingStatus: selectOutgoingStatus(global, chatId, topic.id, lastMessage.id, 'thread'),
|
||||
}),
|
||||
canScrollDown: isSelected && chat?.id === currentChatId && currentThreadId === topic.id,
|
||||
wasTopicOpened,
|
||||
topics,
|
||||
topicIds,
|
||||
lastMessageStory,
|
||||
unreadCount: readState?.unreadCount,
|
||||
};
|
||||
},
|
||||
)(Topic));
|
||||
|
||||
@ -36,7 +36,7 @@ import TypingStatus from '../../../common/TypingStatus';
|
||||
|
||||
export default function useChatListEntry({
|
||||
chat,
|
||||
topics,
|
||||
topicIds,
|
||||
lastMessage,
|
||||
statefulMediaContent,
|
||||
chatId,
|
||||
@ -56,7 +56,7 @@ export default function useChatListEntry({
|
||||
onReorderAnimationEnd,
|
||||
}: {
|
||||
chat?: ApiChat;
|
||||
topics?: Record<number, ApiTopic>;
|
||||
topicIds?: number[];
|
||||
lastMessage?: ApiMessage;
|
||||
statefulMediaContent: StatefulMediaContent | undefined;
|
||||
chatId: string;
|
||||
@ -157,7 +157,7 @@ export default function useChatListEntry({
|
||||
chat={chat}
|
||||
renderLastMessage={renderLastMessageOrTyping}
|
||||
observeIntersection={observeIntersection}
|
||||
topics={topics}
|
||||
topicIds={topicIds}
|
||||
hasTags={hasTags}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { useMemo } from '../../../../lib/teact/teact';
|
||||
import { useMemo } from '@teact';
|
||||
import { getActions } from '../../../../global';
|
||||
|
||||
import type { ApiChat, ApiTopic } from '../../../../api/types';
|
||||
@ -14,6 +14,7 @@ import useOldLang from '../../../../hooks/useOldLang';
|
||||
|
||||
export default function useTopicContextActions({
|
||||
topic,
|
||||
unreadCount,
|
||||
chat,
|
||||
isChatMuted,
|
||||
wasOpened,
|
||||
@ -24,6 +25,7 @@ export default function useTopicContextActions({
|
||||
}: {
|
||||
topic: ApiTopic;
|
||||
chat: ApiChat;
|
||||
unreadCount?: number;
|
||||
isChatMuted?: boolean;
|
||||
wasOpened?: boolean;
|
||||
canDelete?: boolean;
|
||||
@ -34,7 +36,7 @@ export default function useTopicContextActions({
|
||||
const lang = useLang();
|
||||
const oldLang = useOldLang();
|
||||
|
||||
return useMemo(() => {
|
||||
const preparedActions = useMemo(() => {
|
||||
const {
|
||||
isPinned, notifySettings, isClosed, id: topicId,
|
||||
} = topic;
|
||||
@ -68,7 +70,7 @@ export default function useTopicContextActions({
|
||||
},
|
||||
};
|
||||
|
||||
const actionUnreadMark = topic.unreadCount || !wasOpened
|
||||
const actionUnreadMark = unreadCount || !wasOpened
|
||||
? {
|
||||
title: oldLang('MarkAsRead'),
|
||||
icon: 'readchats',
|
||||
@ -130,5 +132,10 @@ export default function useTopicContextActions({
|
||||
actionCloseTopic,
|
||||
actionDelete,
|
||||
]) as MenuItemContextAction[];
|
||||
}, [topic, chat, isChatMuted, wasOpened, lang, oldLang, canDelete, handleDelete, handleMute, handleUnmute]);
|
||||
}, [
|
||||
chat, topic, unreadCount, wasOpened, isChatMuted, canDelete,
|
||||
handleDelete, handleMute, handleUnmute, lang, oldLang,
|
||||
]);
|
||||
|
||||
return preparedActions;
|
||||
}
|
||||
|
||||
@ -1,4 +1,3 @@
|
||||
import type { FC } from '@teact';
|
||||
import { memo, useCallback } from '@teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
@ -12,6 +11,7 @@ import {
|
||||
selectIsChatPinned,
|
||||
selectNotifyDefaults,
|
||||
selectNotifyException,
|
||||
selectTopicsInfo,
|
||||
selectUser,
|
||||
} from '../../../global/selectors';
|
||||
import { onDragEnter, onDragLeave } from '../../../util/dragNDropHandlers.ts';
|
||||
@ -41,22 +41,24 @@ type OwnProps = {
|
||||
type StateProps = {
|
||||
chat?: ApiChat;
|
||||
user?: ApiUser;
|
||||
listedTopicIds?: number[];
|
||||
isPinned?: boolean;
|
||||
isMuted?: boolean;
|
||||
canChangeFolder?: boolean;
|
||||
};
|
||||
|
||||
const LeftSearchResultChat: FC<OwnProps & StateProps> = ({
|
||||
const LeftSearchResultChat = ({
|
||||
chatId,
|
||||
withUsername,
|
||||
chat,
|
||||
user,
|
||||
listedTopicIds,
|
||||
isPinned,
|
||||
isMuted,
|
||||
canChangeFolder,
|
||||
withOpenAppButton,
|
||||
onClick,
|
||||
}) => {
|
||||
}: OwnProps & StateProps) => {
|
||||
const { requestMainWebView, updateChatMutedState, openQuickPreview } = getActions();
|
||||
const lang = useLang();
|
||||
|
||||
@ -85,6 +87,7 @@ const LeftSearchResultChat: FC<OwnProps & StateProps> = ({
|
||||
isPinned,
|
||||
isMuted,
|
||||
canChangeFolder,
|
||||
topicIds: listedTopicIds,
|
||||
handleMute,
|
||||
handleUnmute,
|
||||
handleChatFolderChange,
|
||||
@ -186,12 +189,15 @@ export default memo(withGlobal<OwnProps>(
|
||||
const isPinned = selectIsChatPinned(global, chatId);
|
||||
const isMuted = chat && getIsChatMuted(chat, selectNotifyDefaults(global), selectNotifyException(global, chat.id));
|
||||
|
||||
const listedTopicIds = selectTopicsInfo(global, chatId)?.listedTopicIds;
|
||||
|
||||
return {
|
||||
chat,
|
||||
user,
|
||||
isPinned,
|
||||
isMuted,
|
||||
canChangeFolder: Boolean(global.chatFolders.orderedIds?.length),
|
||||
listedTopicIds,
|
||||
};
|
||||
},
|
||||
)(LeftSearchResultChat));
|
||||
|
||||
@ -4,19 +4,21 @@ import {
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type {
|
||||
ApiBusinessIntro, ApiSticker, ApiUpdateConnectionStateType, ApiUser,
|
||||
} from '../../api/types';
|
||||
import type { MessageList } from '../../types';
|
||||
import {
|
||||
type ApiBusinessIntro, type ApiSticker, type ApiUpdateConnectionStateType, type ApiUser,
|
||||
MAIN_THREAD_ID,
|
||||
} from '../../api/types';
|
||||
|
||||
import { getUserFullName } from '../../global/helpers';
|
||||
import {
|
||||
selectChat,
|
||||
selectChatLastMessage,
|
||||
selectChatLastMessageId,
|
||||
selectCurrentMessageList,
|
||||
selectUser,
|
||||
selectUserFullInfo,
|
||||
} from '../../global/selectors';
|
||||
import { selectThreadReadState } from '../../global/selectors/threads';
|
||||
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
@ -31,7 +33,7 @@ type OwnProps = {
|
||||
|
||||
type StateProps = {
|
||||
defaultStickers?: ApiSticker[];
|
||||
lastUnreadMessageId?: number;
|
||||
lastMessageId?: number;
|
||||
connectionState?: ApiUpdateConnectionStateType;
|
||||
currentMessageList?: MessageList;
|
||||
businessIntro?: ApiBusinessIntro;
|
||||
@ -41,7 +43,7 @@ type StateProps = {
|
||||
const ContactGreeting: FC<OwnProps & StateProps> = ({
|
||||
defaultStickers,
|
||||
connectionState,
|
||||
lastUnreadMessageId,
|
||||
lastMessageId,
|
||||
currentMessageList,
|
||||
businessIntro,
|
||||
user,
|
||||
@ -52,7 +54,7 @@ const ContactGreeting: FC<OwnProps & StateProps> = ({
|
||||
markMessageListRead,
|
||||
} = getActions();
|
||||
|
||||
const lang = useOldLang();
|
||||
const oldLang = useOldLang();
|
||||
|
||||
const containerRef = useRef<HTMLDivElement>();
|
||||
|
||||
@ -73,10 +75,10 @@ const ContactGreeting: FC<OwnProps & StateProps> = ({
|
||||
}, [connectionState, loadGreetingStickers, defaultStickers]);
|
||||
|
||||
useEffect(() => {
|
||||
if (connectionState === 'connectionStateReady' && lastUnreadMessageId) {
|
||||
markMessageListRead({ maxId: lastUnreadMessageId });
|
||||
if (connectionState === 'connectionStateReady' && lastMessageId) {
|
||||
markMessageListRead({ maxId: lastMessageId });
|
||||
}
|
||||
}, [connectionState, markMessageListRead, lastUnreadMessageId]);
|
||||
}, [connectionState, lastMessageId]);
|
||||
|
||||
const handleStickerSelect = useLastCallback(() => {
|
||||
if (!currentMessageList) {
|
||||
@ -92,8 +94,8 @@ const ContactGreeting: FC<OwnProps & StateProps> = ({
|
||||
});
|
||||
});
|
||||
|
||||
const title = businessIntro?.title || lang('Conversation.EmptyPlaceholder');
|
||||
const description = businessIntro?.description || lang('Conversation.GreetingText');
|
||||
const title = businessIntro?.title || oldLang('Conversation.EmptyPlaceholder');
|
||||
const description = businessIntro?.description || oldLang('Conversation.GreetingText');
|
||||
|
||||
return (
|
||||
<div className={styles.root}>
|
||||
@ -114,7 +116,7 @@ const ContactGreeting: FC<OwnProps & StateProps> = ({
|
||||
</div>
|
||||
{businessIntro && (
|
||||
<div className={styles.explainer}>
|
||||
{lang('Chat.EmptyStateIntroFooter', getUserFullName(user))}
|
||||
{oldLang('Chat.EmptyStateIntroFooter', getUserFullName(user))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
@ -131,14 +133,16 @@ export default memo(withGlobal<OwnProps>(
|
||||
|
||||
const user = selectUser(global, userId);
|
||||
const fullInfo = selectUserFullInfo(global, userId);
|
||||
const {
|
||||
unreadCount,
|
||||
} = selectThreadReadState(global, chat.id, MAIN_THREAD_ID) || {};
|
||||
|
||||
const lastMessage = selectChatLastMessage(global, chat.id);
|
||||
// Pass last message id only if there are unread messages
|
||||
const lastMessageId = selectChatLastMessageId(global, chat.id);
|
||||
|
||||
return {
|
||||
defaultStickers: stickers,
|
||||
lastUnreadMessageId: lastMessage && lastMessage.id !== chat.lastReadInboxMessageId
|
||||
? lastMessage.id
|
||||
: undefined,
|
||||
lastMessageId: unreadCount ? lastMessageId : undefined,
|
||||
connectionState: global.connectionState,
|
||||
currentMessageList: selectCurrentMessageList(global),
|
||||
businessIntro: fullInfo?.businessIntro,
|
||||
|
||||
@ -1,11 +1,10 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import { memo, useEffect, useRef } from '../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type { MessageListType, ThreadId } from '../../types';
|
||||
import { MAIN_THREAD_ID } from '../../api/types';
|
||||
import type { MessageListType, ThreadId, ThreadReadState } from '../../types';
|
||||
|
||||
import { selectChat, selectCurrentMessageList, selectCurrentMiddleSearch } from '../../global/selectors';
|
||||
import { selectThreadReadState } from '../../global/selectors/threads';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
@ -24,35 +23,33 @@ type StateProps = {
|
||||
chatId?: string;
|
||||
messageListType?: MessageListType;
|
||||
threadId?: ThreadId;
|
||||
unreadCount?: number;
|
||||
unreadReactions?: number[];
|
||||
unreadMentions?: number[];
|
||||
reactionsCount?: number;
|
||||
mentionsCount?: number;
|
||||
threadReadState?: ThreadReadState;
|
||||
shouldShowCount?: boolean;
|
||||
};
|
||||
|
||||
const FloatingActionButtons: FC<OwnProps & StateProps> = ({
|
||||
const FloatingActionButtons = ({
|
||||
withScrollDown,
|
||||
canPost,
|
||||
messageListType,
|
||||
chatId,
|
||||
threadId,
|
||||
unreadCount,
|
||||
unreadReactions,
|
||||
unreadMentions,
|
||||
reactionsCount,
|
||||
mentionsCount,
|
||||
withExtraShift,
|
||||
}) => {
|
||||
threadReadState,
|
||||
shouldShowCount,
|
||||
}: OwnProps & StateProps) => {
|
||||
const {
|
||||
focusNextReply, focusNextReaction, focusNextMention, fetchUnreadReactions,
|
||||
readAllMentions, readAllReactions, fetchUnreadMentions, scrollMessageListToBottom,
|
||||
focusNextReply, focusNextReaction, focusNextMention, loadUnreadReactions,
|
||||
readAllMentions, readAllReactions, loadUnreadMentions, scrollMessageListToBottom,
|
||||
} = getActions();
|
||||
|
||||
const elementRef = useRef<HTMLDivElement>();
|
||||
|
||||
const hasUnreadReactions = Boolean(reactionsCount);
|
||||
const hasUnreadMentions = Boolean(mentionsCount);
|
||||
const {
|
||||
unreadReactionsCount, unreadMentionsCount, unreadCount, unreadReactions, unreadMentions,
|
||||
} = (shouldShowCount && threadReadState) || {};
|
||||
|
||||
const hasUnreadReactions = Boolean(unreadReactionsCount);
|
||||
const hasUnreadMentions = Boolean(unreadMentionsCount);
|
||||
|
||||
const handleReadAllReactions = useLastCallback(() => {
|
||||
if (!chatId) return;
|
||||
@ -66,27 +63,15 @@ const FloatingActionButtons: FC<OwnProps & StateProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (hasUnreadReactions && chatId && !unreadReactions?.length) {
|
||||
fetchUnreadReactions({ chatId });
|
||||
loadUnreadReactions({ chatId, threadId });
|
||||
}
|
||||
}, [chatId, fetchUnreadReactions, hasUnreadReactions, unreadReactions?.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasUnreadReactions && chatId) {
|
||||
fetchUnreadReactions({ chatId });
|
||||
}
|
||||
}, [chatId, fetchUnreadReactions, hasUnreadReactions]);
|
||||
}, [chatId, threadId, hasUnreadReactions, unreadReactions?.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasUnreadMentions && chatId && !unreadMentions?.length) {
|
||||
fetchUnreadMentions({ chatId });
|
||||
loadUnreadMentions({ chatId, threadId });
|
||||
}
|
||||
}, [chatId, fetchUnreadMentions, hasUnreadMentions, unreadMentions?.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (hasUnreadMentions && chatId) {
|
||||
fetchUnreadMentions({ chatId });
|
||||
}
|
||||
}, [chatId, fetchUnreadMentions, hasUnreadMentions]);
|
||||
}, [chatId, threadId, hasUnreadMentions, unreadMentions?.length]);
|
||||
|
||||
const handleScrollDownClick = useLastCallback(() => {
|
||||
if (!withScrollDown) {
|
||||
@ -100,10 +85,20 @@ const FloatingActionButtons: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
});
|
||||
|
||||
const handleFocusNextReaction = useLastCallback(() => {
|
||||
if (!chatId) return;
|
||||
focusNextReaction({ chatId, threadId });
|
||||
});
|
||||
|
||||
const handleFocusNextMention = useLastCallback(() => {
|
||||
if (!chatId) return;
|
||||
focusNextMention({ chatId, threadId });
|
||||
});
|
||||
|
||||
const fabClassName = buildClassName(
|
||||
styles.root,
|
||||
(withScrollDown || Boolean(reactionsCount) || Boolean(mentionsCount)) && styles.revealed,
|
||||
(Boolean(reactionsCount) || Boolean(mentionsCount)) && !withScrollDown && styles.hideScrollDown,
|
||||
(withScrollDown || hasUnreadReactions || hasUnreadMentions) && styles.revealed,
|
||||
(hasUnreadReactions || hasUnreadMentions) && !withScrollDown && styles.hideScrollDown,
|
||||
!canPost && styles.noComposer,
|
||||
!withExtraShift && styles.noExtraShift,
|
||||
);
|
||||
@ -113,9 +108,9 @@ const FloatingActionButtons: FC<OwnProps & StateProps> = ({
|
||||
<ScrollDownButton
|
||||
icon="heart-outline"
|
||||
ariaLabelLang="AccDescrReactionMentionDown"
|
||||
onClick={focusNextReaction}
|
||||
onClick={handleFocusNextReaction}
|
||||
onReadAll={handleReadAllReactions}
|
||||
unreadCount={reactionsCount}
|
||||
unreadCount={unreadReactionsCount}
|
||||
className={buildClassName(
|
||||
styles.reactions,
|
||||
!hasUnreadReactions && styles.hidden,
|
||||
@ -126,9 +121,9 @@ const FloatingActionButtons: FC<OwnProps & StateProps> = ({
|
||||
<ScrollDownButton
|
||||
icon="mention"
|
||||
ariaLabelLang="AccDescrMentionDown"
|
||||
onClick={focusNextMention}
|
||||
onClick={handleFocusNextMention}
|
||||
onReadAll={handleReadAllMentions}
|
||||
unreadCount={mentionsCount}
|
||||
unreadCount={unreadMentionsCount}
|
||||
className={!hasUnreadMentions && styles.hidden}
|
||||
/>
|
||||
|
||||
@ -154,18 +149,14 @@ export default memo(withGlobal<OwnProps>(
|
||||
const chat = selectChat(global, chatId);
|
||||
const hasActiveMiddleSearch = Boolean(selectCurrentMiddleSearch(global));
|
||||
|
||||
const shouldShowCount = chat && threadId === MAIN_THREAD_ID && messageListType === 'thread'
|
||||
&& !hasActiveMiddleSearch;
|
||||
const shouldShowCount = chat && messageListType === 'thread' && !hasActiveMiddleSearch;
|
||||
|
||||
return {
|
||||
messageListType,
|
||||
chatId,
|
||||
threadId,
|
||||
reactionsCount: shouldShowCount ? chat.unreadReactionsCount : undefined,
|
||||
unreadReactions: shouldShowCount ? chat.unreadReactions : undefined,
|
||||
unreadMentions: shouldShowCount ? chat.unreadMentions : undefined,
|
||||
mentionsCount: shouldShowCount ? chat.unreadMentionsCount : undefined,
|
||||
unreadCount: shouldShowCount ? chat.unreadCount : undefined,
|
||||
threadReadState: selectThreadReadState(global, chatId, threadId),
|
||||
shouldShowCount,
|
||||
};
|
||||
},
|
||||
)(FloatingActionButtons));
|
||||
|
||||
@ -40,18 +40,21 @@ import {
|
||||
selectIsCurrentUserPremium,
|
||||
selectIsInSelectMode,
|
||||
selectIsViewportNewest,
|
||||
selectLastScrollOffset,
|
||||
selectMonoforumChannel,
|
||||
selectPerformanceSettingsValue,
|
||||
selectScrollOffset,
|
||||
selectTabState,
|
||||
selectThreadInfo,
|
||||
selectTopic,
|
||||
selectTranslationLanguage,
|
||||
selectUserFullInfo,
|
||||
} from '../../global/selectors';
|
||||
import { selectIsChatRestricted } from '../../global/selectors/chats';
|
||||
import { selectActiveRestrictionReasons, selectCurrentMessageList } from '../../global/selectors/messages';
|
||||
import {
|
||||
selectLastScrollOffset,
|
||||
selectScrollOffset,
|
||||
selectThreadInfo,
|
||||
selectThreadReadState,
|
||||
} from '../../global/selectors/threads';
|
||||
import animateScroll, { isAnimatingScroll, restartCurrentScrollAnimation } from '../../util/animateScroll';
|
||||
import { IS_FIREFOX } from '../../util/browser/windowEnvironment';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
@ -907,6 +910,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
const currentUserId = global.currentUserId!;
|
||||
const chat = selectChat(global, chatId);
|
||||
const userFullInfo = selectUserFullInfo(global, chatId);
|
||||
const readState = selectThreadReadState(global, chatId, threadId);
|
||||
if (!chat) {
|
||||
return { currentUserId } as Complete<StateProps>;
|
||||
}
|
||||
@ -932,7 +936,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
|
||||
const withLastMessageWhenPreloading = (
|
||||
threadId === MAIN_THREAD_ID
|
||||
&& !messageIds && !chat.unreadCount && !focusingId && lastMessage && !lastMessage.groupedId
|
||||
&& !messageIds && readState && !readState.unreadCount && !focusingId && lastMessage && !lastMessage.groupedId
|
||||
);
|
||||
|
||||
const chatBot = selectBot(global, chatId);
|
||||
|
||||
@ -37,8 +37,6 @@ import {
|
||||
selectChatFullInfo,
|
||||
selectCurrentMessageList,
|
||||
selectCurrentMiddleSearch,
|
||||
selectDraft,
|
||||
selectEditingId,
|
||||
selectIsChatBotNotStarted,
|
||||
selectIsCurrentUserFrozen,
|
||||
selectIsInSelectMode,
|
||||
@ -50,12 +48,12 @@ import {
|
||||
selectTabState,
|
||||
selectTheme,
|
||||
selectThemeValues,
|
||||
selectThreadInfo,
|
||||
selectTopic,
|
||||
selectTopics,
|
||||
selectUserFullInfo,
|
||||
} from '../../global/selectors';
|
||||
import { selectSharedSettings } from '../../global/selectors/sharedState';
|
||||
import { selectDraft, selectEditingId, selectThreadInfo } from '../../global/selectors/threads';
|
||||
import { IS_TAURI } from '../../util/browser/globalEnvironment';
|
||||
import {
|
||||
IS_ANDROID, IS_IOS, IS_MAC_OS, IS_SAFARI, IS_TRANSLATION_SUPPORTED, MASK_IMAGE_DISABLED,
|
||||
|
||||
@ -31,9 +31,8 @@ import {
|
||||
selectPinnedIds,
|
||||
selectScheduledIds,
|
||||
selectTabState,
|
||||
selectThreadInfo,
|
||||
selectThreadParam,
|
||||
} from '../../global/selectors';
|
||||
import { selectThreadInfo, selectThreadLocalStateParam } from '../../global/selectors/threads';
|
||||
import { IS_TAURI } from '../../util/browser/globalEnvironment';
|
||||
import { IS_MAC_OS } from '../../util/browser/windowEnvironment';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
@ -400,7 +399,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
messagesCount = threadInfo?.messagesCount || 0;
|
||||
}
|
||||
|
||||
const typingStatus = selectThreadParam(global, chatId, threadId, 'typingStatus');
|
||||
const typingStatus = selectThreadLocalStateParam(global, chatId, threadId, 'typingStatus');
|
||||
|
||||
const emojiStatus = peer?.emojiStatus;
|
||||
const emojiStatusSticker = emojiStatus && selectCustomEmoji(global, emojiStatus.documentId);
|
||||
|
||||
@ -13,10 +13,7 @@ import {
|
||||
selectCanAnimateInterface,
|
||||
selectChat,
|
||||
selectChatMessage,
|
||||
selectDraft,
|
||||
selectEditingId,
|
||||
selectEditingMessage,
|
||||
selectEditingScheduledId,
|
||||
selectForwardedSender,
|
||||
selectIsChatWithSelf,
|
||||
selectIsCurrentUserPremium,
|
||||
@ -25,6 +22,7 @@ import {
|
||||
selectTheme,
|
||||
} from '../../../global/selectors';
|
||||
import { selectIsMediaNsfw } from '../../../global/selectors/media';
|
||||
import { selectDraft, selectEditingId, selectEditingScheduledId } from '../../../global/selectors/threads';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
||||
import { unique } from '../../../util/iteratees';
|
||||
|
||||
@ -16,8 +16,9 @@ import type { Signal } from '../../../util/signals';
|
||||
|
||||
import { EDITABLE_INPUT_ID, EDITABLE_INPUT_MODAL_ID } from '../../../config';
|
||||
import { requestForcedReflow, requestMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
import { selectCanPlayAnimatedEmojis, selectDraft, selectIsInSelectMode } from '../../../global/selectors';
|
||||
import { selectCanPlayAnimatedEmojis, selectIsInSelectMode } from '../../../global/selectors';
|
||||
import { selectSharedSettings } from '../../../global/selectors/sharedState';
|
||||
import { selectDraft } from '../../../global/selectors/threads';
|
||||
import { IS_TAURI } from '../../../util/browser/globalEnvironment';
|
||||
import {
|
||||
IS_ANDROID, IS_EMOJI_SUPPORTED, IS_IOS, IS_TOUCH_ENV,
|
||||
|
||||
@ -16,7 +16,8 @@ import {
|
||||
getWebPagePhoto,
|
||||
getWebPageVideo,
|
||||
} from '../../../global/helpers';
|
||||
import { selectNoWebPage, selectTabState, selectWebPage } from '../../../global/selectors';
|
||||
import { selectTabState, selectWebPage } from '../../../global/selectors';
|
||||
import { selectNoWebPage } from '../../../global/selectors/threads';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
|
||||
import useThumbnail from '../../../hooks/media/useThumbnail';
|
||||
|
||||
@ -34,6 +34,8 @@ export default function useMessageObservers(
|
||||
} = useIntersectionObserver({
|
||||
rootRef: containerRef,
|
||||
throttleMs: INTERSECTION_THROTTLE_FOR_READING,
|
||||
// `memoFirstUnreadIdRef` is set after the first render, firing callback before that can skip some entries, like the last message
|
||||
shouldSkipFirst: true,
|
||||
}, (entries) => {
|
||||
if (type !== 'thread' || isBackgroundModeActive()) {
|
||||
return;
|
||||
@ -97,7 +99,7 @@ export default function useMessageObservers(
|
||||
}
|
||||
|
||||
if (reactionIds.length) {
|
||||
animateUnreadReaction({ messageIds: reactionIds });
|
||||
animateUnreadReaction({ chatId, messageIds: reactionIds });
|
||||
}
|
||||
|
||||
if (viewportPinnedIdsToAdd.length || viewportPinnedIdsToRemove.length) {
|
||||
|
||||
@ -34,6 +34,7 @@ import {
|
||||
selectSender,
|
||||
selectTabState,
|
||||
} from '../../../global/selectors';
|
||||
import { selectThreadReadState } from '../../../global/selectors/threads';
|
||||
import { IS_TAURI } from '../../../util/browser/globalEnvironment';
|
||||
import { IS_ANDROID, IS_FLUID_BACKGROUND_SUPPORTED } from '../../../util/browser/windowEnvironment';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
@ -295,7 +296,7 @@ const ActionMessage = ({
|
||||
if (!bottomMarker || !isElementInViewport(bottomMarker)) return;
|
||||
|
||||
if (hasUnreadReaction) {
|
||||
animateUnreadReaction({ messageIds: [id] });
|
||||
animateUnreadReaction({ chatId, messageIds: [id] });
|
||||
}
|
||||
|
||||
if (message.hasUnreadMention) {
|
||||
@ -672,7 +673,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
|
||||
const isCurrentUserPremium = selectIsCurrentUserPremium(global);
|
||||
|
||||
const hasUnreadReaction = chat?.unreadReactions?.includes(message.id);
|
||||
const readState = selectThreadReadState(global, message.chatId, threadId);
|
||||
const hasUnreadReaction = readState?.unreadReactions?.includes(message.id);
|
||||
const isAccountFrozen = selectIsCurrentUserFrozen(global);
|
||||
|
||||
return {
|
||||
|
||||
@ -23,9 +23,9 @@ import {
|
||||
selectMonoforumChannel,
|
||||
selectPeer,
|
||||
selectSender,
|
||||
selectThreadIdFromMessage,
|
||||
selectTopic,
|
||||
} from '../../../global/selectors';
|
||||
import { selectThreadIdFromMessage } from '../../../global/selectors/threads';
|
||||
import { ensureProtocol } from '../../../util/browser/url';
|
||||
import { formatDateTimeToString, formatScheduledDateTime, formatShortDuration } from '../../../util/dates/dateFormat';
|
||||
import { formatCurrency } from '../../../util/formatCurrency';
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import { memo, useMemo } from '../../../lib/teact/teact';
|
||||
import { getActions, getGlobal } from '../../../global';
|
||||
|
||||
import type { ApiCommentsInfo } from '../../../api/types';
|
||||
import type { ThreadReadState } from '../../../types';
|
||||
|
||||
import { selectIsCurrentUserFrozen, selectPeer } from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
@ -22,6 +22,7 @@ import './CommentButton.scss';
|
||||
|
||||
type OwnProps = {
|
||||
threadInfo?: ApiCommentsInfo;
|
||||
threadReadState?: ThreadReadState;
|
||||
disabled?: boolean;
|
||||
isLoading?: boolean;
|
||||
isCustomShape?: boolean;
|
||||
@ -29,12 +30,13 @@ type OwnProps = {
|
||||
|
||||
const SHOW_LOADER_DELAY = 450;
|
||||
|
||||
const CommentButton: FC<OwnProps> = ({
|
||||
const CommentButton = ({
|
||||
isCustomShape,
|
||||
threadInfo,
|
||||
threadReadState,
|
||||
disabled,
|
||||
isLoading,
|
||||
}) => {
|
||||
}: OwnProps) => {
|
||||
const { openThread, openFrozenAccountModal } = getActions();
|
||||
|
||||
const shouldRenderLoading = useAsyncRendering([isLoading], SHOW_LOADER_DELAY);
|
||||
@ -42,8 +44,9 @@ const CommentButton: FC<OwnProps> = ({
|
||||
const oldLang = useOldLang();
|
||||
const lang = useLang();
|
||||
const {
|
||||
originMessageId, chatId, messagesCount, lastMessageId, lastReadInboxMessageId, recentReplierIds, originChannelId,
|
||||
originMessageId, chatId, messagesCount, lastMessageId, recentReplierIds, originChannelId,
|
||||
} = threadInfo || {};
|
||||
const { lastReadInboxMessageId } = threadReadState || {};
|
||||
|
||||
const handleClick = useLastCallback(() => {
|
||||
const global = getGlobal();
|
||||
|
||||
@ -65,15 +65,14 @@ import {
|
||||
selectPollFromMessage,
|
||||
selectRequestedChatTranslationLanguage,
|
||||
selectRequestedMessageTranslationLanguage,
|
||||
selectSavedDialogIdFromMessage,
|
||||
selectStickerSet,
|
||||
selectThreadInfo,
|
||||
selectTopic,
|
||||
selectUser,
|
||||
selectUserStatus,
|
||||
selectWebPageFromMessage,
|
||||
} from '../../../global/selectors';
|
||||
import { selectMessageDownloadableMedia } from '../../../global/selectors/media';
|
||||
import { selectSavedDialogIdFromMessage, selectThreadInfo } from '../../../global/selectors/threads';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { copyTextToClipboard } from '../../../util/clipboard';
|
||||
import { isUserId } from '../../../util/entities/ids';
|
||||
@ -837,7 +836,9 @@ export default memo(withGlobal<OwnProps>(
|
||||
const isOwn = isOwnMessage(message);
|
||||
const chatBot = chat && selectBot(global, chat.id);
|
||||
const isBot = Boolean(chatBot);
|
||||
const isMessageUnread = selectIsMessageUnread(global, message);
|
||||
const isMessageUnread = selectIsMessageUnread(
|
||||
global, message.chatId, threadId || MAIN_THREAD_ID, message.id, messageListType,
|
||||
);
|
||||
const canLoadReadDate = Boolean(
|
||||
isPrivate
|
||||
&& isOwn
|
||||
|
||||
@ -39,6 +39,7 @@ import type {
|
||||
TextSummary,
|
||||
ThemeKey,
|
||||
ThreadId,
|
||||
ThreadReadState,
|
||||
} from '../../../types';
|
||||
import type { Signal } from '../../../util/signals';
|
||||
import type { OnIntersectPinnedMessage } from '../hooks/usePinnedMessage';
|
||||
@ -112,7 +113,6 @@ import {
|
||||
selectShouldLoopStickers,
|
||||
selectTabState,
|
||||
selectTheme,
|
||||
selectThreadInfo,
|
||||
selectTopicFromMessage,
|
||||
selectUploadProgress,
|
||||
selectUser,
|
||||
@ -124,6 +124,7 @@ import {
|
||||
selectMessageTimestampableDuration,
|
||||
} from '../../../global/selectors/media';
|
||||
import { selectSharedSettings } from '../../../global/selectors/sharedState';
|
||||
import { selectThread, selectThreadReadState } from '../../../global/selectors/threads';
|
||||
import { IS_TAURI } from '../../../util/browser/globalEnvironment';
|
||||
import { IS_ANDROID, IS_TRANSLATION_SUPPORTED } from '../../../util/browser/windowEnvironment';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
@ -293,6 +294,7 @@ type StateProps = {
|
||||
shouldLoopStickers?: boolean;
|
||||
autoLoadFileMaxSizeMb: number;
|
||||
repliesThreadInfo?: ApiThreadInfo;
|
||||
repliesReadState?: ThreadReadState;
|
||||
reactionMessage?: ApiMessage;
|
||||
availableReactions?: ApiAvailableReaction[];
|
||||
defaultReaction?: ApiReaction;
|
||||
@ -428,6 +430,7 @@ const Message = ({
|
||||
shouldLoopStickers,
|
||||
autoLoadFileMaxSizeMb,
|
||||
repliesThreadInfo,
|
||||
repliesReadState,
|
||||
hasUnreadReaction,
|
||||
memoFirstUnreadIdRef,
|
||||
senderAdminMember,
|
||||
@ -839,6 +842,7 @@ const Message = ({
|
||||
const phoneCall = action?.type === 'phoneCall' ? action : undefined;
|
||||
|
||||
const commentsThreadInfo = repliesThreadInfo?.isCommentsInfo ? repliesThreadInfo : undefined;
|
||||
const commentsReadState = repliesThreadInfo?.isCommentsInfo ? repliesReadState : undefined;
|
||||
const isLocalWithCommentButton = hasLinkedChat && isChannel && isLocal;
|
||||
|
||||
const isMediaWithCommentButton = (commentsThreadInfo || isLocalWithCommentButton)
|
||||
@ -872,7 +876,8 @@ const Message = ({
|
||||
asForwarded,
|
||||
hasThread: hasThread && !noComments,
|
||||
forceSenderName,
|
||||
hasCommentCounter: hasThread && repliesThreadInfo.messagesCount > 0,
|
||||
hasCommentCounter: hasThread && repliesThreadInfo.messagesCount !== undefined
|
||||
&& repliesThreadInfo.messagesCount > 0,
|
||||
hasBottomCommentButton: withCommentButton && !isCustomShape,
|
||||
hasActionButton: canForward || canFocus || (withCommentButton && isCustomShape),
|
||||
hasReactions,
|
||||
@ -955,7 +960,7 @@ const Message = ({
|
||||
if (!bottomMarker || !isElementInViewport(bottomMarker)) return;
|
||||
|
||||
if (hasUnreadReaction) {
|
||||
animateUnreadReaction({ messageIds: [messageId] });
|
||||
animateUnreadReaction({ chatId, messageIds: [messageId] });
|
||||
}
|
||||
|
||||
let unreadMentionIds: number[] = [];
|
||||
@ -1889,6 +1894,7 @@ const Message = ({
|
||||
{withCommentButton && isCustomShape && (
|
||||
<CommentButton
|
||||
threadInfo={commentsThreadInfo}
|
||||
threadReadState={commentsReadState}
|
||||
disabled={noComments || !commentsThreadInfo}
|
||||
isLoading={isLoadingComments}
|
||||
isCustomShape
|
||||
@ -1920,6 +1926,7 @@ const Message = ({
|
||||
{withCommentButton && !isCustomShape && (
|
||||
<CommentButton
|
||||
threadInfo={commentsThreadInfo}
|
||||
threadReadState={commentsReadState}
|
||||
disabled={noComments || !commentsThreadInfo}
|
||||
isLoading={isLoadingComments}
|
||||
/>
|
||||
@ -2086,7 +2093,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
const downloadableMedia = selectMessageDownloadableMedia(global, message);
|
||||
const isDownloading = downloadableMedia && getIsDownloading(activeDownloads, downloadableMedia);
|
||||
|
||||
const repliesThreadInfo = selectThreadInfo(global, chatId, album?.commentsMessage?.id || id);
|
||||
const repliesThread = selectThread(global, chatId, album?.commentsMessage?.id || id);
|
||||
const { threadInfo: repliesThreadInfo, readState: repliesReadState } = repliesThread || {};
|
||||
|
||||
const isInDocumentGroup = Boolean(message.groupedId) && !message.isInAlbum;
|
||||
const documentGroupFirstMessageId = isInDocumentGroup
|
||||
@ -2096,7 +2104,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
isLastInDocumentGroup ? selectChatMessage(global, chatId, documentGroupFirstMessageId!) : undefined
|
||||
) : message;
|
||||
|
||||
const hasUnreadReaction = chat?.unreadReactions?.includes(message.id);
|
||||
const readState = selectThreadReadState(global, chatId, threadId);
|
||||
const hasUnreadReaction = readState?.unreadReactions?.includes(message.id);
|
||||
|
||||
const hasTopicChip = threadId === MAIN_THREAD_ID && chat?.isForum && !chat.isBotForum && isFirstInGroup;
|
||||
const messageTopic = selectTopicFromMessage(global, message);
|
||||
@ -2187,6 +2196,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
autoLoadFileMaxSizeMb: global.settings.byKey.autoLoadFileMaxSizeMb,
|
||||
shouldLoopStickers: selectShouldLoopStickers(global),
|
||||
repliesThreadInfo,
|
||||
repliesReadState,
|
||||
availableReactions: global.reactions.availableReactions,
|
||||
defaultReaction: isMessageLocal(message) || messageListType === 'scheduled'
|
||||
? undefined : selectDefaultReaction(global, chatId),
|
||||
@ -2212,7 +2222,9 @@ export default memo(withGlobal<OwnProps>(
|
||||
&& loadingThread?.loadingChatId === repliesThreadInfo?.originChannelId
|
||||
&& loadingThread?.loadingMessageId === repliesThreadInfo?.originMessageId,
|
||||
shouldWarnAboutFiles,
|
||||
outgoingStatus: isOutgoing ? selectOutgoingStatus(global, message, messageListType === 'scheduled') : undefined,
|
||||
outgoingStatus: isOutgoing
|
||||
? selectOutgoingStatus(global, chatId, threadId, message.id, messageListType)
|
||||
: undefined,
|
||||
uploadProgress: typeof uploadProgress === 'number' ? uploadProgress : undefined,
|
||||
focusDirection: isFocused ? focusDirection : undefined,
|
||||
noFocusHighlight: isFocused ? noFocusHighlight : undefined,
|
||||
|
||||
@ -191,7 +191,7 @@ export default function useInnerHandlers({
|
||||
});
|
||||
|
||||
const handleReadMedia = useLastCallback((): void => {
|
||||
markMessagesRead({ messageIds: [messageId] });
|
||||
markMessagesRead({ chatId, messageIds: [messageId] });
|
||||
});
|
||||
|
||||
const handleCancelUpload = useLastCallback(() => {
|
||||
|
||||
@ -7,7 +7,8 @@ import type { ThreadId } from '../../../types';
|
||||
import { MAIN_THREAD_ID } from '../../../api/types';
|
||||
|
||||
import { getIsSavedDialog } from '../../../global/helpers';
|
||||
import { selectChat, selectThreadParam, selectTopic } from '../../../global/selectors';
|
||||
import { selectChat } from '../../../global/selectors';
|
||||
import { selectThreadLocalStateParam, selectThreadReadState } from '../../../global/selectors/threads';
|
||||
import { isUserId } from '../../../util/entities/ids';
|
||||
|
||||
import useConnectionStatus from '../../../hooks/useConnectionStatus';
|
||||
@ -75,7 +76,7 @@ const QuickPreviewModalHeader: FC<OwnProps & StateProps> = ({
|
||||
round
|
||||
color="translucent"
|
||||
size="smaller"
|
||||
ariaLabel={lang('ChatListContextMaskAsRead')}
|
||||
ariaLabel={lang('ChatListContextMarkAsRead')}
|
||||
onClick={handleMarkAsRead}
|
||||
className={styles.markAsReadButton}
|
||||
iconName="readchats"
|
||||
@ -134,11 +135,10 @@ const QuickPreviewModalHeader: FC<OwnProps & StateProps> = ({
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { chatId, threadId }): Complete<StateProps> => {
|
||||
const chat = selectChat(global, chatId);
|
||||
const typingStatus = selectThreadParam(global, chatId, threadId || MAIN_THREAD_ID, 'typingStatus');
|
||||
const typingStatus = selectThreadLocalStateParam(global, chatId, threadId || MAIN_THREAD_ID, 'typingStatus');
|
||||
const isSavedDialog = getIsSavedDialog(chatId, threadId || MAIN_THREAD_ID, global.currentUserId);
|
||||
const unreadCount = chat?.isForum && threadId
|
||||
? selectTopic(global, chatId, threadId)?.unreadCount
|
||||
: chat?.unreadCount;
|
||||
const readState = selectThreadReadState(global, chatId, threadId || MAIN_THREAD_ID);
|
||||
const unreadCount = readState?.unreadCount;
|
||||
|
||||
return {
|
||||
chat,
|
||||
@ -148,7 +148,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
typingStatus,
|
||||
isSavedDialog,
|
||||
unreadCount,
|
||||
hasUnreadMark: chat?.hasUnreadMark,
|
||||
hasUnreadMark: readState?.hasUnreadMark,
|
||||
};
|
||||
},
|
||||
)(QuickPreviewModalHeader));
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import { memo, useMemo } from '../../../../lib/teact/teact';
|
||||
import { memo, useCallback, useMemo } from '../../../../lib/teact/teact';
|
||||
import { getActions } from '../../../../global';
|
||||
|
||||
import type {
|
||||
@ -42,12 +42,6 @@ type OwnProps = {
|
||||
|
||||
const GIFT_STICKER_SIZE = 36;
|
||||
|
||||
function selectOptionalPeer(peerId?: string) {
|
||||
return (global: GlobalState) => (
|
||||
peerId ? selectPeer(global, peerId) : undefined
|
||||
);
|
||||
}
|
||||
|
||||
const StarsTransactionItem = ({ transaction, className }: OwnProps) => {
|
||||
const { openStarsTransactionModal } = getActions();
|
||||
const {
|
||||
@ -62,7 +56,11 @@ const StarsTransactionItem = ({ transaction, className }: OwnProps) => {
|
||||
const oldLang = useOldLang();
|
||||
|
||||
const peerId = transactionPeer.type === 'peer' ? transactionPeer.id : undefined;
|
||||
const peer = useSelector(selectOptionalPeer(peerId));
|
||||
|
||||
const peerSelector = useCallback((global: GlobalState) => {
|
||||
return peerId ? selectPeer(global, peerId) : undefined;
|
||||
}, [peerId]);
|
||||
const peer = useSelector(peerSelector);
|
||||
const starGift = transaction.starGift;
|
||||
const isUniqueGift = starGift?.type === 'starGiftUnique';
|
||||
const giftSticker = starGift && getStickerFromGift(starGift);
|
||||
|
||||
@ -14,7 +14,7 @@ import {
|
||||
TON_CURRENCY_CODE,
|
||||
} from '../../../config';
|
||||
import { selectIsMonoforumAdmin, selectPeer } from '../../../global/selectors';
|
||||
import { selectDraft } from '../../../global/selectors/messages';
|
||||
import { selectDraft } from '../../../global/selectors/threads';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { formatScheduledDateTime, formatShortDuration } from '../../../util/dates/dateFormat';
|
||||
import { convertTonFromNanos, convertTonToNanos } from '../../../util/formatCurrency';
|
||||
|
||||
@ -13,9 +13,10 @@ import {
|
||||
selectCurrentGifSearch,
|
||||
selectCurrentMessageList,
|
||||
selectIsChatWithBot,
|
||||
selectIsChatWithSelf, selectThreadInfo,
|
||||
selectIsChatWithSelf,
|
||||
selectTopic,
|
||||
} from '../../global/selectors';
|
||||
import { selectThreadInfo } from '../../global/selectors/threads';
|
||||
import { IS_TOUCH_ENV } from '../../util/browser/windowEnvironment';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
|
||||
|
||||
@ -56,7 +56,6 @@ import {
|
||||
selectChatMessage,
|
||||
selectCurrentChat,
|
||||
selectCurrentMessageList,
|
||||
selectDraft,
|
||||
selectIsCurrentUserFrozen,
|
||||
selectIsTrustedBot,
|
||||
selectMessageReplyInfo,
|
||||
@ -68,6 +67,7 @@ import {
|
||||
selectUserFullInfo,
|
||||
} from '../../selectors';
|
||||
import { selectSharedSettings } from '../../selectors/sharedState';
|
||||
import { selectDraft } from '../../selectors/threads.ts';
|
||||
import { fetchChatByUsername } from './chats';
|
||||
import { getPeerStarsForMessage } from './messages';
|
||||
|
||||
|
||||
@ -40,7 +40,7 @@ import { isUserId } from '../../../util/entities/ids';
|
||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||
import { getOrderedIds } from '../../../util/folderManager';
|
||||
import {
|
||||
buildCollectionByKey, omit, pick, unique,
|
||||
buildCollectionByKey, omit, unique,
|
||||
} from '../../../util/iteratees';
|
||||
import { isLocalMessageId } from '../../../util/keys/messageKey';
|
||||
import * as langProvider from '../../../util/oldLangProvider';
|
||||
@ -79,7 +79,6 @@ import {
|
||||
replaceMessages,
|
||||
replaceNotifyExceptions,
|
||||
replaceSimilarChannels,
|
||||
replaceThreadParam,
|
||||
replaceUserStatuses,
|
||||
toggleSimilarChannels,
|
||||
updateChat,
|
||||
@ -92,15 +91,22 @@ import {
|
||||
updateManagementProgress,
|
||||
updateMissingInvitedUsers,
|
||||
updatePeerFullInfo,
|
||||
updateThread,
|
||||
updateThreadInfo,
|
||||
updateTopic,
|
||||
updateTopics,
|
||||
updateTopicsInfo,
|
||||
updateTopicWithState,
|
||||
updateUser,
|
||||
updateUsers,
|
||||
} from '../../reducers';
|
||||
import { updateGroupCall } from '../../reducers/calls';
|
||||
import { updateTabState } from '../../reducers/tabs';
|
||||
import {
|
||||
replaceThreadLocalStateParam,
|
||||
replaceThreadReadStateParam,
|
||||
updateMainThreadReadStates,
|
||||
updateThreadInfo,
|
||||
updateThreadInfoLastMessageId,
|
||||
updateThreadReadState,
|
||||
} from '../../reducers/threads';
|
||||
import {
|
||||
selectChat,
|
||||
selectChatByUsername,
|
||||
@ -112,7 +118,6 @@ import {
|
||||
selectChatMessages,
|
||||
selectCurrentChat,
|
||||
selectCurrentMessageList,
|
||||
selectDraft,
|
||||
selectIsChatPinned,
|
||||
selectIsChatWithSelf,
|
||||
selectIsCurrentUserFrozen,
|
||||
@ -123,8 +128,6 @@ import {
|
||||
selectStickerSet,
|
||||
selectSupportChat,
|
||||
selectTabState,
|
||||
selectThread,
|
||||
selectThreadInfo,
|
||||
selectTopic,
|
||||
selectTopics,
|
||||
selectTopicsInfo,
|
||||
@ -133,6 +136,13 @@ import {
|
||||
} from '../../selectors';
|
||||
import { selectGroupCall } from '../../selectors/calls';
|
||||
import { selectCurrentLimit } from '../../selectors/limits';
|
||||
import {
|
||||
selectDraft,
|
||||
selectThread,
|
||||
selectThreadInfo,
|
||||
selectThreadLocalState,
|
||||
selectThreadReadState,
|
||||
} from '../../selectors/threads';
|
||||
|
||||
const TOP_CHAT_MESSAGES_PRELOAD_INTERVAL = 100;
|
||||
const INFINITE_LOOP_MARKER = 100;
|
||||
@ -227,8 +237,9 @@ addActionHandler('openChat', (global, actions, payload): ActionReturnType => {
|
||||
}
|
||||
|
||||
const chat = selectChat(global, id);
|
||||
const chatReadState = selectThreadReadState(global, id, MAIN_THREAD_ID);
|
||||
|
||||
if (chat?.hasUnreadMark) {
|
||||
if (chatReadState?.hasUnreadMark) {
|
||||
actions.markChatRead({ id });
|
||||
}
|
||||
|
||||
@ -308,14 +319,17 @@ addActionHandler('openThread', async (global, actions, payload): Promise<void> =
|
||||
|
||||
const chat = selectChat(global, loadingChatId);
|
||||
const threadInfo = selectThreadInfo(global, loadingChatId, loadingThreadId);
|
||||
const thread = selectThread(global, loadingChatId, loadingThreadId);
|
||||
const threadLocalState = selectThreadLocalState(global, loadingChatId, loadingThreadId);
|
||||
if (!chat) return;
|
||||
|
||||
abortChatRequestsForCurrentChat(global, loadingChatId, loadingThreadId, tabId);
|
||||
|
||||
if (chatId
|
||||
&& threadInfo?.threadId
|
||||
&& (isComments || (thread?.listedIds?.length && thread.listedIds.includes(Number(threadInfo.threadId))))) {
|
||||
&& (
|
||||
isComments
|
||||
|| (threadLocalState?.listedIds?.length && threadLocalState.listedIds.includes(Number(threadInfo.threadId)))
|
||||
)) {
|
||||
global = updateTabState(global, {
|
||||
loadingThread: undefined,
|
||||
}, tabId);
|
||||
@ -407,29 +421,23 @@ addActionHandler('openThread', async (global, actions, payload): Promise<void> =
|
||||
|
||||
global = getGlobal();
|
||||
global = addMessages(global, result.messages);
|
||||
if (isComments) {
|
||||
global = updateThreadInfo(global, loadingChatId, loadingThreadId, {
|
||||
threadId,
|
||||
});
|
||||
global = updateThreadReadState(global, chatId, result.threadId, result.threadReadState);
|
||||
global = updateThreadInfoLastMessageId(global, chatId, result.threadId, result.lastMessageId);
|
||||
|
||||
if (isComments) {
|
||||
const lastMessageId = threadInfo?.lastMessageId !== undefined ? threadInfo.lastMessageId
|
||||
: threadInfo?.messagesCount === 0 ? result.threadId : undefined;
|
||||
|
||||
global = updateThreadInfo(global, chatId, threadId, {
|
||||
global = updateThreadInfo(global, {
|
||||
isCommentsInfo: false,
|
||||
threadId,
|
||||
chatId,
|
||||
fromChannelId: loadingChatId,
|
||||
fromMessageId: loadingThreadId,
|
||||
lastMessageId,
|
||||
...(threadInfo
|
||||
&& pick(threadInfo, ['messagesCount', 'lastReadInboxMessageId', 'recentReplierIds'])
|
||||
),
|
||||
});
|
||||
}
|
||||
global = updateThread(global, chatId, threadId, {
|
||||
firstMessageId: result.firstMessageId,
|
||||
});
|
||||
global = replaceThreadLocalStateParam(global, chatId, threadId, 'firstMessageId', result.firstMessageId);
|
||||
setGlobal(global);
|
||||
|
||||
if (focusMessageId) {
|
||||
@ -535,16 +543,36 @@ addActionHandler('loadAllChats', async (global, actions, payload): Promise<void>
|
||||
return;
|
||||
}
|
||||
|
||||
await loadChats(
|
||||
const result = await loadChats(
|
||||
listType,
|
||||
true,
|
||||
);
|
||||
|
||||
const isFirstBatch = !isCallbackFired;
|
||||
if (!isCallbackFired) {
|
||||
await whenFirstBatchDone?.();
|
||||
isCallbackFired = true;
|
||||
}
|
||||
global = getGlobal();
|
||||
|
||||
if (result?.messages) {
|
||||
if (isFirstBatch) {
|
||||
global = replaceMessages(global, result.messages);
|
||||
} else {
|
||||
global = addMessages(global, result.messages);
|
||||
}
|
||||
}
|
||||
|
||||
if (result?.threadInfos) {
|
||||
result.threadInfos.forEach((threadInfo) => {
|
||||
global = updateThreadInfo(global, threadInfo);
|
||||
});
|
||||
}
|
||||
|
||||
if (result?.threadReadStatesById) {
|
||||
global = updateMainThreadReadStates(global, result.threadReadStatesById);
|
||||
}
|
||||
setGlobal(global);
|
||||
global = getGlobal();
|
||||
}
|
||||
});
|
||||
@ -1288,11 +1316,12 @@ addActionHandler('markChatUnread', (global, actions, payload): ActionReturnType
|
||||
actions.openFrozenAccountModal({ tabId: getCurrentTabId() });
|
||||
return;
|
||||
}
|
||||
|
||||
const chat = selectChat(global, id);
|
||||
if (!chat) return;
|
||||
void callApi('toggleDialogUnread', {
|
||||
chat,
|
||||
hasUnreadMark: !chat.hasUnreadMark,
|
||||
hasUnreadMark: true,
|
||||
});
|
||||
});
|
||||
|
||||
@ -1306,12 +1335,14 @@ addActionHandler('markChatMessagesRead', async (global, actions, payload): Promi
|
||||
}
|
||||
|
||||
const chat = selectChat(global, id);
|
||||
const chatReadState = selectThreadReadState(global, id, MAIN_THREAD_ID);
|
||||
if (!chat) return;
|
||||
|
||||
if (!chat.isForum) {
|
||||
await callApi('markMessageListRead', { chat, threadId: MAIN_THREAD_ID });
|
||||
actions.readAllMentions({ chatId: id });
|
||||
actions.readAllReactions({ chatId: id });
|
||||
if (chat.hasUnreadMark) {
|
||||
if (chatReadState?.hasUnreadMark) {
|
||||
actions.markChatRead({ id });
|
||||
}
|
||||
return;
|
||||
@ -1322,18 +1353,29 @@ addActionHandler('markChatMessagesRead', async (global, actions, payload): Promi
|
||||
let processedCount = 0;
|
||||
|
||||
while (hasMoreTopics) {
|
||||
const lastTopicThreadInfo = lastTopic && selectThreadInfo(global, id, lastTopic.id);
|
||||
const result = await callApi('fetchTopics', {
|
||||
chat, offsetDate: lastTopic?.date, offsetTopicId: lastTopic?.id, offsetId: lastTopic?.lastMessageId, limit: 100,
|
||||
chat,
|
||||
offsetDate: lastTopic?.date,
|
||||
offsetTopicId: lastTopic?.id,
|
||||
offsetId: lastTopicThreadInfo?.lastMessageId,
|
||||
limit: 100,
|
||||
});
|
||||
|
||||
if (!result?.topics?.length) return;
|
||||
|
||||
result.topics.forEach((topic) => {
|
||||
if (!topic.unreadCount && !topic.unreadMentionsCount && !topic.unreadReactionsCount) return;
|
||||
actions.markTopicRead({ chatId: id, topicId: topic.id });
|
||||
result.topics.forEach((topicWithState) => {
|
||||
global = updateTopicWithState(global, id, topicWithState);
|
||||
|
||||
const { readState } = topicWithState;
|
||||
if (readState && !readState.unreadCount && !readState.unreadMentionsCount && !readState.unreadReactionsCount) {
|
||||
return;
|
||||
}
|
||||
|
||||
actions.markTopicRead({ chatId: id, topicId: topicWithState.topic.id });
|
||||
});
|
||||
|
||||
lastTopic = result.topics[result.topics.length - 1];
|
||||
lastTopic = result.topics[result.topics.length - 1].topic;
|
||||
processedCount += result.topics.length;
|
||||
if (result.count <= processedCount) {
|
||||
hasMoreTopics = false;
|
||||
@ -1348,7 +1390,7 @@ addActionHandler('markChatRead', (global, actions, payload): ActionReturnType =>
|
||||
|
||||
callApi('toggleDialogUnread', {
|
||||
chat,
|
||||
hasUnreadMark: !chat.hasUnreadMark,
|
||||
hasUnreadMark: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
@ -1358,9 +1400,8 @@ addActionHandler('markTopicRead', (global, actions, payload): ActionReturnType =
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) return;
|
||||
|
||||
const topic = selectTopic(global, chatId, topicId);
|
||||
|
||||
const lastTopicMessageId = topic?.lastMessageId;
|
||||
const lastTopicThreadInfo = selectThreadInfo(global, chatId, topicId);
|
||||
const lastTopicMessageId = lastTopicThreadInfo?.lastMessageId;
|
||||
if (!lastTopicMessageId) return;
|
||||
|
||||
void callApi('markMessageListRead', {
|
||||
@ -1372,12 +1413,8 @@ addActionHandler('markTopicRead', (global, actions, payload): ActionReturnType =
|
||||
actions.readAllReactions({ chatId, threadId: topicId });
|
||||
|
||||
global = getGlobal();
|
||||
global = updateTopic(global, chatId, topicId, {
|
||||
unreadCount: 0,
|
||||
});
|
||||
global = updateThreadInfo(global, chatId, topicId, {
|
||||
lastReadInboxMessageId: lastTopicMessageId,
|
||||
});
|
||||
global = replaceThreadReadStateParam(global, chatId, topicId, 'lastReadInboxMessageId', lastTopicMessageId);
|
||||
global = replaceThreadReadStateParam(global, chatId, topicId, 'unreadCount', 0);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
@ -2396,16 +2433,19 @@ addActionHandler('loadTopics', async (global, actions, payload): Promise<void> =
|
||||
}
|
||||
|
||||
const offsetTopic = !force ? topicsInfo?.listedTopicIds?.reduce((acc, el) => {
|
||||
const topic = selectTopic(global, chatId, el);
|
||||
const accTopic = selectTopic(global, chatId, acc);
|
||||
if (!topic) return acc;
|
||||
if (!accTopic || topic.lastMessageId < accTopic.lastMessageId) {
|
||||
const topicThreadInfo = selectThreadInfo(global, chatId, el);
|
||||
const accTopicThreadInfo = selectThreadInfo(global, chatId, acc);
|
||||
if (!topicThreadInfo?.lastMessageId) return acc;
|
||||
if (!accTopicThreadInfo?.lastMessageId || topicThreadInfo.lastMessageId < accTopicThreadInfo.lastMessageId) {
|
||||
return el;
|
||||
}
|
||||
return acc;
|
||||
}) : undefined;
|
||||
|
||||
const { id: offsetTopicId, date: offsetDate, lastMessageId: offsetId } = (offsetTopic
|
||||
const offsetTopicThreadInfo = offsetTopic ? selectThreadInfo(global, chatId, offsetTopic) : undefined;
|
||||
const offsetId = offsetTopicThreadInfo?.lastMessageId;
|
||||
|
||||
const { id: offsetTopicId, date: offsetDate } = (offsetTopic
|
||||
&& selectTopic(global, chatId, offsetTopic)) || {};
|
||||
const result = await callApi('fetchTopics', {
|
||||
chat, offsetTopicId, offsetId, offsetDate, limit: offsetTopicId ? TOPICS_SLICE : TOPICS_SLICE_SECOND_LOAD,
|
||||
@ -2415,13 +2455,15 @@ addActionHandler('loadTopics', async (global, actions, payload): Promise<void> =
|
||||
|
||||
global = getGlobal();
|
||||
global = addMessages(global, result.messages);
|
||||
global = updateTopics(global, chatId, result.count, result.topics);
|
||||
global = updateListedTopicIds(global, chatId, result.topics.map((topic) => topic.id));
|
||||
Object.entries(result.draftsById || {}).forEach(([threadId, draft]) => {
|
||||
global = replaceThreadParam(global, chatId, Number(threadId), 'draft', draft);
|
||||
result.topics.forEach((topic) => {
|
||||
global = updateTopicWithState(global, chatId, topic);
|
||||
});
|
||||
Object.entries(result.readInboxMessageIdByTopicId || {}).forEach(([topicId, messageId]) => {
|
||||
global = updateThreadInfo(global, chatId, Number(topicId), { lastReadInboxMessageId: messageId });
|
||||
global = updateTopicsInfo(global, chatId, {
|
||||
totalCount: result.count,
|
||||
});
|
||||
global = updateListedTopicIds(global, chatId, result.topics.map((topicState) => topicState.topic.id));
|
||||
Object.entries(result.draftsById || {}).forEach(([threadId, draft]) => {
|
||||
global = replaceThreadLocalStateParam(global, chatId, Number(threadId), 'draft', draft);
|
||||
});
|
||||
|
||||
setGlobal(global);
|
||||
@ -2445,8 +2487,7 @@ addActionHandler('loadTopicById', async (global, actions, payload): Promise<void
|
||||
|
||||
global = getGlobal();
|
||||
global = addMessages(global, result.messages);
|
||||
global = updateTopic(global, chatId, topicId, result.topic);
|
||||
|
||||
global = updateTopicWithState(global, chatId, result.topic);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
@ -2918,7 +2959,7 @@ addActionHandler('updateChatDetectedLanguage', (global, actions, payload): Actio
|
||||
global = getGlobal();
|
||||
global = updateChat(global, chatId, {
|
||||
detectedLanguage,
|
||||
}, undefined, true);
|
||||
});
|
||||
|
||||
return global;
|
||||
});
|
||||
@ -3153,8 +3194,10 @@ async function loadChats(
|
||||
const isFirstBatch = !shouldIgnorePagination && !offsetPeer && !offsetDate && !offsetId;
|
||||
const shouldReplaceStaleState = listType === 'active' && isFirstBatch;
|
||||
const isAccountFreeze = selectIsCurrentUserFrozen(global);
|
||||
const currentUser = selectUser(global, global.currentUserId!)!;
|
||||
|
||||
const result = listType === 'saved' ? await callApi('fetchSavedChats', {
|
||||
parentPeer: currentUser,
|
||||
limit: CHAT_LIST_LOAD_SLICE,
|
||||
offsetDate,
|
||||
offsetId,
|
||||
@ -3198,7 +3241,6 @@ async function loadChats(
|
||||
}
|
||||
|
||||
global = updateChatListSecondaryInfo(global, listType, result);
|
||||
global = replaceMessages(global, result.messages);
|
||||
global = updateChatsLastMessageId(global, result.lastMessageByChatId, listType);
|
||||
|
||||
if (!shouldIgnorePagination) {
|
||||
@ -3216,7 +3258,7 @@ async function loadChats(
|
||||
if (!draft && !thread) return;
|
||||
|
||||
if (!selectDraft(global, chatId, MAIN_THREAD_ID)?.isLocal) {
|
||||
global = replaceThreadParam(
|
||||
global = replaceThreadLocalStateParam(
|
||||
global, chatId, MAIN_THREAD_ID, 'draft', draft,
|
||||
);
|
||||
}
|
||||
@ -3237,6 +3279,12 @@ async function loadChats(
|
||||
}
|
||||
|
||||
setGlobal(global);
|
||||
|
||||
return {
|
||||
threadInfos: result.threadInfos,
|
||||
threadReadStatesById: result.threadReadStatesById,
|
||||
messages: result.messages,
|
||||
};
|
||||
}
|
||||
|
||||
export async function loadFullChat<T extends GlobalState>(
|
||||
@ -3540,7 +3588,7 @@ async function openChatWithParams<T extends GlobalState>(
|
||||
let topic = selectTopics(global, chat.id)?.[messageId];
|
||||
if (!topic) {
|
||||
const topicResult = await callApi('fetchTopicById', { chat, topicId: messageId });
|
||||
topic = topicResult?.topic;
|
||||
topic = topicResult?.topic.topic;
|
||||
}
|
||||
|
||||
if (topic) {
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import { getActions } from '../../../global';
|
||||
|
||||
import type {
|
||||
ApiChat, ApiGlobalMessageSearchType, ApiMessage, ApiMessageSearchContext, ApiPeer, ApiSearchPostsFlood, ApiTopic,
|
||||
ApiChat, ApiGlobalMessageSearchType, ApiMessage, ApiMessageSearchContext, ApiPeer, ApiSearchPostsFlood,
|
||||
ApiTopicWithState,
|
||||
ApiUserStatus,
|
||||
} from '../../../api/types';
|
||||
import type { ActionReturnType, GlobalState, TabArgs } from '../../types';
|
||||
@ -24,7 +25,7 @@ import {
|
||||
updateGlobalSearch,
|
||||
updateGlobalSearchFetchingStatus,
|
||||
updateGlobalSearchResults,
|
||||
updateTopics,
|
||||
updateTopicWithState,
|
||||
} from '../../reducers';
|
||||
import {
|
||||
selectChat, selectChatByUsername, selectChatMessage, selectCurrentGlobalSearchQuery, selectPeer, selectTabState,
|
||||
@ -205,7 +206,7 @@ async function searchMessagesGlobal<T extends GlobalState>(global: T, params: {
|
||||
let result: {
|
||||
messages: ApiMessage[];
|
||||
userStatusesById?: Record<number, ApiUserStatus>;
|
||||
topics?: ApiTopic[];
|
||||
topics?: ApiTopicWithState[];
|
||||
totalTopicsCount?: number;
|
||||
totalCount: number;
|
||||
nextOffsetRate?: number;
|
||||
@ -239,13 +240,13 @@ async function searchMessagesGlobal<T extends GlobalState>(global: T, params: {
|
||||
|
||||
if (inChatResult) {
|
||||
const {
|
||||
messages, totalCount, nextOffsetId,
|
||||
messages, totalCount, nextOffsetId, topics: messagesTopics,
|
||||
} = inChatResult;
|
||||
|
||||
const { topics: localTopics, count } = topics || {};
|
||||
|
||||
result = {
|
||||
topics: localTopics,
|
||||
topics: messagesTopics.concat(localTopics || []),
|
||||
totalTopicsCount: count,
|
||||
messages,
|
||||
totalCount,
|
||||
@ -264,11 +265,16 @@ async function searchMessagesGlobal<T extends GlobalState>(global: T, params: {
|
||||
maxDate,
|
||||
minDate,
|
||||
});
|
||||
|
||||
if (isDeepLink(query)) {
|
||||
const link = tryParseDeepLink(query);
|
||||
if (link?.type === 'publicMessageLink') {
|
||||
global = getGlobal();
|
||||
messageLink = await getMessageByPublicLink(global, link);
|
||||
} else if (link?.type === 'privateMessageLink') {
|
||||
}
|
||||
|
||||
if (link?.type === 'privateMessageLink') {
|
||||
global = getGlobal();
|
||||
messageLink = await getMessageByPrivateLink(global, link);
|
||||
}
|
||||
}
|
||||
@ -321,13 +327,17 @@ async function searchMessagesGlobal<T extends GlobalState>(global: T, params: {
|
||||
tabId,
|
||||
);
|
||||
|
||||
if (result.topics) {
|
||||
global = updateTopics(global, peer!.id, result.totalTopicsCount!, result.topics);
|
||||
if (peer && result.topics?.length) {
|
||||
result.topics.forEach((topicState) => {
|
||||
global = updateTopicWithState(global, peer.id, topicState);
|
||||
});
|
||||
}
|
||||
|
||||
const sortedTopics = result.topics?.map(({ id }) => id).sort((a, b) => b - a);
|
||||
const sortedTopicIds = result.topics?.sort((a, b) => (
|
||||
(b.lastMessageId || b.topic.id) - (a.lastMessageId || a.topic.id)
|
||||
)).map(({ topic }) => topic.id);
|
||||
global = updateGlobalSearch(global, {
|
||||
foundTopicIds: sortedTopics,
|
||||
foundTopicIds: sortedTopicIds,
|
||||
}, tabId);
|
||||
|
||||
setGlobal(global);
|
||||
|
||||
@ -63,6 +63,7 @@ import { callApi, cancelApiProgress } from '../../../api/gramjs';
|
||||
import {
|
||||
getIsSavedDialog,
|
||||
getUserFullName,
|
||||
groupMessageIdsByThreadId,
|
||||
isChatChannel,
|
||||
isChatSuperGroup,
|
||||
isDeletedUser,
|
||||
@ -73,7 +74,7 @@ import {
|
||||
} from '../../helpers';
|
||||
import { isApiPeerChat, isApiPeerUser } from '../../helpers/peers';
|
||||
import {
|
||||
addActionHandler, getActions, getGlobal, setGlobal,
|
||||
addActionHandler, getActions, getGlobal, getPromiseActions, setGlobal,
|
||||
} from '../../index';
|
||||
import {
|
||||
addChatMessagesById,
|
||||
@ -84,7 +85,6 @@ import {
|
||||
removeRequestedMessageTranslation,
|
||||
removeUnreadMentions,
|
||||
replaceSettings,
|
||||
replaceThreadParam,
|
||||
replaceUserStatuses,
|
||||
safeReplacePinnedIds,
|
||||
safeReplaceViewportIds,
|
||||
@ -102,13 +102,18 @@ import {
|
||||
updateRequestedMessageTranslation,
|
||||
updateScheduledMessages,
|
||||
updateSponsoredMessage,
|
||||
updateThreadInfo,
|
||||
updateThreadUnreadFromForwardedMessage,
|
||||
updateTopic,
|
||||
updateTopicWithState,
|
||||
updateUploadByMessageKey,
|
||||
updateUserFullInfo,
|
||||
} from '../../reducers';
|
||||
import { updateTabState } from '../../reducers/tabs';
|
||||
import {
|
||||
replaceThreadLocalStateParam,
|
||||
replaceThreadReadStateParam,
|
||||
updateThreadInfo,
|
||||
updateThreadInfoMessagesCount,
|
||||
updateThreadReadState,
|
||||
} from '../../reducers/threads';
|
||||
import {
|
||||
selectCanForwardMessage,
|
||||
selectChat,
|
||||
@ -119,10 +124,7 @@ import {
|
||||
selectCurrentMessageList,
|
||||
selectCurrentViewedStory,
|
||||
selectCustomEmoji,
|
||||
selectDraft,
|
||||
selectEditingId,
|
||||
selectEditingMessage,
|
||||
selectEditingScheduledId,
|
||||
selectFirstMessageId,
|
||||
selectFirstUnreadId,
|
||||
selectFocusedMessageId,
|
||||
@ -137,7 +139,6 @@ import {
|
||||
selectLanguageCode,
|
||||
selectListedIds,
|
||||
selectMessageReplyInfo,
|
||||
selectNoWebPage,
|
||||
selectOutlyingListByMessageId,
|
||||
selectPeer,
|
||||
selectPeerStory,
|
||||
@ -145,20 +146,26 @@ import {
|
||||
selectPollFromMessage,
|
||||
selectRealLastReadId,
|
||||
selectReplyCanBeSentToChat,
|
||||
selectSavedDialogIdFromMessage,
|
||||
selectScheduledMessage,
|
||||
selectSendAs,
|
||||
selectTabState,
|
||||
selectThreadIdFromMessage,
|
||||
selectThreadInfo,
|
||||
selectThreadParam,
|
||||
selectTopic,
|
||||
selectTranslationLanguage,
|
||||
selectUser,
|
||||
selectUserFullInfo,
|
||||
selectUserStatus,
|
||||
selectViewportIds,
|
||||
} from '../../selectors';
|
||||
import {
|
||||
selectDraft,
|
||||
selectEditingId,
|
||||
selectEditingScheduledId,
|
||||
selectNoWebPage,
|
||||
selectSavedDialogIdFromMessage,
|
||||
selectThreadIdFromMessage,
|
||||
selectThreadInfo,
|
||||
selectThreadLocalStateParam,
|
||||
selectThreadReadState,
|
||||
} from '../../selectors/threads';
|
||||
import { updateWithLocalMedia } from '../apiUpdaters/messages';
|
||||
import { deleteMessages } from '../apiUpdaters/messages';
|
||||
|
||||
@ -299,7 +306,7 @@ async function loadWithBudget<T extends GlobalState>(
|
||||
|
||||
addActionHandler('loadMessage', async (global, actions, payload): Promise<void> => {
|
||||
const {
|
||||
chatId, messageId, replyOriginForId, threadUpdate,
|
||||
chatId, messageId, replyOriginForId,
|
||||
} = payload;
|
||||
|
||||
const chat = selectChat(global, chatId);
|
||||
@ -307,20 +314,28 @@ addActionHandler('loadMessage', async (global, actions, payload): Promise<void>
|
||||
return;
|
||||
}
|
||||
|
||||
const message = await loadMessage(global, chat, messageId, replyOriginForId);
|
||||
if (message && threadUpdate) {
|
||||
const { lastMessageId, isDeleting } = threadUpdate;
|
||||
global = getGlobal();
|
||||
|
||||
global = updateThreadUnreadFromForwardedMessage(
|
||||
global,
|
||||
message,
|
||||
chatId,
|
||||
lastMessageId,
|
||||
isDeleting,
|
||||
);
|
||||
setGlobal(global);
|
||||
const result = await callApi('fetchMessage', { chat, messageId });
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (result === MESSAGE_DELETED) {
|
||||
if (replyOriginForId) {
|
||||
global = getGlobal();
|
||||
const replyMessage = selectChatMessage(global, chat.id, replyOriginForId);
|
||||
global = updateChatMessage(global, chat.id, replyOriginForId, {
|
||||
...replyMessage,
|
||||
replyInfo: undefined,
|
||||
});
|
||||
setGlobal(global);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
global = getGlobal();
|
||||
global = updateChatMessage(global, chat.id, messageId, result.message);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('loadMessagesById', async (global, actions, payload): Promise<void> => {
|
||||
@ -953,14 +968,11 @@ async function saveDraft<T extends GlobalState>({
|
||||
const newDraft: ApiDraft | undefined = draft ? {
|
||||
...draft,
|
||||
replyInfo,
|
||||
date: Math.floor(Date.now() / 1000),
|
||||
date: (noLocalTimeUpdate && draft.date) || getServerTime(),
|
||||
isLocal: true,
|
||||
} : undefined;
|
||||
|
||||
global = replaceThreadParam(global, chatId, threadId, 'draft', newDraft);
|
||||
if (!noLocalTimeUpdate) {
|
||||
global = updateChat(global, chatId, { draftDate: newDraft?.date });
|
||||
}
|
||||
global = replaceThreadLocalStateParam(global, chatId, threadId, 'draft', newDraft);
|
||||
|
||||
setGlobal(global);
|
||||
|
||||
@ -976,8 +988,7 @@ async function saveDraft<T extends GlobalState>({
|
||||
}
|
||||
|
||||
global = getGlobal();
|
||||
global = replaceThreadParam(global, chatId, threadId, 'draft', newDraft);
|
||||
global = updateChat(global, chatId, { draftDate: newDraft?.date });
|
||||
global = replaceThreadLocalStateParam(global, chatId, threadId, 'draft', newDraft);
|
||||
|
||||
setGlobal(global);
|
||||
}
|
||||
@ -985,7 +996,7 @@ async function saveDraft<T extends GlobalState>({
|
||||
addActionHandler('toggleMessageWebPage', (global, actions, payload): ActionReturnType => {
|
||||
const { chatId, threadId, noWebPage } = payload;
|
||||
|
||||
return replaceThreadParam(global, chatId, threadId, 'noWebPage', noWebPage);
|
||||
return replaceThreadLocalStateParam(global, chatId, threadId, 'noWebPage', noWebPage);
|
||||
});
|
||||
|
||||
addActionHandler('pinMessage', (global, actions, payload): ActionReturnType => {
|
||||
@ -1017,7 +1028,7 @@ addActionHandler('unpinAllMessages', async (global, actions, payload): Promise<v
|
||||
pinnedIds?.forEach((id) => {
|
||||
global = updateChatMessage(global, chatId, id, { isPinned: false });
|
||||
});
|
||||
global = replaceThreadParam(global, chat.id, MAIN_THREAD_ID, 'pinnedIds', []);
|
||||
global = replaceThreadLocalStateParam(global, chat.id, threadId, 'pinnedIds', []);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
@ -1287,16 +1298,9 @@ addActionHandler('markMessageListRead', (global, actions, payload): ActionReturn
|
||||
|
||||
const viewportIds = selectViewportIds(global, chatId, threadId, tabId);
|
||||
const minId = selectFirstUnreadId(global, chatId, threadId);
|
||||
const topic = selectTopic(global, chatId, threadId);
|
||||
const threadReadState = selectThreadReadState(global, chatId, threadId);
|
||||
|
||||
if (threadId !== MAIN_THREAD_ID && !chat.isForum) {
|
||||
global = updateThreadInfo(global, chatId, threadId, {
|
||||
lastReadInboxMessageId: maxId,
|
||||
});
|
||||
return global;
|
||||
}
|
||||
|
||||
if (!viewportIds || !minId || (!chat.unreadCount && !topic?.unreadCount)) {
|
||||
if (!viewportIds || !minId || !threadReadState?.unreadCount) {
|
||||
return global;
|
||||
}
|
||||
|
||||
@ -1305,42 +1309,21 @@ addActionHandler('markMessageListRead', (global, actions, payload): ActionReturn
|
||||
return global;
|
||||
}
|
||||
|
||||
if (chat.isForum && topic) {
|
||||
global = updateThreadInfo(global, chatId, threadId, {
|
||||
lastReadInboxMessageId: maxId,
|
||||
});
|
||||
const newTopicUnreadCount = Math.max(0, topic.unreadCount - readCount);
|
||||
if (newTopicUnreadCount === 0 && !chat.isBotForum && chat.unreadCount) {
|
||||
global = updateChat(global, chatId, {
|
||||
unreadCount: Math.max(0, chat.unreadCount - 1),
|
||||
});
|
||||
}
|
||||
const newUnreadCount = Math.max(0, (threadReadState.unreadCount || 0) - readCount);
|
||||
global = replaceThreadReadStateParam(global, chatId, threadId, 'unreadCount', newUnreadCount);
|
||||
global = replaceThreadReadStateParam(global, chatId, threadId, 'lastReadInboxMessageId', maxId);
|
||||
|
||||
return updateTopic(global, chatId, Number(threadId), {
|
||||
unreadCount: newTopicUnreadCount,
|
||||
});
|
||||
}
|
||||
|
||||
return updateChat(global, chatId, {
|
||||
lastReadInboxMessageId: maxId,
|
||||
unreadCount: Math.max(0, (chat.unreadCount || 0) - readCount),
|
||||
});
|
||||
return global;
|
||||
});
|
||||
|
||||
addActionHandler('markMessagesRead', (global, actions, payload): ActionReturnType => {
|
||||
const { messageIds, tabId = getCurrentTabId(), shouldFetchUnreadReactions } = payload;
|
||||
|
||||
const chat = selectCurrentChat(global, tabId);
|
||||
const { chatId, messageIds } = payload;
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) {
|
||||
return;
|
||||
}
|
||||
|
||||
void callApi('markMessagesRead', { chat, messageIds })
|
||||
.then(() => {
|
||||
if (shouldFetchUnreadReactions) {
|
||||
actions.fetchUnreadReactions({ chatId: chat.id });
|
||||
}
|
||||
});
|
||||
void callApi('markMessagesRead', { chat, messageIds });
|
||||
});
|
||||
|
||||
addActionHandler('loadWebPagePreview', async (global, actions, payload): Promise<void> => {
|
||||
@ -1530,24 +1513,17 @@ addActionHandler('loadScheduledHistory', async (global, actions, payload): Promi
|
||||
|
||||
global = getGlobal();
|
||||
global = updateScheduledMessages(global, chat.id, byId);
|
||||
global = replaceThreadParam(global, chat.id, MAIN_THREAD_ID, 'scheduledIds', ids);
|
||||
|
||||
const idsByThreadId = groupMessageIdsByThreadId(global, chat.id, ids, true);
|
||||
if (!ids.length) {
|
||||
global = updatePeerFullInfo(global, chat.id, { hasScheduledMessages: false });
|
||||
}
|
||||
|
||||
if (chat?.isForum) {
|
||||
const scheduledPerThread: Record<ThreadId, number[]> = {};
|
||||
messages.forEach((message) => {
|
||||
const threadId = selectThreadIdFromMessage(global, message);
|
||||
const scheduledInThread = scheduledPerThread[threadId] || [];
|
||||
scheduledInThread.push(message.id);
|
||||
scheduledPerThread[threadId] = scheduledInThread;
|
||||
});
|
||||
|
||||
Object.entries(scheduledPerThread).forEach(([threadId, scheduledIds]) => {
|
||||
global = replaceThreadParam(global, chat.id, Number(threadId), 'scheduledIds', scheduledIds);
|
||||
});
|
||||
}
|
||||
Object.entries(idsByThreadId).forEach(([tId, newThreadScheduledIds]) => {
|
||||
const threadId = tId as ThreadId;
|
||||
if (!chat.isForum && threadId !== MAIN_THREAD_ID) return;
|
||||
global = replaceThreadLocalStateParam(global, chat.id, threadId, 'scheduledIds', newThreadScheduledIds);
|
||||
});
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
@ -1798,12 +1774,12 @@ async function loadViewportMessages<T extends GlobalState>(
|
||||
}
|
||||
|
||||
const {
|
||||
messages, count,
|
||||
messages, count, topics,
|
||||
} = result;
|
||||
|
||||
global = getGlobal();
|
||||
|
||||
const localTypingDrafts = selectThreadParam(global, chatId, threadId, 'typingDraftIdByRandomId');
|
||||
const localTypingDrafts = selectThreadLocalStateParam(global, chatId, threadId, 'typingDraftIdByRandomId');
|
||||
const typingDraftMessages = localTypingDrafts ? Object.values(localTypingDrafts)
|
||||
.map((id) => selectChatMessage(global, chatId, id))
|
||||
.filter(Boolean) : [];
|
||||
@ -1846,45 +1822,18 @@ async function loadViewportMessages<T extends GlobalState>(
|
||||
}
|
||||
}
|
||||
|
||||
if (count) {
|
||||
global = updateThreadInfo(global, chat.id, threadId, {
|
||||
messagesCount: count,
|
||||
});
|
||||
if (count !== undefined) {
|
||||
global = updateThreadInfoMessagesCount(global, chat.id, threadId, count);
|
||||
}
|
||||
|
||||
topics.forEach((topicState) => {
|
||||
global = updateTopicWithState(global, chat.id, topicState);
|
||||
});
|
||||
|
||||
setGlobal(global);
|
||||
onLoaded?.();
|
||||
}
|
||||
|
||||
async function loadMessage<T extends GlobalState>(
|
||||
global: T, chat: ApiChat, messageId: number, replyOriginForId?: number,
|
||||
) {
|
||||
const result = await callApi('fetchMessage', { chat, messageId });
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (result === MESSAGE_DELETED) {
|
||||
if (replyOriginForId) {
|
||||
global = getGlobal();
|
||||
const replyMessage = selectChatMessage(global, chat.id, replyOriginForId);
|
||||
global = updateChatMessage(global, chat.id, replyOriginForId, {
|
||||
...replyMessage,
|
||||
replyInfo: undefined,
|
||||
});
|
||||
setGlobal(global);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
global = getGlobal();
|
||||
global = updateChatMessage(global, chat.id, messageId, result.message);
|
||||
setGlobal(global);
|
||||
|
||||
return result.message;
|
||||
}
|
||||
|
||||
function findClosestIndex(sourceIds: number[], offsetId: number) {
|
||||
if (offsetId < sourceIds[0]) {
|
||||
return 0;
|
||||
@ -2297,9 +2246,38 @@ addActionHandler('hideSponsored', async (global, actions, payload): Promise<void
|
||||
});
|
||||
});
|
||||
|
||||
addActionHandler('fetchUnreadMentions', async (global, actions, payload): Promise<void> => {
|
||||
const { chatId, offsetId } = payload;
|
||||
await fetchUnreadMentions(global, chatId, offsetId);
|
||||
addActionHandler('loadUnreadMentions', async (global, actions, payload): Promise<void> => {
|
||||
const { chatId, threadId = MAIN_THREAD_ID, offsetId } = payload;
|
||||
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) return;
|
||||
|
||||
const result = await callApi('fetchUnreadMentions', {
|
||||
chat,
|
||||
threadId: threadId !== MAIN_THREAD_ID ? threadId : undefined,
|
||||
offsetId,
|
||||
});
|
||||
|
||||
if (!result) return;
|
||||
|
||||
const { messages, topics, totalCount } = result;
|
||||
|
||||
const byId = buildCollectionByKey(messages, 'id');
|
||||
const ids = Object.keys(byId).map(Number);
|
||||
|
||||
global = getGlobal();
|
||||
global = addChatMessagesById(global, chat.id, byId);
|
||||
topics.forEach((topicState) => {
|
||||
global = updateTopicWithState(global, chat.id, topicState);
|
||||
});
|
||||
global = addUnreadMentions({
|
||||
global,
|
||||
chatId,
|
||||
ids,
|
||||
totalCount,
|
||||
});
|
||||
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('approveSuggestedPost', async (global, actions, payload): Promise<void> => {
|
||||
@ -2373,53 +2351,35 @@ addActionHandler('rejectSuggestedPost', async (global, actions, payload): Promis
|
||||
});
|
||||
});
|
||||
|
||||
async function fetchUnreadMentions<T extends GlobalState>(global: T, chatId: string, offsetId?: number) {
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) return;
|
||||
|
||||
const result = await callApi('fetchUnreadMentions', { chat, offsetId });
|
||||
|
||||
if (!result) return;
|
||||
|
||||
const { messages } = result;
|
||||
|
||||
const byId = buildCollectionByKey(messages, 'id');
|
||||
const ids = Object.keys(byId).map(Number);
|
||||
|
||||
global = getGlobal();
|
||||
global = addChatMessagesById(global, chat.id, byId);
|
||||
global = addUnreadMentions(global, chatId, chat, ids);
|
||||
|
||||
setGlobal(global);
|
||||
}
|
||||
|
||||
addActionHandler('markMentionsRead', (global, actions, payload): ActionReturnType => {
|
||||
const { chatId, messageIds, tabId = getCurrentTabId() } = payload;
|
||||
const { chatId, messageIds } = payload;
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) return;
|
||||
|
||||
global = removeUnreadMentions(global, chatId, chat, messageIds, true);
|
||||
global = removeUnreadMentions({
|
||||
global,
|
||||
chatId,
|
||||
ids: messageIds,
|
||||
});
|
||||
setGlobal(global);
|
||||
|
||||
actions.markMessagesRead({ messageIds, tabId });
|
||||
actions.markMessagesRead({ chatId, messageIds });
|
||||
});
|
||||
|
||||
addActionHandler('focusNextMention', async (global, actions, payload): Promise<void> => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
const { chatId, threadId = MAIN_THREAD_ID, tabId = getCurrentTabId() } = payload;
|
||||
|
||||
let chat = selectCurrentChat(global, tabId);
|
||||
let readState = selectThreadReadState(global, chatId, threadId);
|
||||
|
||||
if (!chat) return;
|
||||
if (!readState?.unreadMentions) {
|
||||
await getPromiseActions().loadUnreadMentions({ chatId, threadId });
|
||||
|
||||
if (!chat.unreadMentions) {
|
||||
await fetchUnreadMentions(global, chat.id);
|
||||
global = getGlobal();
|
||||
const previousChatId = chat.id;
|
||||
chat = selectCurrentChat(global, tabId);
|
||||
if (!chat?.unreadMentions || previousChatId !== chat.id) return;
|
||||
readState = selectThreadReadState(global, chatId, threadId);
|
||||
if (!readState?.unreadMentions) return;
|
||||
}
|
||||
|
||||
actions.focusMessage({ chatId: chat.id, messageId: chat.unreadMentions[0], tabId });
|
||||
actions.focusMessage({ chatId, messageId: readState.unreadMentions[0], tabId });
|
||||
});
|
||||
|
||||
addActionHandler('readAllMentions', (global, actions, payload): ActionReturnType => {
|
||||
@ -2428,17 +2388,13 @@ addActionHandler('readAllMentions', (global, actions, payload): ActionReturnType
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) return undefined;
|
||||
|
||||
callApi('readAllMentions', { chat, threadId: threadId === MAIN_THREAD_ID ? undefined : threadId });
|
||||
callApi('readAllMentions', { chat, threadId: threadId !== MAIN_THREAD_ID ? threadId : undefined });
|
||||
|
||||
if (threadId === MAIN_THREAD_ID) {
|
||||
return updateChat(global, chat.id, {
|
||||
unreadMentionsCount: undefined,
|
||||
unreadMentions: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
// TODO[Forums]: Support mentions in threads
|
||||
return undefined;
|
||||
global = updateThreadReadState(global, chatId, threadId, {
|
||||
unreadMentionsCount: 0,
|
||||
unreadMentions: undefined,
|
||||
});
|
||||
return global;
|
||||
});
|
||||
|
||||
addActionHandler('openUrl', (global, actions, payload): ActionReturnType => {
|
||||
@ -2805,7 +2761,7 @@ addActionHandler('loadMessageViews', async (global, actions, payload): Promise<v
|
||||
}, true);
|
||||
|
||||
if (update.threadInfo) {
|
||||
global = updateThreadInfo(global, chatId, update.id, update.threadInfo);
|
||||
global = updateThreadInfo(global, update.threadInfo);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ import { ApiMediaFormat, MAIN_THREAD_ID } from '../../../api/types';
|
||||
import { GENERAL_REFETCH_INTERVAL } from '../../../config';
|
||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||
import {
|
||||
buildCollectionByCallback, buildCollectionByKey, omit, partition, unique,
|
||||
buildCollectionByCallback, buildCollectionByKey, omit, partition,
|
||||
} from '../../../util/iteratees';
|
||||
import { getMessageKey } from '../../../util/keys/messageKey';
|
||||
import * as mediaLoader from '../../../util/mediaLoader';
|
||||
@ -19,16 +19,23 @@ import {
|
||||
isMessageLocal,
|
||||
isSameReaction,
|
||||
} from '../../helpers';
|
||||
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
||||
import { addActionHandler, getGlobal, getPromiseActions, setGlobal } from '../../index';
|
||||
import {
|
||||
addChatMessagesById, updateChat, updateChatMessage,
|
||||
addChatMessagesById,
|
||||
updateChatMessage,
|
||||
updateTopicWithState,
|
||||
} from '../../reducers';
|
||||
import { addMessageReaction, subtractXForEmojiInteraction, updateUnreadReactions } from '../../reducers/reactions';
|
||||
import {
|
||||
addMessageReaction,
|
||||
addUnreadReactions,
|
||||
removeUnreadReactions,
|
||||
subtractXForEmojiInteraction,
|
||||
} from '../../reducers/reactions';
|
||||
import { updateTabState } from '../../reducers/tabs';
|
||||
import { updateThreadReadState } from '../../reducers/threads';
|
||||
import {
|
||||
selectChat,
|
||||
selectChatMessage,
|
||||
selectCurrentChat,
|
||||
selectDefaultReaction,
|
||||
selectIsChatWithSelf,
|
||||
selectIsCurrentUserFrozen,
|
||||
@ -37,6 +44,7 @@ import {
|
||||
selectPerformanceSettingsValue,
|
||||
selectTabState,
|
||||
} from '../../selectors';
|
||||
import { selectThreadReadState } from '../../selectors/threads';
|
||||
|
||||
const INTERACTION_RANDOM_OFFSET = 40;
|
||||
|
||||
@ -467,98 +475,82 @@ addActionHandler('sendWatchingEmojiInteraction', (global, actions, payload): Act
|
||||
}, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('fetchUnreadReactions', async (global, actions, payload): Promise<void> => {
|
||||
const { chatId, offsetId } = payload;
|
||||
addActionHandler('loadUnreadReactions', async (global, actions, payload): Promise<void> => {
|
||||
const { chatId, threadId = MAIN_THREAD_ID, offsetId } = payload;
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) return;
|
||||
|
||||
const result = await callApi('fetchUnreadReactions', { chat, offsetId, addOffset: offsetId ? -1 : undefined });
|
||||
const result = await callApi('fetchUnreadReactions', {
|
||||
chat,
|
||||
threadId: threadId !== MAIN_THREAD_ID ? threadId : undefined,
|
||||
offsetId,
|
||||
});
|
||||
|
||||
// Server side bug, when server returns unread reactions count > 0 for deleted messages
|
||||
if (!result || !result.messages.length) {
|
||||
global = getGlobal();
|
||||
global = updateUnreadReactions(global, chatId, {
|
||||
unreadReactionsCount: 0,
|
||||
});
|
||||
if (!result) return;
|
||||
|
||||
setGlobal(global);
|
||||
return;
|
||||
}
|
||||
|
||||
const { messages } = result;
|
||||
const { messages, topics, totalCount } = result;
|
||||
|
||||
const byId = buildCollectionByKey(messages, 'id');
|
||||
const ids = Object.keys(byId).map(Number);
|
||||
|
||||
global = getGlobal();
|
||||
global = addChatMessagesById(global, chat.id, byId);
|
||||
global = updateUnreadReactions(global, chatId, {
|
||||
unreadReactions: unique([...(chat.unreadReactions || []), ...ids]).sort((a, b) => b - a),
|
||||
topics.forEach((topicState) => {
|
||||
global = updateTopicWithState(global, chat.id, topicState);
|
||||
});
|
||||
global = addUnreadReactions({
|
||||
global,
|
||||
chatId,
|
||||
ids,
|
||||
totalCount,
|
||||
});
|
||||
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('animateUnreadReaction', (global, actions, payload): ActionReturnType => {
|
||||
const { messageIds, tabId = getCurrentTabId() } = payload;
|
||||
const { chatId, messageIds, tabId = getCurrentTabId() } = payload;
|
||||
|
||||
const chat = selectCurrentChat(global, tabId);
|
||||
if (!chat) return undefined;
|
||||
|
||||
if (!chat.unreadReactionsCount) {
|
||||
return updateUnreadReactions(global, chat.id, {
|
||||
unreadReactions: [],
|
||||
});
|
||||
}
|
||||
|
||||
const unreadReactionsCount = Math.max(chat.unreadReactionsCount - messageIds.length, 0);
|
||||
const unreadReactions = (chat.unreadReactions || []).filter((id) => !messageIds.includes(id));
|
||||
|
||||
global = updateUnreadReactions(global, chat.id, {
|
||||
unreadReactions,
|
||||
unreadReactionsCount,
|
||||
});
|
||||
global = removeUnreadReactions({ global, chatId, ids: messageIds });
|
||||
|
||||
setGlobal(global);
|
||||
|
||||
actions.markMessagesRead({ messageIds, shouldFetchUnreadReactions: true, tabId });
|
||||
actions.markMessagesRead({ chatId, messageIds });
|
||||
|
||||
if (!selectPerformanceSettingsValue(global, 'reactionEffects')) return undefined;
|
||||
|
||||
global = getGlobal();
|
||||
|
||||
messageIds.forEach((id) => {
|
||||
const message = selectChatMessage(global, chat.id, id);
|
||||
const message = selectChatMessage(global, chatId, id);
|
||||
if (!message) return;
|
||||
|
||||
const { reaction, isOwn, isUnread } = message.reactions?.recentReactions?.[0] ?? {};
|
||||
if (reaction && isUnread && !isOwn) {
|
||||
const messageKey = getMessageKey(message);
|
||||
actions.startActiveReaction({ containerId: messageKey, reaction, tabId: getCurrentTabId() });
|
||||
actions.startActiveReaction({ containerId: messageKey, reaction, tabId });
|
||||
}
|
||||
});
|
||||
|
||||
return undefined;
|
||||
});
|
||||
|
||||
addActionHandler('focusNextReaction', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
const chat = selectCurrentChat(global, tabId);
|
||||
addActionHandler('focusNextReaction', async (global, actions, payload): Promise<void> => {
|
||||
const { chatId, threadId = MAIN_THREAD_ID, tabId = getCurrentTabId() } = payload || {};
|
||||
let readState = selectThreadReadState(global, chatId, threadId);
|
||||
|
||||
if (!chat?.unreadReactions) {
|
||||
if (chat?.unreadReactionsCount) {
|
||||
return updateChat(global, chat.id, {
|
||||
unreadReactionsCount: 0,
|
||||
});
|
||||
}
|
||||
return undefined;
|
||||
if (!readState?.unreadReactions?.length) {
|
||||
await getPromiseActions().loadUnreadReactions({ chatId, threadId });
|
||||
|
||||
global = getGlobal();
|
||||
readState = selectThreadReadState(global, chatId, threadId);
|
||||
if (!readState?.unreadReactions?.length) return;
|
||||
}
|
||||
|
||||
actions.focusMessage({
|
||||
chatId: chat.id, messageId: chat.unreadReactions[0], tabId, scrollTargetPosition: 'end',
|
||||
chatId, threadId, messageId: readState.unreadReactions[0], tabId, scrollTargetPosition: 'end',
|
||||
});
|
||||
actions.markMessagesRead({ messageIds: [chat.unreadReactions[0]], tabId });
|
||||
return undefined;
|
||||
actions.markMessagesRead({ chatId, messageIds: [readState.unreadReactions[0]] });
|
||||
});
|
||||
|
||||
addActionHandler('readAllReactions', (global, actions, payload): ActionReturnType => {
|
||||
@ -566,17 +558,13 @@ addActionHandler('readAllReactions', (global, actions, payload): ActionReturnTyp
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) return undefined;
|
||||
|
||||
callApi('readAllReactions', { chat, threadId: threadId === MAIN_THREAD_ID ? undefined : threadId });
|
||||
callApi('readAllReactions', { chat, threadId: threadId !== MAIN_THREAD_ID ? threadId : undefined });
|
||||
|
||||
if (threadId === MAIN_THREAD_ID) {
|
||||
return updateUnreadReactions(global, chat.id, {
|
||||
unreadReactionsCount: undefined,
|
||||
unreadReactions: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
// TODO[Forums]: Support unread reactions in threads
|
||||
return undefined;
|
||||
global = updateThreadReadState(global, chatId, threadId, {
|
||||
unreadReactionsCount: 0,
|
||||
unreadReactions: undefined,
|
||||
});
|
||||
return global;
|
||||
});
|
||||
|
||||
addActionHandler('loadTopReactions', async (global): Promise<void> => {
|
||||
|
||||
@ -1,7 +1,6 @@
|
||||
import { addCallback } from '../../../lib/teact/teactn';
|
||||
|
||||
import type { ApiThreadInfo } from '../../../api/types/messages';
|
||||
import type { Thread, ThreadId } from '../../../types';
|
||||
import type { ThreadId, ThreadLocalState } from '../../../types';
|
||||
import type { RequiredGlobalActions } from '../../index';
|
||||
import type { ActionReturnType, GlobalState } from '../../types';
|
||||
import { MAIN_THREAD_ID } from '../../../api/types';
|
||||
@ -18,27 +17,28 @@ import {
|
||||
} from '../../index';
|
||||
import {
|
||||
addChatMessagesById,
|
||||
addMessages,
|
||||
safeReplaceViewportIds,
|
||||
updateChats,
|
||||
updateListedIds,
|
||||
updateThread,
|
||||
updateThreadInfo,
|
||||
updateUsers,
|
||||
} from '../../reducers';
|
||||
import { updateTabState } from '../../reducers/tabs';
|
||||
import { updateThreadInfo, updateThreadLocalState } from '../../reducers/threads';
|
||||
import {
|
||||
selectChat,
|
||||
selectChatMessage,
|
||||
selectChatMessages,
|
||||
selectCurrentMessageList,
|
||||
selectTabState,
|
||||
selectTopics,
|
||||
} from '../../selectors';
|
||||
import {
|
||||
selectDraft,
|
||||
selectEditingDraft,
|
||||
selectEditingId,
|
||||
selectTabState,
|
||||
selectThreadInfo,
|
||||
selectTopics,
|
||||
} from '../../selectors';
|
||||
selectThreadReadState,
|
||||
} from '../../selectors/threads';
|
||||
|
||||
const RELEASE_STATUS_TIMEOUT = 15000; // 15 sec;
|
||||
|
||||
@ -106,48 +106,26 @@ async function loadAndReplaceMessages<T extends GlobalState>(global: T, actions:
|
||||
|
||||
// Memoize drafts
|
||||
const draftChatIds = Object.keys(global.messages.byChatId);
|
||||
const draftsByChatId = draftChatIds.reduce<Record<string, Record<number, Partial<Thread>>>>((acc, chatId) => {
|
||||
acc[chatId] = Object
|
||||
.keys(global.messages.byChatId[chatId].threadsById)
|
||||
.reduce<Record<number, Partial<Thread>>>((acc2, threadId) => {
|
||||
acc2[Number(threadId)] = omitUndefined({
|
||||
draft: selectDraft(global, chatId, Number(threadId)),
|
||||
editingId: selectEditingId(global, chatId, Number(threadId)),
|
||||
editingDraft: selectEditingDraft(global, chatId, Number(threadId)),
|
||||
});
|
||||
const draftsByChatId = draftChatIds
|
||||
.reduce<Record<string, Record<number, Partial<ThreadLocalState>>>>((acc, chatId) => {
|
||||
acc[chatId] = Object
|
||||
.keys(global.messages.byChatId[chatId].threadsById)
|
||||
.reduce<Record<number, Partial<ThreadLocalState>>>((acc2, 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;
|
||||
}, {});
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// Memoize last messages
|
||||
const lastMessages = Object.entries(global.chats.lastMessageIds.all || {}).map(([chatId, messageId]) => (
|
||||
selectChatMessage(global, chatId, Number(messageId))
|
||||
)).filter(Boolean);
|
||||
const savedLastMessages = Object.values(global.chats.lastMessageIds.saved || {}).map((messageId) => (
|
||||
selectChatMessage(global, global.currentUserId!, Number(messageId))
|
||||
)).filter(Boolean);
|
||||
|
||||
// Memoize thread infos for last messages
|
||||
const lastMessagesThreadInfos: { chatId: string; messageId: number; threadInfo: ApiThreadInfo }[] = [];
|
||||
lastMessages.forEach((message) => {
|
||||
const threadInfo = selectThreadInfo(global, message.chatId, message.id);
|
||||
if (threadInfo) {
|
||||
lastMessagesThreadInfos.push({
|
||||
chatId: message.chatId,
|
||||
messageId: message.id,
|
||||
threadInfo,
|
||||
});
|
||||
}
|
||||
});
|
||||
return acc2;
|
||||
}, {});
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
for (const { id: tabId } of Object.values(global.byTabId)) {
|
||||
global = getGlobal();
|
||||
const { chatId: currentChatId, threadId: currentThreadId } = selectCurrentMessageList(global, tabId) || {};
|
||||
const activeThreadId = currentThreadId || MAIN_THREAD_ID;
|
||||
const threadInfo = currentChatId && currentThreadId
|
||||
? selectThreadInfo(global, currentChatId, currentThreadId) : undefined;
|
||||
const currentChat = currentChatId ? global.chats.byId[currentChatId] : undefined;
|
||||
if (currentChatId && currentChat) {
|
||||
const [result, resultDiscussion] = await Promise.all([
|
||||
@ -173,12 +151,13 @@ async function loadAndReplaceMessages<T extends GlobalState>(global: T, actions:
|
||||
: [];
|
||||
const topics = selectTopics(global, currentChatId);
|
||||
const topicLastMessages = topics ? Object.values(topics)
|
||||
.map(({ lastMessageId }) => currentChatMessages[lastMessageId])
|
||||
.filter(Boolean)
|
||||
: [];
|
||||
.map(({ id }) => {
|
||||
const topicThreadInfo = selectThreadInfo(global, currentChatId, id);
|
||||
return topicThreadInfo?.lastMessageId ? currentChatMessages[topicThreadInfo.lastMessageId] : undefined;
|
||||
}).filter(Boolean) : [];
|
||||
|
||||
const resultMessageIds = result.messages.map(({ id }) => id);
|
||||
const messagesThreadInfos = pick(global.messages.byChatId[currentChatId].threadsById, resultMessageIds);
|
||||
const messagesThreads = pick(global.messages.byChatId[currentChatId].threadsById, resultMessageIds);
|
||||
|
||||
const isDiscussionStartLoaded = !result.messages.length
|
||||
|| result.messages.some(({ id }) => id === resultDiscussion?.firstMessageId);
|
||||
@ -189,13 +168,7 @@ async function loadAndReplaceMessages<T extends GlobalState>(global: T, actions:
|
||||
const listedIds = unique(allMessages.map(({ id }) => id));
|
||||
|
||||
if (!wasReset) {
|
||||
global = {
|
||||
...global,
|
||||
messages: {
|
||||
...global.messages,
|
||||
byChatId: {},
|
||||
},
|
||||
};
|
||||
global = resetMessages(global);
|
||||
|
||||
Object.values(global.byTabId).forEach(({ id: otherTabId }) => {
|
||||
global = updateTabState(global, {
|
||||
@ -208,17 +181,11 @@ async function loadAndReplaceMessages<T extends GlobalState>(global: T, actions:
|
||||
global = addChatMessagesById(global, currentChatId, byId);
|
||||
global = updateListedIds(global, currentChatId, activeThreadId, listedIds);
|
||||
|
||||
Object.entries(messagesThreadInfos).forEach(([id, thread]) => {
|
||||
Object.entries(messagesThreads).forEach(([id, thread]) => {
|
||||
if (!thread?.threadInfo) return;
|
||||
global = updateThreadInfo(global, currentChatId, id, thread.threadInfo);
|
||||
global = updateThreadInfo(global, thread.threadInfo);
|
||||
});
|
||||
|
||||
if (threadInfo && !threadInfo.isCommentsInfo && activeThreadId !== MAIN_THREAD_ID) {
|
||||
global = updateThreadInfo(global, currentChatId, activeThreadId, {
|
||||
...pick(threadInfo, ['fromChannelId', 'fromMessageId']),
|
||||
});
|
||||
}
|
||||
|
||||
Object.values(global.byTabId).forEach(({ id: otherTabId }) => {
|
||||
const { chatId: otherChatId, threadId: otherThreadId } = selectCurrentMessageList(global, otherTabId) || {};
|
||||
if (otherChatId === currentChatId && otherThreadId === activeThreadId) {
|
||||
@ -247,13 +214,7 @@ async function loadAndReplaceMessages<T extends GlobalState>(global: T, actions:
|
||||
global = getGlobal();
|
||||
|
||||
if (!areMessagesLoaded) {
|
||||
global = {
|
||||
...global,
|
||||
messages: {
|
||||
...global.messages,
|
||||
byChatId: {},
|
||||
},
|
||||
};
|
||||
global = resetMessages(global);
|
||||
|
||||
Object.values(global.byTabId).forEach(({ id: otherTabId }) => {
|
||||
global = updateTabState(global, {
|
||||
@ -263,26 +224,13 @@ async function loadAndReplaceMessages<T extends GlobalState>(global: T, actions:
|
||||
}
|
||||
|
||||
// Restore drafts
|
||||
|
||||
Object.keys(draftsByChatId).forEach((chatId) => {
|
||||
const threads = draftsByChatId[chatId];
|
||||
Object.keys(threads).forEach((threadId) => {
|
||||
global = updateThread(global, chatId, Number(threadId), draftsByChatId[chatId][Number(threadId)]);
|
||||
global = updateThreadLocalState(global, chatId, Number(threadId), draftsByChatId[chatId][Number(threadId)]);
|
||||
});
|
||||
});
|
||||
|
||||
// Restore thread infos
|
||||
lastMessagesThreadInfos.forEach(({ chatId, messageId, threadInfo }) => {
|
||||
const memoThreadInfo = selectThreadInfo(global, chatId, messageId);
|
||||
if (!memoThreadInfo) {
|
||||
global = updateThreadInfo(global, chatId, String(messageId), threadInfo);
|
||||
}
|
||||
});
|
||||
|
||||
// Restore last messages
|
||||
global = addMessages(global, lastMessages);
|
||||
global = addMessages(global, savedLastMessages);
|
||||
|
||||
setGlobal(global);
|
||||
|
||||
Object.values(global.byTabId).forEach(({ id: tabId }) => {
|
||||
@ -293,17 +241,28 @@ async function loadAndReplaceMessages<T extends GlobalState>(global: T, actions:
|
||||
});
|
||||
}
|
||||
|
||||
function resetMessages<T extends GlobalState>(global: T) {
|
||||
return {
|
||||
...global,
|
||||
messages: {
|
||||
...global.messages,
|
||||
byChatId: {},
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function loadTopMessages<T extends GlobalState>(global: T, chatId: string, threadId: ThreadId) {
|
||||
const currentUserId = global.currentUserId!;
|
||||
const isSavedDialog = getIsSavedDialog(chatId, threadId, currentUserId);
|
||||
const realChatId = isSavedDialog ? String(threadId) : chatId;
|
||||
|
||||
const chat = selectChat(global, realChatId)!;
|
||||
const readState = selectThreadReadState(global, chatId, threadId);
|
||||
|
||||
return callApi('fetchMessages', {
|
||||
chat,
|
||||
threadId,
|
||||
offsetId: !isSavedDialog ? chat.lastReadInboxMessageId : undefined,
|
||||
offsetId: !isSavedDialog ? readState?.lastReadInboxMessageId : undefined,
|
||||
addOffset: -(Math.round(MESSAGE_LIST_SLICE / 2) + 1),
|
||||
limit: MESSAGE_LIST_SLICE,
|
||||
isSavedDialog,
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { ApiChat, ApiMessage, ApiUpdateChat } from '../../../api/types';
|
||||
import type { ApiChat, ApiUpdateChat } from '../../../api/types';
|
||||
import type { ActionReturnType } from '../../types';
|
||||
import { MAIN_THREAD_ID } from '../../../api/types';
|
||||
|
||||
@ -20,16 +20,15 @@ import {
|
||||
replaceChatMessages,
|
||||
replacePeerPhotos,
|
||||
replacePinnedTopicIds,
|
||||
replaceThreadParam,
|
||||
updateChat,
|
||||
updateChatFullInfo,
|
||||
updateChatListType,
|
||||
updatePeerStoriesHidden,
|
||||
updateThreadInfo,
|
||||
updateTopic,
|
||||
} from '../../reducers';
|
||||
import { updateUnreadReactions } from '../../reducers/reactions';
|
||||
import { removeUnreadReactions } from '../../reducers/reactions';
|
||||
import { updateTabState } from '../../reducers/tabs';
|
||||
import { addUnreadMessageToCounter, replaceThreadLocalStateParam } from '../../reducers/threads';
|
||||
import {
|
||||
selectChat,
|
||||
selectChatFullInfo,
|
||||
@ -40,9 +39,8 @@ import {
|
||||
selectIsChatListed,
|
||||
selectPeer,
|
||||
selectTabState,
|
||||
selectThreadParam,
|
||||
selectTopicFromMessage,
|
||||
} from '../../selectors';
|
||||
import { selectThreadLocalStateParam, selectThreadReadState } from '../../selectors/threads';
|
||||
|
||||
const TYPING_STATUS_CLEAR_DELAY = 6000; // 6 seconds
|
||||
const INVALIDATE_FULL_CHAT_FIELDS = new Set<keyof ApiChat>([
|
||||
@ -54,16 +52,16 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
switch (update['@type']) {
|
||||
case 'updateChat': {
|
||||
const localChat = selectChat(global, update.id);
|
||||
const { isForum: prevIsForum, lastReadOutboxMessageId } = localChat || {};
|
||||
|
||||
if (update.chat.lastReadOutboxMessageId && lastReadOutboxMessageId
|
||||
&& update.chat.lastReadOutboxMessageId < lastReadOutboxMessageId) {
|
||||
const localReadState = selectThreadReadState(global, update.id, MAIN_THREAD_ID);
|
||||
const { isForum: prevIsForum } = localChat || {};
|
||||
const { lastReadOutboxMessageId } = localReadState || {};
|
||||
if (update.readState?.lastReadOutboxMessageId && lastReadOutboxMessageId
|
||||
&& update.readState.lastReadOutboxMessageId < lastReadOutboxMessageId) {
|
||||
update = {
|
||||
...update,
|
||||
chat: omit(update.chat, ['lastReadInboxMessageId']),
|
||||
readState: omit(update.readState, ['lastReadOutboxMessageId']),
|
||||
};
|
||||
}
|
||||
|
||||
global = updateChat(global, update.id, update.chat);
|
||||
|
||||
if (localChat?.areStoriesHidden !== update.chat.areStoriesHidden) {
|
||||
@ -91,7 +89,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
if (update.chat.id) {
|
||||
closeMessageNotifications({
|
||||
chatId: update.chat.id,
|
||||
lastReadInboxMessageId: update.chat.lastReadInboxMessageId,
|
||||
lastReadInboxMessageId: update.readState?.lastReadInboxMessageId,
|
||||
});
|
||||
}
|
||||
|
||||
@ -159,34 +157,16 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
return global;
|
||||
}
|
||||
|
||||
case 'updateChatInbox': {
|
||||
const { id, threadId, lastReadInboxMessageId, unreadCount } = update;
|
||||
const chat = selectChat(global, id);
|
||||
if (chat?.isBotForum && threadId) {
|
||||
global = updateTopic(global, id, Number(threadId), {
|
||||
unreadCount,
|
||||
});
|
||||
return updateThreadInfo(global, id, threadId, {
|
||||
lastReadInboxMessageId,
|
||||
});
|
||||
} else {
|
||||
return updateChat(global, id, {
|
||||
lastReadInboxMessageId,
|
||||
unreadCount,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
case 'updateChatTypingStatus': {
|
||||
const { id, threadId = MAIN_THREAD_ID, typingStatus } = update;
|
||||
global = replaceThreadParam(global, id, threadId, 'typingStatus', typingStatus);
|
||||
global = replaceThreadLocalStateParam(global, id, threadId, 'typingStatus', typingStatus);
|
||||
setGlobal(global);
|
||||
|
||||
setTimeout(() => {
|
||||
global = getGlobal();
|
||||
const currentTypingStatus = selectThreadParam(global, id, threadId, 'typingStatus');
|
||||
const currentTypingStatus = selectThreadLocalStateParam(global, id, threadId, 'typingStatus');
|
||||
if (typingStatus && currentTypingStatus && typingStatus.timestamp === currentTypingStatus.timestamp) {
|
||||
global = replaceThreadParam(global, id, threadId, 'typingStatus', undefined);
|
||||
global = replaceThreadLocalStateParam(global, id, threadId, 'typingStatus', undefined);
|
||||
setGlobal(global);
|
||||
}
|
||||
}, TYPING_STATUS_CLEAR_DELAY);
|
||||
@ -195,39 +175,32 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
}
|
||||
|
||||
case 'newMessage': {
|
||||
const { message } = update;
|
||||
const { chatId, id, message } = update;
|
||||
|
||||
const isOur = message.senderId ? message.senderId === global.currentUserId : message.isOutgoing;
|
||||
if (isOur && !message.isFromScheduled) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const isLocal = isLocalMessageId(message.id!);
|
||||
const isLocal = isLocalMessageId(id);
|
||||
|
||||
const chat = selectChat(global, update.chatId);
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const hasMention = Boolean(update.message.id && update.message.hasUnreadMention);
|
||||
const hasMention = Boolean(message.hasUnreadMention);
|
||||
|
||||
if (!isLocal || chat.id === SERVICE_NOTIFICATIONS_USER_ID) {
|
||||
global = updateChat(global, update.chatId, {
|
||||
unreadCount: chat.unreadCount ? chat.unreadCount + 1 : 1,
|
||||
});
|
||||
global = addUnreadMessageToCounter(global, chatId, message);
|
||||
|
||||
if (hasMention) {
|
||||
global = addUnreadMentions(global, update.chatId, chat, [update.message.id!], true);
|
||||
}
|
||||
|
||||
const topic = chat.isForum ? selectTopicFromMessage(global, message as ApiMessage) : undefined;
|
||||
if (topic) {
|
||||
global = updateTopic(global, update.chatId, topic.id, {
|
||||
unreadCount: topic.unreadCount ? topic.unreadCount + 1 : 1,
|
||||
global = addUnreadMentions({
|
||||
global,
|
||||
chatId,
|
||||
ids: [id],
|
||||
});
|
||||
}
|
||||
|
||||
// TODO Replace draft with new message
|
||||
}
|
||||
|
||||
setGlobal(global);
|
||||
@ -246,18 +219,17 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
|
||||
ids.forEach((id) => {
|
||||
const chatId = ('channelId' in update ? update.channelId : selectCommonBoxChatId(global, id))!;
|
||||
const chat = selectChat(global, chatId);
|
||||
|
||||
if (messageUpdate.reactions && chat?.unreadReactionsCount
|
||||
&& !checkIfHasUnreadReactions(global, messageUpdate.reactions)) {
|
||||
global = updateUnreadReactions(global, chatId, {
|
||||
unreadReactionsCount: Math.max(chat.unreadReactionsCount - 1, 0) || undefined,
|
||||
unreadReactions: chat.unreadReactions?.filter((i) => i !== id),
|
||||
});
|
||||
if (messageUpdate.reactions && !checkIfHasUnreadReactions(global, messageUpdate.reactions)) {
|
||||
global = removeUnreadReactions({ global, chatId, ids: [id] });
|
||||
}
|
||||
|
||||
if (!messageUpdate.hasUnreadMention && chat?.unreadMentionsCount) {
|
||||
global = removeUnreadMentions(global, chatId, chat, [id], true);
|
||||
if (!messageUpdate.hasUnreadMention) {
|
||||
global = removeUnreadMentions({
|
||||
global,
|
||||
chatId,
|
||||
ids: [id],
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@ -481,8 +453,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
global = replaceThreadParam(global, chatId, threadId || MAIN_THREAD_ID, 'draft', draft);
|
||||
global = updateChat(global, chatId, { draftDate: draft?.date });
|
||||
global = replaceThreadLocalStateParam(global, chatId, threadId || MAIN_THREAD_ID, 'draft', draft);
|
||||
return global;
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type {
|
||||
ApiChat, ApiMediaExtendedPreview, ApiMessage, ApiReactions,
|
||||
ApiMediaExtendedPreview, ApiMessage, ApiReactions,
|
||||
MediaContent,
|
||||
} from '../../../api/types';
|
||||
import type { ActiveEmojiInteraction, ThreadId } from '../../../types';
|
||||
@ -14,7 +14,7 @@ import { areDeepEqual } from '../../../util/areDeepEqual';
|
||||
import { isUserId } from '../../../util/entities/ids';
|
||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||
import {
|
||||
buildCollectionByKey, omit, pickTruthy, unique,
|
||||
buildCollectionByKey, omit, unique,
|
||||
} from '../../../util/iteratees';
|
||||
import { getMessageKey, isLocalMessageId } from '../../../util/keys/messageKey';
|
||||
import { notifyAboutMessage } from '../../../util/notifications';
|
||||
@ -28,6 +28,7 @@ import {
|
||||
getIsSavedDialog,
|
||||
getMessageContent,
|
||||
getMessageText,
|
||||
groupMessageIdsByThreadId,
|
||||
isActionMessage,
|
||||
isMessageLocal,
|
||||
} from '../../helpers';
|
||||
@ -49,9 +50,7 @@ import {
|
||||
deleteQuickReplyMessages,
|
||||
deleteTopic,
|
||||
removeChatFromChatLists,
|
||||
replaceThreadParam,
|
||||
replaceWebPage,
|
||||
updateChat,
|
||||
updateChatLastMessageId,
|
||||
updateChatMediaLoadingState,
|
||||
updateChatMessage,
|
||||
@ -63,13 +62,17 @@ import {
|
||||
updateQuickReplies,
|
||||
updateQuickReplyMessage,
|
||||
updateScheduledMessage,
|
||||
updateThreadInfo,
|
||||
updateThreadInfos,
|
||||
updateThreadUnreadFromForwardedMessage,
|
||||
updateTopic,
|
||||
} from '../../reducers';
|
||||
import { updateUnreadReactions } from '../../reducers/reactions';
|
||||
import { addUnreadReactions, removeUnreadReactions } from '../../reducers/reactions';
|
||||
import { updateTabState } from '../../reducers/tabs';
|
||||
import {
|
||||
replaceThreadLocalStateParam,
|
||||
replaceThreadReadStateParam,
|
||||
updateThreadInfo,
|
||||
updateThreadInfoLastMessageId,
|
||||
updateThreadInfoMessagesCount,
|
||||
updateThreadReadState,
|
||||
} from '../../reducers/threads';
|
||||
import {
|
||||
selectCanAnimateSnapEffect,
|
||||
selectChat,
|
||||
@ -88,19 +91,22 @@ import {
|
||||
selectListedIds,
|
||||
selectPerformanceSettingsValue,
|
||||
selectPinnedIds,
|
||||
selectSavedDialogIdFromMessage,
|
||||
selectScheduledIds,
|
||||
selectScheduledMessage,
|
||||
selectTabState,
|
||||
selectThread,
|
||||
selectThreadByMessage,
|
||||
selectThreadIdFromMessage,
|
||||
selectThreadInfo,
|
||||
selectThreadParam,
|
||||
selectTopic,
|
||||
selectTopicFromMessage,
|
||||
selectViewportIds,
|
||||
} from '../../selectors';
|
||||
import {
|
||||
selectSavedDialogIdFromMessage,
|
||||
selectThread,
|
||||
selectThreadByMessage,
|
||||
selectThreadIdFromMessage,
|
||||
selectThreadInfo,
|
||||
selectThreadLocalStateParam,
|
||||
selectThreadReadState,
|
||||
} from '../../selectors/threads';
|
||||
|
||||
const ANIMATION_DELAY = 350;
|
||||
const SNAP_ANIMATION_DELAY = 1000;
|
||||
@ -114,7 +120,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
chatId, id, message, shouldForceReply, wasDrafted, poll, webPage,
|
||||
} = update;
|
||||
global = updateWithLocalMedia(global, chatId, id, true, message);
|
||||
global = updateListedAndViewportIds(global, actions, message as ApiMessage);
|
||||
global = updateListedAndViewportIds(global, message);
|
||||
|
||||
const newMessage = selectChatMessage(global, chatId, id)!;
|
||||
const replyInfo = getMessageReplyInfo(newMessage);
|
||||
@ -127,7 +133,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
actions.loadTopicById({ chatId, topicId: replyInfo.replyToMsgId });
|
||||
}
|
||||
|
||||
const isLocal = isMessageLocal(message as ApiMessage);
|
||||
const isLocal = isMessageLocal(message);
|
||||
|
||||
Object.values(global.byTabId).forEach(({ id: tabId }) => {
|
||||
// Force update for last message on drafted messages to prevent flickering
|
||||
@ -138,7 +144,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
const threadId = selectThreadIdFromMessage(global, newMessage);
|
||||
global = updateChatMediaLoadingState(global, newMessage, chatId, threadId, tabId);
|
||||
|
||||
if (selectIsMessageInCurrentMessageList(global, chatId, message as ApiMessage, tabId)) {
|
||||
if (selectIsMessageInCurrentMessageList(global, chatId, message, tabId)) {
|
||||
if (isLocal && message.isOutgoing && !(message.content?.action) && !storyReplyInfo?.storyId
|
||||
&& !message.content?.storyData) {
|
||||
const currentMessageList = selectCurrentMessageList(global, tabId);
|
||||
@ -147,7 +153,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
actions.focusMessage({
|
||||
chatId,
|
||||
threadId: currentMessageList.threadId,
|
||||
messageId: message.id!,
|
||||
messageId: message.id,
|
||||
noHighlight: true,
|
||||
isResizingContainer: true,
|
||||
tabId,
|
||||
@ -189,10 +195,10 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
|
||||
if (chat?.isBotForum && !newMessage.isOutgoing && !isLocal) {
|
||||
const threadId = selectThreadIdFromMessage(global, newMessage);
|
||||
const typingDraftStore = selectThreadParam(global, chatId, threadId, 'typingDraftIdByRandomId');
|
||||
const typingDraftStore = selectThreadLocalStateParam(global, chatId, threadId, 'typingDraftIdByRandomId');
|
||||
const localDraftIds = Object.values(typingDraftStore || {});
|
||||
global = deleteChatMessages(global, chatId, localDraftIds);
|
||||
global = replaceThreadParam(global, chatId, threadId, 'typingDraftIdByRandomId', undefined);
|
||||
global = replaceThreadLocalStateParam(global, chatId, threadId, 'typingDraftIdByRandomId', undefined);
|
||||
}
|
||||
|
||||
setGlobal(global);
|
||||
@ -256,12 +262,16 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
global = updateWithLocalMedia(global, chatId, id, true, message, true);
|
||||
|
||||
const scheduledIds = selectScheduledIds(global, chatId, MAIN_THREAD_ID) || [];
|
||||
global = replaceThreadParam(global, chatId, MAIN_THREAD_ID, 'scheduledIds', unique([...scheduledIds, id]));
|
||||
global = replaceThreadLocalStateParam(
|
||||
global, chatId, MAIN_THREAD_ID, 'scheduledIds', unique([...scheduledIds, id]),
|
||||
);
|
||||
|
||||
const threadId = selectThreadIdFromMessage(global, message);
|
||||
if (threadId !== MAIN_THREAD_ID) {
|
||||
const threadScheduledIds = selectScheduledIds(global, chatId, threadId) || [];
|
||||
global = replaceThreadParam(global, chatId, threadId, 'scheduledIds', unique([...threadScheduledIds, id]));
|
||||
global = replaceThreadLocalStateParam(
|
||||
global, chatId, threadId, 'scheduledIds', unique([...threadScheduledIds, id]),
|
||||
);
|
||||
}
|
||||
|
||||
if (poll) {
|
||||
@ -303,12 +313,14 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
|
||||
global = updateWithLocalMedia(global, chatId, id, false, message, true);
|
||||
const ids = Object.keys(selectChatScheduledMessages(global, chatId) || {}).map(Number).sort((a, b) => b - a);
|
||||
global = replaceThreadParam(global, chatId, MAIN_THREAD_ID, 'scheduledIds', ids);
|
||||
global = replaceThreadLocalStateParam(global, chatId, MAIN_THREAD_ID, 'scheduledIds', ids);
|
||||
|
||||
const threadId = selectThreadIdFromMessage(global, currentMessage);
|
||||
if (threadId !== MAIN_THREAD_ID) {
|
||||
const threadScheduledIds = selectScheduledIds(global, chatId, threadId) || [];
|
||||
global = replaceThreadParam(global, chatId, threadId, 'scheduledIds', threadScheduledIds.sort((a, b) => b - a));
|
||||
global = replaceThreadLocalStateParam(
|
||||
global, chatId, threadId, 'scheduledIds', [...threadScheduledIds].sort((a, b) => b - a),
|
||||
);
|
||||
}
|
||||
if (poll) {
|
||||
global = updatePoll(global, poll.id, poll);
|
||||
@ -325,13 +337,31 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
|
||||
case 'updateMessage': {
|
||||
const {
|
||||
chatId, id, message, poll, webPage, isFromNew, shouldForceReply,
|
||||
chatId, id, message, poll, webPage, isFromNew, isFull, shouldForceReply,
|
||||
} = update;
|
||||
|
||||
const currentMessage = selectChatMessage(global, chatId, id);
|
||||
|
||||
if (message.reactions) {
|
||||
global = updateReactions(
|
||||
global, actions, {
|
||||
chatId,
|
||||
id,
|
||||
reactions: message.reactions,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if (poll) {
|
||||
global = updatePoll(global, poll.id, poll);
|
||||
}
|
||||
|
||||
if (webPage) {
|
||||
global = replaceWebPage(global, webPage.id, webPage);
|
||||
}
|
||||
|
||||
if (!currentMessage) {
|
||||
if (isFromNew) {
|
||||
if (isFromNew && isFull) {
|
||||
actions.apiUpdate({
|
||||
'@type': 'newMessage',
|
||||
id: update.id,
|
||||
@ -342,33 +372,21 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
shouldForceReply,
|
||||
});
|
||||
}
|
||||
|
||||
// If update contains the full message, store it
|
||||
if (update.isFull) {
|
||||
global = addMessages(global, [update.message]);
|
||||
}
|
||||
setGlobal(global);
|
||||
return;
|
||||
}
|
||||
|
||||
const chat = selectChat(global, chatId);
|
||||
|
||||
global = updateWithLocalMedia(global, chatId, id, false, message);
|
||||
|
||||
const newMessage = selectChatMessage(global, chatId, id)!;
|
||||
|
||||
if (message.reactions && chat) {
|
||||
global = updateReactions(
|
||||
global, actions, chatId, id, message.reactions, chat, newMessage.isOutgoing, currentMessage,
|
||||
);
|
||||
}
|
||||
|
||||
if (message.content?.text?.text !== currentMessage?.content?.text?.text) {
|
||||
global = clearMessageTranslation(global, chatId, id);
|
||||
global = clearMessageSummary(global, chatId, id);
|
||||
}
|
||||
|
||||
if (poll) {
|
||||
global = updatePoll(global, poll.id, poll);
|
||||
}
|
||||
|
||||
if (webPage) {
|
||||
global = replaceWebPage(global, webPage.id, webPage);
|
||||
}
|
||||
global = updateWithLocalMedia(global, chatId, id, false, message);
|
||||
|
||||
setGlobal(global);
|
||||
|
||||
@ -465,7 +483,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
chatId, localId, message, poll,
|
||||
} = update;
|
||||
|
||||
global = updateListedAndViewportIds(global, actions, message);
|
||||
global = updateListedAndViewportIds(global, message);
|
||||
|
||||
const currentMessage = selectChatMessage(global, chatId, localId);
|
||||
|
||||
@ -509,17 +527,12 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
actions.markMessageListRead({ maxId: message.id, tabId });
|
||||
});
|
||||
if (thread?.threadInfo?.threadId) {
|
||||
global = replaceThreadParam(global, chatId, thread.threadInfo.threadId, 'threadInfo', {
|
||||
...thread.threadInfo,
|
||||
lastMessageId: message.id,
|
||||
lastReadInboxMessageId: message.id,
|
||||
});
|
||||
global = replaceThreadReadStateParam(
|
||||
global, chatId, thread.threadInfo.threadId, 'lastReadInboxMessageId', message.id,
|
||||
);
|
||||
global = updateThreadInfoLastMessageId(global, chatId, thread.threadInfo.threadId, message.id);
|
||||
}
|
||||
|
||||
global = updateChat(global, chatId, {
|
||||
lastReadInboxMessageId: message.id,
|
||||
});
|
||||
|
||||
const chat = selectChat(global, chatId);
|
||||
// Reload dialogs if chat is not present in the list
|
||||
if (!chat?.isNotJoined && !selectIsChatListed(global, chatId)) {
|
||||
@ -543,12 +556,16 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
chatId, localId, message, poll,
|
||||
} = update;
|
||||
const scheduledIds = selectScheduledIds(global, chatId, MAIN_THREAD_ID) || [];
|
||||
global = replaceThreadParam(global, chatId, MAIN_THREAD_ID, 'scheduledIds', [...scheduledIds, message.id]);
|
||||
global = replaceThreadLocalStateParam(
|
||||
global, chatId, MAIN_THREAD_ID, 'scheduledIds', [...scheduledIds, message.id],
|
||||
);
|
||||
|
||||
const threadId = selectThreadIdFromMessage(global, message);
|
||||
if (threadId !== MAIN_THREAD_ID) {
|
||||
const threadScheduledIds = selectScheduledIds(global, chatId, threadId) || [];
|
||||
global = replaceThreadParam(global, chatId, threadId, 'scheduledIds', [...threadScheduledIds, message.id]);
|
||||
global = replaceThreadLocalStateParam(
|
||||
global, chatId, threadId, 'scheduledIds', [...threadScheduledIds, message.id],
|
||||
);
|
||||
}
|
||||
|
||||
const currentMessage = selectScheduledMessage(global, chatId, localId);
|
||||
@ -572,27 +589,14 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
case 'updatePinnedIds': {
|
||||
const { chatId, isPinned, messageIds } = update;
|
||||
|
||||
const messages = pickTruthy(selectChatMessages(global, chatId), messageIds);
|
||||
const updatePerThread: Record<ThreadId, number[]> = {
|
||||
[MAIN_THREAD_ID]: messageIds,
|
||||
};
|
||||
Object.values(messages).forEach((message) => {
|
||||
const threadId = selectThreadIdFromMessage(global, message);
|
||||
global = updateChatMessage(global, chatId, message.id, {
|
||||
isPinned,
|
||||
});
|
||||
if (threadId === MAIN_THREAD_ID) return;
|
||||
const currentUpdatedInThread = updatePerThread[threadId] || [];
|
||||
currentUpdatedInThread.push(message.id);
|
||||
updatePerThread[threadId] = currentUpdatedInThread;
|
||||
});
|
||||
const messageIdsByThreadId = groupMessageIdsByThreadId(global, chatId, messageIds, false);
|
||||
|
||||
Object.entries(updatePerThread).forEach(([threadId, ids]) => {
|
||||
const pinnedIds = selectPinnedIds(global, chatId, MAIN_THREAD_ID) || [];
|
||||
Object.entries(messageIdsByThreadId).forEach(([threadId, ids]) => {
|
||||
const pinnedIds = selectPinnedIds(global, chatId, threadId) || [];
|
||||
const newPinnedIds = isPinned
|
||||
? unique(pinnedIds.concat(ids)).sort((a, b) => b - a)
|
||||
: pinnedIds.filter((id) => !ids.includes(id));
|
||||
global = replaceThreadParam(global, chatId, Number(threadId), 'pinnedIds', newPinnedIds);
|
||||
global = replaceThreadLocalStateParam(global, chatId, threadId, 'pinnedIds', newPinnedIds);
|
||||
});
|
||||
setGlobal(global);
|
||||
|
||||
@ -604,36 +608,18 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
threadInfo,
|
||||
} = update;
|
||||
|
||||
global = updateThreadInfos(global, [threadInfo]);
|
||||
global = updateThreadInfo(global, threadInfo);
|
||||
setGlobal(global);
|
||||
global = getGlobal();
|
||||
|
||||
const { chatId, threadId } = threadInfo;
|
||||
if (!chatId || !threadId) return;
|
||||
break;
|
||||
}
|
||||
|
||||
const chat = selectChat(global, chatId);
|
||||
const currentThreadInfo = selectThreadInfo(global, chatId, threadId);
|
||||
const topic = selectTopic(global, chatId, threadId);
|
||||
if (chat?.isForum) {
|
||||
if (!topic || topic.lastMessageId !== currentThreadInfo?.lastReadInboxMessageId) {
|
||||
actions.loadTopicById({ chatId, topicId: Number(threadId) });
|
||||
} else {
|
||||
global = updateTopic(global, chatId, Number(threadId), {
|
||||
unreadCount: 0,
|
||||
});
|
||||
}
|
||||
}
|
||||
case 'updateThreadReadState': {
|
||||
const {
|
||||
chatId, threadId, readState,
|
||||
} = update;
|
||||
|
||||
// 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),
|
||||
});
|
||||
}
|
||||
}
|
||||
global = updateThreadReadState(global, chatId, threadId, readState);
|
||||
setGlobal(global);
|
||||
|
||||
break;
|
||||
@ -803,13 +789,11 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
}
|
||||
|
||||
case 'updateMessageReactions': {
|
||||
const { chatId, id, reactions } = update;
|
||||
const message = selectChatMessage(global, chatId, id);
|
||||
const chat = selectChat(global, update.chatId);
|
||||
const { chatId, id, threadId, reactions } = update;
|
||||
|
||||
if (!chat || !message) return;
|
||||
|
||||
global = updateReactions(global, actions, chatId, id, reactions, chat, message.isOutgoing, message);
|
||||
global = updateReactions(global, actions, {
|
||||
chatId, id, threadId, reactions,
|
||||
});
|
||||
setGlobal(global);
|
||||
break;
|
||||
}
|
||||
@ -939,7 +923,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
const thread = selectThread(global, chatId, threadId);
|
||||
if (!thread) return undefined;
|
||||
|
||||
let typingDraftStore = selectThreadParam(global, chatId, threadId, 'typingDraftIdByRandomId');
|
||||
let typingDraftStore = selectThreadLocalStateParam(global, chatId, threadId, 'typingDraftIdByRandomId');
|
||||
const messageId = typingDraftStore?.[id];
|
||||
|
||||
const isUpdatingDraft = Boolean(messageId);
|
||||
@ -949,7 +933,9 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
// Clear typing draft after timeout
|
||||
setTimeout(() => {
|
||||
global = getGlobal();
|
||||
const currentTypingDraftStore = selectThreadParam(global, chatId, threadId, 'typingDraftIdByRandomId');
|
||||
const currentTypingDraftStore = selectThreadLocalStateParam(
|
||||
global, chatId, threadId, 'typingDraftIdByRandomId',
|
||||
);
|
||||
if (currentTypingDraftStore?.[id]) {
|
||||
const currentMessageId = currentTypingDraftStore[id];
|
||||
const currentMessage = selectChatMessage(global, chatId, currentMessageId);
|
||||
@ -957,7 +943,9 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
if (!currentMessage || getServerTime() - currentMessage.editDate! < global.appConfig.typingDraftTtl) return;
|
||||
|
||||
const newTypingDraftIds = omit(currentTypingDraftStore, [id]);
|
||||
global = replaceThreadParam(global, chatId, threadId, 'typingDraftIdByRandomId', newTypingDraftIds);
|
||||
global = replaceThreadLocalStateParam(
|
||||
global, chatId, threadId, 'typingDraftIdByRandomId', newTypingDraftIds,
|
||||
);
|
||||
global = deleteChatMessages(global, chatId, [currentMessageId]);
|
||||
setGlobal(global);
|
||||
}
|
||||
@ -998,7 +986,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
...typingDraftStore,
|
||||
[id]: newMessage.id,
|
||||
};
|
||||
global = replaceThreadParam(global, chatId, threadId, 'typingDraftIdByRandomId', typingDraftStore);
|
||||
global = replaceThreadLocalStateParam(global, chatId, threadId, 'typingDraftIdByRandomId', typingDraftStore);
|
||||
|
||||
rescheduleDraftRemoval();
|
||||
|
||||
@ -1010,13 +998,30 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
function updateReactions<T extends GlobalState>(
|
||||
global: T,
|
||||
actions: RequiredGlobalActions,
|
||||
chatId: string,
|
||||
id: number,
|
||||
reactions: ApiReactions,
|
||||
chat: ApiChat,
|
||||
isOutgoing?: boolean,
|
||||
message?: ApiMessage,
|
||||
{
|
||||
chatId, id, threadId, reactions,
|
||||
}: {
|
||||
chatId: string;
|
||||
id: number;
|
||||
threadId?: ThreadId;
|
||||
reactions: ApiReactions;
|
||||
},
|
||||
): T {
|
||||
const chat = selectChat(global, chatId);
|
||||
const message = selectChatMessage(global, chatId, id);
|
||||
|
||||
if (!chat || !message) {
|
||||
// Simplified logic to update counter only
|
||||
const hasUnread = checkIfHasUnreadReactions(global, reactions);
|
||||
if (hasUnread) {
|
||||
global = addUnreadReactions({ global, chatId, ids: [id] });
|
||||
} else {
|
||||
// Reload unread reactions to update counter
|
||||
actions.loadUnreadReactions({ chatId, threadId });
|
||||
}
|
||||
return global;
|
||||
}
|
||||
|
||||
const currentReactions = message?.reactions;
|
||||
|
||||
// `updateMessageReactions` happens with an interval, so we try to avoid redundant global state updates
|
||||
@ -1033,7 +1038,7 @@ function updateReactions<T extends GlobalState>(
|
||||
|
||||
global = updateChatMessage(global, chatId, id, { reactions });
|
||||
|
||||
if (!isOutgoing) {
|
||||
if (!message.isOutgoing) {
|
||||
return global;
|
||||
}
|
||||
|
||||
@ -1045,15 +1050,12 @@ function updateReactions<T extends GlobalState>(
|
||||
actions.startActiveReaction({ containerId: messageKey, reaction, tabId: getCurrentTabId() });
|
||||
}
|
||||
|
||||
const hasUnreadReactionsForMessageInChat = chat.unreadReactions?.includes(id);
|
||||
const hasUnreadReactionsForMessageInChat = message.reactions && checkIfHasUnreadReactions(global, message.reactions);
|
||||
const hasUnreadReactionsInNewReactions = checkIfHasUnreadReactions(global, reactions);
|
||||
|
||||
// Only notify about added reactions, not removed ones
|
||||
if (hasUnreadReactionsInNewReactions && !hasUnreadReactionsForMessageInChat) {
|
||||
global = updateUnreadReactions(global, chatId, {
|
||||
unreadReactionsCount: (chat?.unreadReactionsCount || 0) + 1,
|
||||
unreadReactions: [...(chat?.unreadReactions || []), id].sort((a, b) => b - a),
|
||||
});
|
||||
global = addUnreadReactions({ global, chatId, ids: [id] });
|
||||
|
||||
const newMessage = selectChatMessage(global, chatId, id);
|
||||
|
||||
@ -1069,10 +1071,7 @@ function updateReactions<T extends GlobalState>(
|
||||
}
|
||||
|
||||
if (!hasUnreadReactionsInNewReactions && hasUnreadReactionsForMessageInChat) {
|
||||
global = updateUnreadReactions(global, chatId, {
|
||||
unreadReactionsCount: (chat?.unreadReactionsCount || 1) - 1,
|
||||
unreadReactions: chat?.unreadReactions?.filter((i) => i !== id),
|
||||
});
|
||||
global = removeUnreadReactions({ global, chatId, ids: [id] });
|
||||
}
|
||||
|
||||
return global;
|
||||
@ -1117,48 +1116,17 @@ export function updateWithLocalMedia(
|
||||
: updateChatMessage(global, chatId, id, newMessage);
|
||||
}
|
||||
|
||||
function updateThreadUnread<T extends GlobalState>(
|
||||
global: T, actions: RequiredGlobalActions, message: ApiMessage, isDeleting?: boolean,
|
||||
) {
|
||||
const { chatId } = message;
|
||||
|
||||
const replyInfo = getMessageReplyInfo(message);
|
||||
|
||||
const { threadInfo } = selectThreadByMessage(global, message) || {};
|
||||
|
||||
if (!threadInfo && replyInfo?.replyToMsgId) {
|
||||
const originMessage = selectChatMessage(global, chatId, replyInfo.replyToMsgId);
|
||||
if (originMessage) {
|
||||
global = updateThreadUnreadFromForwardedMessage(global, originMessage, chatId, message.id, isDeleting);
|
||||
} else {
|
||||
actions.loadMessage({
|
||||
chatId,
|
||||
messageId: replyInfo.replyToMsgId,
|
||||
threadUpdate: {
|
||||
isDeleting,
|
||||
lastMessageId: message.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return global;
|
||||
}
|
||||
|
||||
function updateListedAndViewportIds<T extends GlobalState>(
|
||||
global: T, actions: RequiredGlobalActions, message: ApiMessage,
|
||||
global: T, message: ApiMessage,
|
||||
) {
|
||||
const { id, chatId } = message;
|
||||
|
||||
const savedDialogId = selectSavedDialogIdFromMessage(global, message);
|
||||
const threadId = savedDialogId || selectThreadIdFromMessage(global, message);
|
||||
const threadInfo = selectThreadInfo(global, chatId, threadId);
|
||||
|
||||
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 ?? { threadId: savedDialogId };
|
||||
const mainThreadReadState = selectThreadReadState(global, chatId, MAIN_THREAD_ID);
|
||||
const isUnreadChatNotLoaded = mainThreadReadState?.unreadCount && !selectListedIds(global, chatId, MAIN_THREAD_ID);
|
||||
|
||||
if (threadId) {
|
||||
global = updateListedIds(global, chatId, threadId, [id]);
|
||||
@ -1177,15 +1145,11 @@ function updateListedAndViewportIds<T extends GlobalState>(
|
||||
});
|
||||
|
||||
if (threadInfo) {
|
||||
global = replaceThreadParam(global, chatId, threadId, 'threadInfo', {
|
||||
...threadInfo,
|
||||
lastMessageId: message.id,
|
||||
});
|
||||
global = updateThreadInfoLastMessageId(global, chatId, threadId, message.id);
|
||||
|
||||
if (!isMessageLocal(message) && !isActionMessage(message)) {
|
||||
global = updateThreadInfo(global, chatId, threadId, {
|
||||
messagesCount: (threadInfo.messagesCount || 0) + 1,
|
||||
});
|
||||
const newCount = (threadInfo.messagesCount || 0) + 1;
|
||||
global = updateThreadInfoMessagesCount(global, chatId, threadId, newCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1218,21 +1182,10 @@ function updateChatLastMessage<T extends GlobalState>(
|
||||
message: ApiMessage,
|
||||
force = false,
|
||||
) {
|
||||
const { chats } = global;
|
||||
const chat = chats.byId[chatId];
|
||||
const currentLastMessageId = selectChatLastMessageId(global, chatId);
|
||||
|
||||
const topic = chat?.isForum ? selectTopicFromMessage(global, message) : undefined;
|
||||
if (topic) {
|
||||
global = updateTopic(global, chatId, topic.id, {
|
||||
lastMessageId: message.id,
|
||||
});
|
||||
}
|
||||
|
||||
const threadId = selectThreadIdFromMessage(global, message);
|
||||
global = updateThreadInfo(global, chatId, threadId, {
|
||||
lastMessageId: message.id,
|
||||
});
|
||||
global = updateThreadInfoLastMessageId(global, chatId, threadId, message.id);
|
||||
|
||||
const savedDialogId = selectSavedDialogIdFromMessage(global, message);
|
||||
if (savedDialogId) {
|
||||
@ -1345,8 +1298,6 @@ export function deleteMessages<T extends GlobalState>(
|
||||
global = deletePeerPhoto(global, chatId, message.content.action.photo.id, true);
|
||||
}
|
||||
|
||||
global = updateThreadUnread(global, actions, message, true);
|
||||
|
||||
const threadId = selectThreadIdFromMessage(global, message);
|
||||
if (threadId) {
|
||||
threadIdsToUpdate.add(threadId);
|
||||
@ -1358,14 +1309,17 @@ export function deleteMessages<T extends GlobalState>(
|
||||
const idsSet = new Set(ids);
|
||||
|
||||
threadIdsToUpdate.forEach((threadId) => {
|
||||
if (chat.isForum && threadId !== MAIN_THREAD_ID) {
|
||||
// Refresh unread count
|
||||
actions.loadTopicById({ chatId, topicId: Number(threadId) });
|
||||
}
|
||||
|
||||
const threadInfo = selectThreadInfo(global, chatId, threadId);
|
||||
if (!threadInfo?.lastMessageId || !idsSet.has(threadInfo.lastMessageId)) return;
|
||||
|
||||
const newLastMessage = findLastMessage(global, chatId, threadId);
|
||||
|
||||
if (!newLastMessage) {
|
||||
if (chat.isForum && threadId !== MAIN_THREAD_ID) {
|
||||
actions.loadTopicById({ chatId, topicId: Number(threadId) });
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1373,15 +1327,7 @@ export function deleteMessages<T extends GlobalState>(
|
||||
global = updateChatLastMessage(global, chatId, newLastMessage, true);
|
||||
}
|
||||
|
||||
global = updateThreadInfo(global, chatId, threadId, {
|
||||
lastMessageId: newLastMessage.id,
|
||||
});
|
||||
|
||||
if (chat.isForum) {
|
||||
global = updateTopic(global, chatId, Number(threadId), {
|
||||
lastMessageId: newLastMessage.id,
|
||||
});
|
||||
}
|
||||
global = updateThreadInfoLastMessageId(global, chatId, threadId, newLastMessage.id);
|
||||
});
|
||||
|
||||
setGlobal(global);
|
||||
@ -1466,10 +1412,6 @@ function deleteScheduledMessages<T extends GlobalState>(
|
||||
setTimeout(() => {
|
||||
global = getGlobal();
|
||||
global = deleteChatScheduledMessages(global, chatId, ids);
|
||||
const scheduledMessages = selectChatScheduledMessages(global, chatId);
|
||||
global = replaceThreadParam(
|
||||
global, chatId, MAIN_THREAD_ID, 'scheduledIds', Object.keys(scheduledMessages || {}).map(Number),
|
||||
);
|
||||
setGlobal(global);
|
||||
}, isAnimatingAsSnap ? SNAP_ANIMATION_DELAY : ANIMATION_DELAY);
|
||||
}
|
||||
|
||||
@ -21,9 +21,9 @@ import {
|
||||
updatePeersWithStories,
|
||||
updatePoll,
|
||||
updateStealthMode,
|
||||
updateThreadInfos,
|
||||
} from '../../reducers';
|
||||
import { updateTabState } from '../../reducers/tabs';
|
||||
import { updateThreadInfo } from '../../reducers/threads';
|
||||
import {
|
||||
selectPeer,
|
||||
selectPeerStories,
|
||||
@ -39,7 +39,11 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
} = update;
|
||||
if (users) global = addUsers(global, users);
|
||||
if (chats) global = addChats(global, chats);
|
||||
if (threadInfos) global = updateThreadInfos(global, threadInfos);
|
||||
if (threadInfos) {
|
||||
threadInfos.forEach((threadInfo) => {
|
||||
global = updateThreadInfo(global, threadInfo);
|
||||
});
|
||||
}
|
||||
if (polls) {
|
||||
polls.forEach((poll) => {
|
||||
global = updatePoll(global, poll.id, poll);
|
||||
@ -305,7 +309,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
if (receiver) {
|
||||
actions.focusMessage({
|
||||
chatId: receiver.id,
|
||||
messageId: update.message.id!,
|
||||
messageId: update.message.id,
|
||||
tabId,
|
||||
});
|
||||
|
||||
|
||||
@ -7,9 +7,10 @@ import { createMessageHashUrl } from '../../../util/routing';
|
||||
import { addActionHandler, execAfterActions, getGlobal, setGlobal } from '../../index';
|
||||
import {
|
||||
closeMiddleSearch,
|
||||
exitMessageSelectMode, replaceTabThreadParam, updateCurrentMessageList, updateRequestedChatTranslation,
|
||||
exitMessageSelectMode, updateCurrentMessageList, updateRequestedChatTranslation,
|
||||
} from '../../reducers';
|
||||
import { updateTabState } from '../../reducers/tabs';
|
||||
import { replaceTabThreadParam } from '../../reducers/threads';
|
||||
import {
|
||||
selectChat, selectCurrentMessageList, selectTabState,
|
||||
} from '../../selectors';
|
||||
|
||||
@ -37,12 +37,11 @@ import {
|
||||
cancelMessageMediaDownload,
|
||||
enterMessageSelectMode,
|
||||
exitMessageSelectMode,
|
||||
replaceTabThreadParam,
|
||||
replaceThreadParam,
|
||||
toggleMessageSelection,
|
||||
updateFocusedMessage,
|
||||
} from '../../reducers';
|
||||
import { updateTabState } from '../../reducers/tabs';
|
||||
import { replaceTabThreadParam, replaceThreadLocalStateParam } from '../../reducers/threads';
|
||||
import {
|
||||
selectAllowedMessageActionsSlow,
|
||||
selectCanForwardMessage,
|
||||
@ -53,20 +52,18 @@ import {
|
||||
selectChatScheduledMessages,
|
||||
selectCurrentChat,
|
||||
selectCurrentMessageList,
|
||||
selectDraft,
|
||||
selectForwardedMessageIdsByGroupId,
|
||||
selectIsRightColumnShown,
|
||||
selectIsViewportNewest,
|
||||
selectMessageIdsByGroupId,
|
||||
selectReplyStack,
|
||||
selectRequestedChatTranslationLanguage,
|
||||
selectRequestedMessageTranslationLanguage,
|
||||
selectSender,
|
||||
selectTabState,
|
||||
selectThreadInfo,
|
||||
selectViewportIds,
|
||||
} from '../../selectors';
|
||||
import { selectMessageDownloadableMedia } from '../../selectors/media';
|
||||
import { selectDraft, selectReplyStack, selectThreadInfo } from '../../selectors/threads';
|
||||
import { getPeerStarsForMessage } from '../api/messages';
|
||||
|
||||
import { getIsMobile } from '../../../hooks/useAppLayout';
|
||||
@ -84,7 +81,7 @@ addActionHandler('setScrollOffset', (global, actions, payload): ActionReturnType
|
||||
chatId, threadId, scrollOffset, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
|
||||
global = replaceThreadParam(global, chatId, threadId, 'lastScrollOffset', scrollOffset);
|
||||
global = replaceThreadLocalStateParam(global, chatId, threadId, 'lastScrollOffset', scrollOffset);
|
||||
|
||||
return replaceTabThreadParam(global, chatId, threadId, 'scrollOffset', scrollOffset, tabId);
|
||||
});
|
||||
@ -99,7 +96,7 @@ addActionHandler('setEditingId', (global, actions, payload): ActionReturnType =>
|
||||
const { chatId, threadId, type } = currentMessageList;
|
||||
const paramName = type === 'scheduled' ? 'editingScheduledId' : 'editingId';
|
||||
|
||||
return replaceThreadParam(global, chatId, threadId, paramName, messageId);
|
||||
return replaceThreadLocalStateParam(global, chatId, threadId, paramName, messageId);
|
||||
});
|
||||
|
||||
addActionHandler('setEditingDraft', (global, actions, payload): ActionReturnType => {
|
||||
@ -109,7 +106,7 @@ addActionHandler('setEditingDraft', (global, actions, payload): ActionReturnType
|
||||
|
||||
const paramName = type === 'scheduled' ? 'editingScheduledDraft' : 'editingDraft';
|
||||
|
||||
return replaceThreadParam(global, chatId, threadId, paramName, text);
|
||||
return replaceThreadLocalStateParam(global, chatId, threadId, paramName, text);
|
||||
});
|
||||
|
||||
addActionHandler('editLastMessage', (global, actions, payload): ActionReturnType => {
|
||||
@ -133,7 +130,7 @@ addActionHandler('editLastMessage', (global, actions, payload): ActionReturnType
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return replaceThreadParam(global, chatId, threadId, 'editingId', lastOwnEditableMessageId);
|
||||
return replaceThreadLocalStateParam(global, chatId, threadId, 'editingId', lastOwnEditableMessageId);
|
||||
});
|
||||
|
||||
addActionHandler('replyToNextMessage', (global, actions, payload): ActionReturnType => {
|
||||
|
||||
@ -33,6 +33,7 @@ import { GLOBAL_STATE_CACHE_KEY } from '../util/multiaccount';
|
||||
import { encryptSession } from '../util/passcode';
|
||||
import { onBeforeUnload, throttle } from '../util/schedulers';
|
||||
import { hasStoredSession } from '../util/sessions';
|
||||
import { selectThreadInfo } from './selectors/threads';
|
||||
import { addActionHandler, getGlobal } from './index';
|
||||
import { INITIAL_GLOBAL_STATE, INITIAL_PERFORMANCE_STATE_MED } from './initialState';
|
||||
import { clearGlobalForLockScreen, clearSharedStateForLockScreen } from './reducers';
|
||||
@ -42,6 +43,7 @@ import {
|
||||
selectCurrentMessageList,
|
||||
selectFullWebPageFromMessage,
|
||||
selectTopics,
|
||||
selectTopicsInfo,
|
||||
selectViewportIds,
|
||||
selectVisibleUsers,
|
||||
} from './selectors';
|
||||
@ -364,6 +366,12 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
|
||||
untypedCached.sharedState.settings.shouldWarnAboutSvg = undefined;
|
||||
}
|
||||
|
||||
if (cached.cacheVersion < 3) {
|
||||
cached.cacheVersion = 3;
|
||||
cached.messages = initialState.messages;
|
||||
cached.chats.listIds = initialState.chats.listIds;
|
||||
}
|
||||
|
||||
if (!cached.auth) {
|
||||
cached.auth = initialState.auth;
|
||||
cached.auth.rememberMe = untypedCached.rememberMe;
|
||||
@ -674,18 +682,20 @@ function reduceMessages<T extends GlobalState>(global: T): GlobalState['messages
|
||||
|
||||
const chatLastMessageId = selectChatLastMessageId(global, chatId);
|
||||
|
||||
const topicsInfo = selectTopicsInfo(global, chatId);
|
||||
const openedThreadIds = Array.from(openedChatThreadIds[chatId] || []);
|
||||
const commentThreadIds = Object.values(global.messages.byChatId[chatId].threadsById || {})
|
||||
.map(({ threadInfo }) => (threadInfo?.isCommentsInfo ? threadInfo?.originMessageId : undefined))
|
||||
.filter(Boolean);
|
||||
const threadIds = unique(openedThreadIds.concat(commentThreadIds));
|
||||
const threadIds = unique(openedThreadIds.concat(commentThreadIds, topicsInfo?.listedTopicIds || []));
|
||||
|
||||
const topics = selectTopics(global, chatId);
|
||||
const threadsToSave = pickTruthy(current.threadsById, [MAIN_THREAD_ID, ...threadIds]);
|
||||
|
||||
const viewportIdsToSave = unique(Object.values(threadsToSave).flatMap((thread) => thread.lastViewportIds || []));
|
||||
const topics = selectTopics(global, chatId);
|
||||
const viewportIdsToSave = unique(Object.values(threadsToSave)
|
||||
.flatMap((thread) => thread.localState?.lastViewportIds || []));
|
||||
const topicLastMessageIds = topics && forumPanelChatIds.includes(chatId)
|
||||
? Object.values(topics).map(({ lastMessageId }) => lastMessageId) : [];
|
||||
? Object.values(topics).map(({ id }) => selectThreadInfo(global, chatId, id)?.lastMessageId).filter(Boolean) : [];
|
||||
const savedLastMessageIds = chatId === currentUserId && global.chats.lastMessageIds.saved
|
||||
? Object.values(global.chats.lastMessageIds.saved) : [];
|
||||
const lastMessageIdsToSave = [chatLastMessageId].concat(topicLastMessageIds).concat(savedLastMessageIds)
|
||||
@ -695,9 +705,11 @@ function reduceMessages<T extends GlobalState>(global: T): GlobalState['messages
|
||||
const thread = threadsToSave[Number(key)];
|
||||
acc[Number(key)] = {
|
||||
...thread,
|
||||
listedIds: thread.lastViewportIds,
|
||||
pinnedIds: undefined,
|
||||
typingStatus: undefined,
|
||||
localState: {
|
||||
...thread.localState,
|
||||
listedIds: thread.localState?.lastViewportIds,
|
||||
typingStatus: undefined,
|
||||
},
|
||||
};
|
||||
return acc;
|
||||
}, {} as GlobalState['messages']['byChatId'][string]['threadsById']);
|
||||
|
||||
@ -9,6 +9,7 @@ import type {
|
||||
ApiPeer,
|
||||
ApiPeerColorCollectible,
|
||||
ApiPreparedInlineMessage,
|
||||
ApiThreadInfo,
|
||||
ApiTopic,
|
||||
} from '../../api/types';
|
||||
import type { OldLangFn } from '../../hooks/useOldLang';
|
||||
@ -346,20 +347,28 @@ export function isChatPublic(chat: ApiChat) {
|
||||
}
|
||||
|
||||
export function getOrderedTopics(
|
||||
topics: ApiTopic[], pinnedOrder?: number[], shouldSortByLastMessage = false,
|
||||
topics: ApiTopic[],
|
||||
topicThreadInfos?: Record<ThreadId, ApiThreadInfo>,
|
||||
pinnedOrder?: number[],
|
||||
shouldSortByLastMessage = false,
|
||||
): ApiTopic[] {
|
||||
const lastMessageIdComparator = (a: ApiTopic, b: ApiTopic) => (
|
||||
(topicThreadInfos?.[b.id]?.lastMessageId || 0) - (topicThreadInfos?.[a.id]?.lastMessageId || 0)
|
||||
);
|
||||
|
||||
if (shouldSortByLastMessage) {
|
||||
return topics.sort((a, b) => b.lastMessageId - a.lastMessageId);
|
||||
return topics.sort(lastMessageIdComparator);
|
||||
} else {
|
||||
const pinned = topics.filter((topic) => topic.isPinned);
|
||||
const ordered = topics
|
||||
.filter((topic) => !topic.isPinned && !topic.isHidden)
|
||||
.sort((a, b) => b.lastMessageId - a.lastMessageId);
|
||||
.sort(lastMessageIdComparator);
|
||||
const hidden = topics.filter((topic) => !topic.isPinned && topic.isHidden)
|
||||
.sort((a, b) => b.lastMessageId - a.lastMessageId);
|
||||
.sort(lastMessageIdComparator);
|
||||
|
||||
const pinnedOrdered = pinnedOrder
|
||||
? pinnedOrder.map((id) => pinned.find((topic) => topic.id === id)).filter(Boolean) : pinned;
|
||||
? pinnedOrder.map((id) => pinned.find((topic) => topic.id === id)).filter(Boolean)
|
||||
: pinned;
|
||||
|
||||
return [...pinnedOrdered, ...ordered, ...hidden];
|
||||
}
|
||||
|
||||
@ -35,7 +35,13 @@ import { areSortedArraysIntersecting, unique } from '../../util/iteratees';
|
||||
import { isLocalMessageId } from '../../util/keys/messageKey';
|
||||
import { getServerTime } from '../../util/serverTime';
|
||||
import { getGlobal } from '../index';
|
||||
import { selectPollFromMessage, selectWebPageFromMessage } from '../selectors';
|
||||
import {
|
||||
selectChatMessage,
|
||||
selectPollFromMessage,
|
||||
selectScheduledMessage,
|
||||
selectWebPageFromMessage,
|
||||
} from '../selectors';
|
||||
import { selectThreadIdFromMessage } from '../selectors/threads';
|
||||
import { getMainUsername } from './users';
|
||||
|
||||
const RE_LINK = new RegExp(RE_LINK_TEMPLATE, 'i');
|
||||
@ -546,3 +552,24 @@ export function createApiMessageFromTypingDraft({
|
||||
editDate: getServerTime(),
|
||||
};
|
||||
}
|
||||
|
||||
export function groupMessageIdsByThreadId(
|
||||
global: GlobalState, chatId: string, messageIds: number[], isScheduled: boolean, notShared?: boolean,
|
||||
): Record<ThreadId, number[]> {
|
||||
const grouped = messageIds.reduce<Record<ThreadId, number[]>>((acc, messageId) => {
|
||||
const message = isScheduled ? selectScheduledMessage(global, chatId, messageId)
|
||||
: selectChatMessage(global, chatId, messageId);
|
||||
if (!message) return acc;
|
||||
|
||||
const threadId = selectThreadIdFromMessage(global, message);
|
||||
acc[threadId] = acc[threadId] || [];
|
||||
acc[threadId].push(messageId);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (!notShared) {
|
||||
grouped[MAIN_THREAD_ID] = messageIds;
|
||||
}
|
||||
|
||||
return grouped;
|
||||
}
|
||||
|
||||
@ -13,7 +13,7 @@ import type { LangFn } from '../../util/localization';
|
||||
import type { GlobalState } from '../types';
|
||||
|
||||
import { STARS_CURRENCY_CODE, TON_CURRENCY_CODE } from '../../config';
|
||||
import arePropsShallowEqual from '../../util/arePropsShallowEqual';
|
||||
import { areRecordsShallowEqual } from '../../util/areShallowEqual';
|
||||
import { convertTonFromNanos } from '../../util/formatCurrency';
|
||||
import { selectChat, selectPeer, selectUser } from '../selectors';
|
||||
|
||||
@ -494,5 +494,5 @@ export function getPrizeStarsTransactionFromGiveaway(message: ApiMessage): ApiSt
|
||||
}
|
||||
|
||||
export function areInputSavedGiftsEqual(one: ApiInputSavedStarGift, two: ApiInputSavedStarGift) {
|
||||
return arePropsShallowEqual(one, two);
|
||||
return areRecordsShallowEqual(one, two);
|
||||
}
|
||||
|
||||
@ -13,14 +13,15 @@ import { parseLocationHash } from '../util/routing';
|
||||
import { updatePeerColors } from '../util/theme';
|
||||
import { initializeChatMediaSearchResults } from './reducers/middleSearch';
|
||||
import { updateTabState } from './reducers/tabs';
|
||||
import { replaceTabThreadParam, replaceThreadLocalStateParam } from './reducers/threads';
|
||||
import { selectThreadLocalStateParam } from './selectors/threads';
|
||||
import { initSharedState } from './shared/sharedStateConnector';
|
||||
import { initCache } from './cache';
|
||||
import {
|
||||
addActionHandler, getGlobal, setGlobal,
|
||||
} from './index';
|
||||
import { INITIAL_TAB_STATE } from './initialState';
|
||||
import { replaceTabThreadParam, replaceThreadParam } from './reducers';
|
||||
import { selectTabState, selectThreadParam } from './selectors';
|
||||
import { selectTabState } from './selectors';
|
||||
|
||||
initCache();
|
||||
|
||||
@ -63,10 +64,10 @@ addActionHandler('init', (global, actions, payload): ActionReturnType => {
|
||||
const threadsById = global.messages.byChatId[chatId].threadsById;
|
||||
Object.keys(threadsById).forEach((thread) => {
|
||||
const threadId = Number(thread);
|
||||
const lastViewportIds = selectThreadParam(global, chatId, threadId, 'lastViewportIds');
|
||||
const lastViewportIds = selectThreadLocalStateParam(global, chatId, threadId, 'lastViewportIds');
|
||||
// Check if migration from previous version is faulty
|
||||
if (!lastViewportIds?.every((id) => isLocalMessageId(id) || global.messages.byChatId[chatId]?.byId[id])) {
|
||||
global = replaceThreadParam(global, chatId, threadId, 'lastViewportIds', undefined);
|
||||
global = replaceThreadLocalStateParam(global, chatId, threadId, 'lastViewportIds', undefined);
|
||||
return;
|
||||
}
|
||||
global = initializeChatMediaSearchResults(global, chatId, threadId, tabId);
|
||||
@ -85,10 +86,18 @@ addActionHandler('init', (global, actions, payload): ActionReturnType => {
|
||||
Object.keys(global.messages.byChatId).forEach((chatId) => {
|
||||
const threadsById = global.messages.byChatId[chatId].threadsById;
|
||||
const fixedThreadsById = Object.keys(threadsById).reduce((acc, key) => {
|
||||
const t = threadsById[Number(key)];
|
||||
acc[Number(key)] = {
|
||||
const t = threadsById[key];
|
||||
if (!t.localState?.lastViewportIds) {
|
||||
acc[key] = t;
|
||||
return acc;
|
||||
}
|
||||
|
||||
acc[key] = {
|
||||
...t,
|
||||
listedIds: t.lastViewportIds,
|
||||
localState: {
|
||||
...t.localState,
|
||||
listedIds: t.localState.lastViewportIds,
|
||||
},
|
||||
};
|
||||
return acc;
|
||||
}, {} as GlobalState['messages']['byChatId'][string]['threadsById']);
|
||||
|
||||
@ -96,7 +96,7 @@ export const INITIAL_SHARED_STATE: SharedState = {
|
||||
};
|
||||
|
||||
export const INITIAL_GLOBAL_STATE: GlobalState = {
|
||||
cacheVersion: 2,
|
||||
cacheVersion: 3,
|
||||
isInited: true,
|
||||
attachMenu: { bots: {} },
|
||||
passcode: {},
|
||||
|
||||
@ -1,15 +1,19 @@
|
||||
import type {
|
||||
ApiChat, ApiChatFullInfo, ApiChatMember,
|
||||
} from '../../api/types';
|
||||
import type { ChatListType } from '../../types';
|
||||
import type { GlobalState } from '../types';
|
||||
import {
|
||||
type ApiChat, type ApiChatFullInfo, type ApiChatMember,
|
||||
MAIN_THREAD_ID,
|
||||
} from '../../api/types';
|
||||
|
||||
import { ARCHIVED_FOLDER_ID } from '../../config';
|
||||
import { areDeepEqual } from '../../util/areDeepEqual';
|
||||
import {
|
||||
areSortedArraysEqual, buildCollectionByKey, omit, omitUndefined, pick, unique,
|
||||
} from '../../util/iteratees';
|
||||
import { groupMessageIdsByThreadId } from '../helpers';
|
||||
import { selectChatFullInfo } from '../selectors';
|
||||
import { selectThreadReadState } from '../selectors/threads';
|
||||
import { replaceThreadReadStateParam, updateThreadInfoLastMessageId } from './threads';
|
||||
|
||||
const DEFAULT_CHAT_LISTS: ChatListType[] = ['active', 'archived'];
|
||||
|
||||
@ -53,6 +57,11 @@ export function updateChatLastMessageId<T extends GlobalState>(
|
||||
global: T, chatId: string, lastMessageId: number, listType?: ChatListType,
|
||||
): T {
|
||||
const key = listType === 'saved' ? 'saved' : 'all';
|
||||
|
||||
if (key === 'all') {
|
||||
global = updateThreadInfoLastMessageId(global, chatId, MAIN_THREAD_ID, lastMessageId);
|
||||
}
|
||||
|
||||
return {
|
||||
...global,
|
||||
chats: {
|
||||
@ -115,49 +124,67 @@ export function replaceChats<T extends GlobalState>(global: T, newById: Record<s
|
||||
};
|
||||
}
|
||||
|
||||
export function addUnreadMentions<T extends GlobalState>(
|
||||
global: T, chatId: string, chat: ApiChat, ids: number[], shouldUpdateCount: boolean = false,
|
||||
): T {
|
||||
const prevChatUnreadMentions = (chat.unreadMentions || []);
|
||||
const updatedUnreadMentions = unique([...prevChatUnreadMentions, ...ids]).sort((a, b) => b - a);
|
||||
global = updateChat(global, chatId, {
|
||||
unreadMentions: updatedUnreadMentions,
|
||||
});
|
||||
export function addUnreadMentions<T extends GlobalState>({
|
||||
global, chatId, ids, totalCount,
|
||||
}: {
|
||||
global: T;
|
||||
chatId: string;
|
||||
ids: number[];
|
||||
totalCount?: number;
|
||||
}): T {
|
||||
const messageIdsByThreadId = groupMessageIdsByThreadId(global, chatId, ids, false);
|
||||
|
||||
if (shouldUpdateCount) {
|
||||
const updatedUnreadMentionsCount = (chat.unreadMentionsCount || 0)
|
||||
for (const threadId in messageIdsByThreadId) {
|
||||
const messageIds = messageIdsByThreadId[threadId];
|
||||
if (totalCount !== undefined) { // Assume that when `totalCount` is passed, server returned full id list
|
||||
global = replaceThreadReadStateParam(global, chatId, threadId, 'unreadMentions', messageIds);
|
||||
global = replaceThreadReadStateParam(global, chatId, threadId, 'unreadMentionsCount', totalCount);
|
||||
continue;
|
||||
}
|
||||
|
||||
const readState = selectThreadReadState(global, chatId, threadId);
|
||||
const prevChatUnreadMentions = readState?.unreadMentions || [];
|
||||
const updatedUnreadMentions = unique([...prevChatUnreadMentions, ...messageIds]).sort((a, b) => b - a);
|
||||
global = replaceThreadReadStateParam(global, chatId, threadId, 'unreadMentions', updatedUnreadMentions);
|
||||
|
||||
const updatedUnreadMentionsCount = (readState?.unreadMentionsCount || 0)
|
||||
+ Math.max(0, updatedUnreadMentions.length - prevChatUnreadMentions.length);
|
||||
|
||||
global = updateChat(global, chatId, {
|
||||
unreadMentionsCount: updatedUnreadMentionsCount,
|
||||
});
|
||||
global = replaceThreadReadStateParam(global, chatId, threadId, 'unreadMentionsCount', updatedUnreadMentionsCount);
|
||||
}
|
||||
|
||||
return global;
|
||||
}
|
||||
|
||||
export function removeUnreadMentions<T extends GlobalState>(
|
||||
global: T, chatId: string, chat: ApiChat, ids: number[], shouldUpdateCount: boolean = false,
|
||||
): T {
|
||||
const prevChatUnreadMentions = (chat.unreadMentions || []);
|
||||
const updatedUnreadMentions = prevChatUnreadMentions?.filter((id) => !ids.includes(id));
|
||||
export function removeUnreadMentions<T extends GlobalState>({
|
||||
global, chatId, ids,
|
||||
}: {
|
||||
global: T;
|
||||
chatId: string;
|
||||
ids: number[];
|
||||
}): T {
|
||||
const messageIdsByThreadId = groupMessageIdsByThreadId(global, chatId, ids, false);
|
||||
|
||||
global = updateChat(global, chatId, {
|
||||
unreadMentions: updatedUnreadMentions,
|
||||
});
|
||||
for (const threadId in messageIdsByThreadId) {
|
||||
const messageIds = messageIdsByThreadId[threadId];
|
||||
const readState = selectThreadReadState(global, chatId, threadId);
|
||||
const prevChatUnreadMentions = (readState?.unreadMentions || []);
|
||||
const updatedUnreadMentions = prevChatUnreadMentions?.filter((id) => !messageIds.includes(id));
|
||||
|
||||
global = replaceThreadReadStateParam(global, chatId, threadId, 'unreadMentions', updatedUnreadMentions);
|
||||
|
||||
if (shouldUpdateCount && chat.unreadMentionsCount) {
|
||||
const removedCount = prevChatUnreadMentions.length - updatedUnreadMentions.length;
|
||||
const updatedUnreadMentionsCount = Math.max(chat.unreadMentionsCount - removedCount, 0) || undefined;
|
||||
if (removedCount && readState?.unreadMentionsCount) {
|
||||
const updatedUnreadMentionsCount = Math.max(readState.unreadMentionsCount - removedCount, 0);
|
||||
|
||||
global = updateChat(global, chatId, {
|
||||
unreadMentionsCount: updatedUnreadMentionsCount,
|
||||
});
|
||||
global = replaceThreadReadStateParam(global, chatId, threadId, 'unreadMentionsCount', updatedUnreadMentionsCount);
|
||||
}
|
||||
}
|
||||
return global;
|
||||
}
|
||||
|
||||
export function updateChat<T extends GlobalState>(
|
||||
global: T, chatId: string, chatUpdate: Partial<ApiChat>, noOmitUnreadReactionCount = false, withDeepCheck = false,
|
||||
global: T, chatId: string, chatUpdate: Partial<ApiChat>, withDeepCheck = false,
|
||||
): T {
|
||||
const { byId } = global.chats;
|
||||
|
||||
@ -169,7 +196,7 @@ export function updateChat<T extends GlobalState>(
|
||||
}
|
||||
}
|
||||
|
||||
const updatedChat = getUpdatedChat(global, chatId, chatUpdate, noOmitUnreadReactionCount);
|
||||
const updatedChat = getUpdatedChat(global, chatId, chatUpdate);
|
||||
if (!updatedChat) {
|
||||
return global;
|
||||
}
|
||||
@ -284,7 +311,6 @@ export function addChats<T extends GlobalState>(global: T, newById: Record<strin
|
||||
// @optimization Don't spread/unspread global for each element, do it in a batch
|
||||
function getUpdatedChat<T extends GlobalState>(
|
||||
global: T, chatId: string, chatUpdate: Partial<ApiChat>,
|
||||
noOmitUnreadReactionCount = false,
|
||||
) {
|
||||
const { byId } = global.chats;
|
||||
const chat = byId[chatId];
|
||||
@ -294,10 +320,6 @@ function getUpdatedChat<T extends GlobalState>(
|
||||
return undefined; // Do not apply updates from min constructor
|
||||
}
|
||||
|
||||
if (!noOmitUnreadReactionCount) {
|
||||
omitProps.push('unreadReactionsCount');
|
||||
}
|
||||
|
||||
if (areDeepEqual(chat?.usernames, chatUpdate.usernames)) {
|
||||
omitProps.push('usernames');
|
||||
}
|
||||
|
||||
@ -1,14 +1,12 @@
|
||||
import type {
|
||||
ApiFormattedText,
|
||||
ApiMessage, ApiPoll, ApiPollResult, ApiQuickReply, ApiSponsoredMessage, ApiThreadInfo,
|
||||
ApiMessage, ApiPoll, ApiPollResult, ApiQuickReply, ApiSponsoredMessage,
|
||||
ApiWebPage,
|
||||
ApiWebPageFull,
|
||||
} from '../../api/types';
|
||||
import type {
|
||||
MessageList,
|
||||
MessageListType,
|
||||
TabThread,
|
||||
Thread,
|
||||
ThreadId,
|
||||
} from '../../types';
|
||||
import type {
|
||||
@ -30,6 +28,7 @@ import { unload } from '../../util/mediaLoader';
|
||||
import {
|
||||
getAllMessageMediaHashes,
|
||||
getMessageStatefulContent,
|
||||
groupMessageIdsByThreadId,
|
||||
hasMessageTtl, isMediaLoadableInViewer, mergeIdRanges, orderHistoryIds, orderPinnedIds,
|
||||
} from '../helpers';
|
||||
import { getEmojiOnlyCountForMessage } from '../helpers/getEmojiOnlyCountForMessage';
|
||||
@ -45,17 +44,22 @@ import {
|
||||
selectPinnedIds,
|
||||
selectPoll,
|
||||
selectQuickReplyMessage,
|
||||
selectScheduledIds,
|
||||
selectScheduledMessage,
|
||||
selectTabState,
|
||||
selectThreadIdFromMessage,
|
||||
selectThreadInfo,
|
||||
selectThreadParam,
|
||||
selectViewportIds,
|
||||
selectWebPage,
|
||||
} from '../selectors';
|
||||
import { selectThreadIdFromMessage, selectThreadInfo, selectThreadLocalStateParam } from '../selectors/threads';
|
||||
import { removeUnreadMentions } from './chats';
|
||||
import { removeIdFromSearchResults } from './middleSearch';
|
||||
import { removeUnreadReactions } from './reactions';
|
||||
import { updateTabState } from './tabs';
|
||||
import {
|
||||
deleteThread,
|
||||
replaceTabThreadParam,
|
||||
replaceThreadLocalStateParam,
|
||||
updateThreadInfoMessagesCount,
|
||||
} from './threads';
|
||||
import { clearMessageSummary, clearMessageTranslation } from './translations';
|
||||
|
||||
type MessageStoreSections = GlobalState['messages']['byChatId'][string];
|
||||
@ -107,49 +111,6 @@ export function replaceChatMessages<T extends GlobalState>(
|
||||
});
|
||||
}
|
||||
|
||||
export function updateTabThread<T extends GlobalState>(
|
||||
global: T, chatId: string, threadId: ThreadId, threadUpdate: Partial<TabThread>,
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
): T {
|
||||
const tabState = selectTabState(global, tabId);
|
||||
const current = tabState.tabThreads[chatId]?.[threadId] || {};
|
||||
|
||||
return updateTabState(global, {
|
||||
tabThreads: {
|
||||
...tabState.tabThreads,
|
||||
[chatId]: {
|
||||
...tabState.tabThreads[chatId],
|
||||
[threadId]: {
|
||||
...current,
|
||||
...threadUpdate,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, tabId);
|
||||
}
|
||||
|
||||
export function updateThread<T extends GlobalState>(
|
||||
global: T, chatId: string, threadId: ThreadId, threadUpdate: Partial<Thread> | undefined,
|
||||
): T {
|
||||
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),
|
||||
[threadId]: {
|
||||
...(current?.threadsById[threadId]),
|
||||
...threadUpdate,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function updateMessageStore<T extends GlobalState>(
|
||||
global: T, chatId: string, update: Partial<MessageStoreSections>,
|
||||
): T {
|
||||
@ -171,24 +132,6 @@ export function updateMessageStore<T extends GlobalState>(
|
||||
};
|
||||
}
|
||||
|
||||
export function replaceTabThreadParam<T extends GlobalState, K extends keyof TabThread>(
|
||||
global: T, chatId: string, threadId: ThreadId, paramName: K, newValue: TabThread[K] | undefined,
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
) {
|
||||
if (paramName === 'viewportIds') {
|
||||
global = replaceThreadParam(
|
||||
global, chatId, threadId, 'lastViewportIds', newValue as number[] | undefined,
|
||||
);
|
||||
}
|
||||
return updateTabThread(global, chatId, threadId, { [paramName]: newValue }, tabId);
|
||||
}
|
||||
|
||||
export function replaceThreadParam<T extends GlobalState, K extends keyof Thread>(
|
||||
global: T, chatId: string, threadId: ThreadId, paramName: K, newValue: Thread[K] | undefined,
|
||||
) {
|
||||
return updateThread(global, chatId, threadId, { [paramName]: newValue });
|
||||
}
|
||||
|
||||
export function addMessages<T extends GlobalState>(
|
||||
global: T, messages: ApiMessage[],
|
||||
): T {
|
||||
@ -462,14 +405,12 @@ 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 = replaceThreadLocalStateParam(global, chatId, threadId, 'listedIds', listedIds);
|
||||
global = replaceThreadLocalStateParam(global, chatId, threadId, 'outlyingLists', outlyingLists);
|
||||
global = replaceThreadLocalStateParam(global, chatId, threadId, 'pinnedIds', pinnedIds);
|
||||
|
||||
if (threadInfo && newMessageCount !== undefined) {
|
||||
global = updateThreadInfo(global, chatId, threadId, {
|
||||
messagesCount: newMessageCount,
|
||||
});
|
||||
global = updateThreadInfoMessagesCount(global, chatId, threadId, newMessageCount);
|
||||
}
|
||||
});
|
||||
|
||||
@ -488,12 +429,15 @@ export function deleteChatMessages<T extends GlobalState>(
|
||||
global = updateCurrentMessageList(global, chatId, undefined, undefined, undefined, undefined, tabId);
|
||||
}
|
||||
if (originalPost) {
|
||||
global = updateThread(global, fromChatId!, fromMessageId!, undefined);
|
||||
global = deleteThread(global, fromChatId!, fromMessageId!);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
global = removeUnreadReactions({ global, chatId, ids: messageIds });
|
||||
global = removeUnreadMentions({ global, chatId, ids: messageIds });
|
||||
|
||||
const newById = omit(byId, messageIds);
|
||||
global = replaceChatMessages(global, chatId, newById);
|
||||
|
||||
@ -511,22 +455,15 @@ export function deleteChatScheduledMessages<T extends GlobalState>(
|
||||
}
|
||||
const newById = omit(byId, messageIds);
|
||||
|
||||
let scheduledIds = selectScheduledIds(global, chatId, MAIN_THREAD_ID);
|
||||
if (scheduledIds) {
|
||||
messageIds.forEach((messageId) => {
|
||||
if (scheduledIds!.includes(messageId)) {
|
||||
scheduledIds = scheduledIds!.filter((id) => id !== messageId);
|
||||
}
|
||||
});
|
||||
global = replaceThreadParam(global, chatId, MAIN_THREAD_ID, 'scheduledIds', scheduledIds);
|
||||
const groupedByThreadId = groupMessageIdsByThreadId(global, chatId, messageIds, true);
|
||||
Object.entries(groupedByThreadId).forEach(([tId, newThreadScheduledIds]) => {
|
||||
const threadId = tId as ThreadId;
|
||||
const scheduledIds = selectThreadLocalStateParam(global, chatId, threadId, 'scheduledIds');
|
||||
if (!scheduledIds?.length) return;
|
||||
|
||||
Object.entries(global.messages.byChatId[chatId].threadsById).forEach(([threadId, thread]) => {
|
||||
if (thread.scheduledIds) {
|
||||
const newScheduledIds = thread.scheduledIds.filter((id) => !messageIds.includes(id));
|
||||
global = replaceThreadParam(global, chatId, Number(threadId), 'scheduledIds', newScheduledIds);
|
||||
}
|
||||
});
|
||||
}
|
||||
const cleanedScheduledIds = scheduledIds.filter((id) => !newThreadScheduledIds.includes(id));
|
||||
global = replaceThreadLocalStateParam(global, chatId, threadId, 'scheduledIds', cleanedScheduledIds);
|
||||
});
|
||||
|
||||
global = {
|
||||
...global,
|
||||
@ -558,7 +495,7 @@ export function updateListedIds<T extends GlobalState>(
|
||||
return global;
|
||||
}
|
||||
|
||||
return replaceThreadParam(global, chatId, threadId, 'listedIds', orderHistoryIds([
|
||||
return replaceThreadLocalStateParam(global, chatId, threadId, 'listedIds', orderHistoryIds([
|
||||
...(listedIds || []),
|
||||
...newIds,
|
||||
]));
|
||||
@ -577,7 +514,7 @@ export function removeOutlyingList<T extends GlobalState>(
|
||||
|
||||
const newOutlyingLists = outlyingLists.filter((l) => l !== list);
|
||||
|
||||
return replaceThreadParam(global, chatId, threadId, 'outlyingLists', newOutlyingLists);
|
||||
return replaceThreadLocalStateParam(global, chatId, threadId, 'outlyingLists', newOutlyingLists);
|
||||
}
|
||||
|
||||
export function updateOutlyingLists<T extends GlobalState>(
|
||||
@ -592,7 +529,7 @@ export function updateOutlyingLists<T extends GlobalState>(
|
||||
|
||||
const newOutlyingLists = mergeIdRanges(outlyingLists || [], idsUpdate);
|
||||
|
||||
return replaceThreadParam(global, chatId, threadId, 'outlyingLists', newOutlyingLists);
|
||||
return replaceThreadLocalStateParam(global, chatId, threadId, 'outlyingLists', newOutlyingLists);
|
||||
}
|
||||
|
||||
export function addViewportId<T extends GlobalState>(
|
||||
@ -648,7 +585,7 @@ export function safeReplacePinnedIds<T extends GlobalState>(
|
||||
const currentIds = selectPinnedIds(global, chatId, threadId) || [];
|
||||
const newIds = orderPinnedIds(newPinnedIds);
|
||||
|
||||
return replaceThreadParam(
|
||||
return replaceThreadLocalStateParam(
|
||||
global,
|
||||
chatId,
|
||||
threadId,
|
||||
@ -657,42 +594,6 @@ export function safeReplacePinnedIds<T extends GlobalState>(
|
||||
);
|
||||
}
|
||||
|
||||
export function updateThreadInfo<T extends GlobalState>(
|
||||
global: T, chatId: string, threadId: ThreadId, update: Partial<ApiThreadInfo> | undefined,
|
||||
doNotUpdateLinked?: boolean,
|
||||
): T {
|
||||
const newThreadInfo = {
|
||||
...(selectThreadInfo(global, chatId, threadId) as ApiThreadInfo),
|
||||
...update,
|
||||
} as ApiThreadInfo;
|
||||
|
||||
if (!doNotUpdateLinked && !newThreadInfo.isCommentsInfo) {
|
||||
const linkedUpdate = pick(newThreadInfo, ['messagesCount', 'lastMessageId', 'lastReadInboxMessageId']);
|
||||
if (newThreadInfo.fromChannelId && newThreadInfo.fromMessageId) {
|
||||
global = updateThreadInfo(
|
||||
global, newThreadInfo.fromChannelId, newThreadInfo.fromMessageId, linkedUpdate, true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return replaceThreadParam(global, chatId, threadId, 'threadInfo', newThreadInfo);
|
||||
}
|
||||
|
||||
export function updateThreadInfos<T extends GlobalState>(
|
||||
global: T, updates: Partial<ApiThreadInfo>[],
|
||||
): T {
|
||||
updates.forEach((update) => {
|
||||
global = updateThreadInfo(
|
||||
global,
|
||||
update.isCommentsInfo ? update.originChannelId! : update.chatId!,
|
||||
update.isCommentsInfo ? update.originMessageId! : update.threadId!,
|
||||
update,
|
||||
);
|
||||
});
|
||||
|
||||
return global;
|
||||
}
|
||||
|
||||
export function updateScheduledMessages<T extends GlobalState>(
|
||||
global: T, chatId: string, newById: Record<number, ApiMessage>,
|
||||
): T {
|
||||
@ -852,27 +753,6 @@ export function exitMessageSelectMode<T extends GlobalState>(
|
||||
}, tabId);
|
||||
}
|
||||
|
||||
export function updateThreadUnreadFromForwardedMessage<T extends GlobalState>(
|
||||
global: T,
|
||||
originMessage: ApiMessage,
|
||||
chatId: string,
|
||||
lastMessageId: number,
|
||||
isDeleting?: boolean,
|
||||
): T {
|
||||
const { channelPostId, fromChatId } = originMessage.forwardInfo || {};
|
||||
if (channelPostId && fromChatId) {
|
||||
const threadInfoOld = selectThreadInfo(global, chatId, channelPostId);
|
||||
if (threadInfoOld) {
|
||||
global = replaceThreadParam(global, chatId, channelPostId, 'threadInfo', {
|
||||
...threadInfoOld,
|
||||
lastMessageId,
|
||||
messagesCount: (threadInfoOld.messagesCount || 0) + (isDeleting ? -1 : 1),
|
||||
});
|
||||
}
|
||||
}
|
||||
return global;
|
||||
}
|
||||
|
||||
export function addActiveMediaDownload<T extends GlobalState>(
|
||||
global: T,
|
||||
mediaHash: string,
|
||||
@ -1086,7 +966,7 @@ export function updateTypingDraft<T extends GlobalState>(
|
||||
randomId: string,
|
||||
text: ApiFormattedText,
|
||||
) {
|
||||
const typingDraftStore = selectThreadParam(global, chatId, threadId, 'typingDraftIdByRandomId');
|
||||
const typingDraftStore = selectThreadLocalStateParam(global, chatId, threadId, 'typingDraftIdByRandomId');
|
||||
const messageId = typingDraftStore?.[randomId];
|
||||
if (!messageId) {
|
||||
return global;
|
||||
|
||||
@ -1,16 +1,18 @@
|
||||
import type { ApiChat, ApiMessage, ApiReactionWithPaid } from '../../api/types';
|
||||
import type { GlobalState } from '../types';
|
||||
import { type ApiMessage, type ApiReactionWithPaid } from '../../api/types';
|
||||
|
||||
import { MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN, MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN } from '../../config';
|
||||
import { unique } from '../../util/iteratees';
|
||||
import windowSize from '../../util/windowSize';
|
||||
import {
|
||||
MIN_LEFT_COLUMN_WIDTH,
|
||||
SIDE_COLUMN_MAX_WIDTH,
|
||||
} from '../../components/middle/helpers/calculateMiddleFooterTransforms';
|
||||
import { updateReactionCount } from '../helpers';
|
||||
import { groupMessageIdsByThreadId, updateReactionCount } from '../helpers';
|
||||
import { selectIsChatWithSelf, selectSendAs, selectTabState } from '../selectors';
|
||||
import { updateChat } from './chats';
|
||||
import { selectThreadReadState } from '../selectors/threads';
|
||||
import { updateChatMessage } from './messages';
|
||||
import { replaceThreadReadStateParam } from './threads';
|
||||
|
||||
import { getIsMobile } from '../../hooks/useAppLayout';
|
||||
|
||||
@ -75,8 +77,60 @@ export function addMessageReaction<T extends GlobalState>(
|
||||
});
|
||||
}
|
||||
|
||||
export function updateUnreadReactions<T extends GlobalState>(
|
||||
global: T, chatId: string, update: Pick<ApiChat, 'unreadReactionsCount' | 'unreadReactions'>,
|
||||
): T {
|
||||
return updateChat(global, chatId, update, true);
|
||||
export function addUnreadReactions<T extends GlobalState>({
|
||||
global, chatId, ids, totalCount,
|
||||
}: {
|
||||
global: T;
|
||||
chatId: string;
|
||||
ids: number[];
|
||||
totalCount?: number;
|
||||
}): T {
|
||||
const messageIdsByThreadId = groupMessageIdsByThreadId(global, chatId, ids, false);
|
||||
|
||||
for (const threadId in messageIdsByThreadId) {
|
||||
const messageIds = messageIdsByThreadId[threadId];
|
||||
if (totalCount !== undefined) { // Assume that when `totalCount` is passed, server returned full id list
|
||||
global = replaceThreadReadStateParam(global, chatId, threadId, 'unreadReactions', messageIds);
|
||||
global = replaceThreadReadStateParam(global, chatId, threadId, 'unreadReactionsCount', totalCount);
|
||||
continue;
|
||||
}
|
||||
|
||||
const readState = selectThreadReadState(global, chatId, threadId);
|
||||
const prevChatUnreadReactions = readState?.unreadReactions || [];
|
||||
const updatedUnreadReactions = unique([...prevChatUnreadReactions, ...messageIds]).sort((a, b) => b - a);
|
||||
global = replaceThreadReadStateParam(global, chatId, threadId, 'unreadReactions', updatedUnreadReactions);
|
||||
|
||||
const delta = updatedUnreadReactions.length - prevChatUnreadReactions.length;
|
||||
if (delta > 0) {
|
||||
const unreadReactionsCount = (readState?.unreadReactionsCount || 0) + delta;
|
||||
global = replaceThreadReadStateParam(global, chatId, threadId, 'unreadReactionsCount', unreadReactionsCount);
|
||||
}
|
||||
}
|
||||
|
||||
return global;
|
||||
}
|
||||
|
||||
export function removeUnreadReactions<T extends GlobalState>({
|
||||
global, chatId, ids,
|
||||
}: {
|
||||
global: T;
|
||||
chatId: string;
|
||||
ids: number[];
|
||||
}): T {
|
||||
const messageIdsByThreadId = groupMessageIdsByThreadId(global, chatId, ids, false);
|
||||
|
||||
for (const threadId in messageIdsByThreadId) {
|
||||
const messageIds = messageIdsByThreadId[threadId];
|
||||
const readState = selectThreadReadState(global, chatId, threadId);
|
||||
const prevChatUnreadReactions = readState?.unreadReactions || [];
|
||||
const updatedUnreadReactions = prevChatUnreadReactions.filter((id) => !messageIds.includes(id));
|
||||
global = replaceThreadReadStateParam(global, chatId, threadId, 'unreadReactions', updatedUnreadReactions);
|
||||
|
||||
const delta = prevChatUnreadReactions.length - updatedUnreadReactions.length;
|
||||
if (delta > 0 && readState?.unreadReactionsCount) {
|
||||
const unreadReactionsCount = Math.max(readState.unreadReactionsCount - delta, 0);
|
||||
global = replaceThreadReadStateParam(global, chatId, threadId, 'unreadReactionsCount', unreadReactionsCount);
|
||||
}
|
||||
}
|
||||
return global;
|
||||
}
|
||||
|
||||
241
src/global/reducers/threads.ts
Normal file
241
src/global/reducers/threads.ts
Normal file
@ -0,0 +1,241 @@
|
||||
import type { ApiMessage, ApiThreadInfo } from '../../api/types';
|
||||
import type { TabThread, Thread, ThreadId, ThreadLocalState, ThreadReadState } from '../../types';
|
||||
import type { GlobalState, TabArgs } from '../types';
|
||||
import { MAIN_THREAD_ID } from '../../api/types';
|
||||
|
||||
import { getCurrentTabId } from '../../util/establishMultitabRole';
|
||||
import { omit, pick } from '../../util/iteratees';
|
||||
import { selectChatMessage, selectTabState } from '../selectors';
|
||||
import { selectThread, selectThreadIdFromMessage, selectThreadInfo, selectThreadReadState } from '../selectors/threads';
|
||||
import { updateMessageStore } from './messages';
|
||||
import { updateTabState } from './tabs';
|
||||
|
||||
export function replaceTabThreadParam<T extends GlobalState, K extends keyof TabThread>(
|
||||
global: T, chatId: string, threadId: ThreadId, paramName: K, newValue: TabThread[K] | undefined,
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
) {
|
||||
if (paramName === 'viewportIds') {
|
||||
global = replaceThreadLocalStateParam(
|
||||
global, chatId, threadId, 'lastViewportIds', newValue as number[] | undefined,
|
||||
);
|
||||
}
|
||||
return updateTabThread(global, chatId, threadId, { [paramName]: newValue }, tabId);
|
||||
}
|
||||
|
||||
export function replaceThreadLocalStateParam<T extends GlobalState, K extends keyof ThreadLocalState>(
|
||||
global: T, chatId: string, threadId: ThreadId, paramName: K, newValue: ThreadLocalState[K] | undefined,
|
||||
) {
|
||||
return updateThreadLocalState(global, chatId, threadId, { [paramName]: newValue });
|
||||
}
|
||||
|
||||
export function replaceThreadReadStateParam<T extends GlobalState, K extends keyof ThreadReadState>(
|
||||
global: T, chatId: string, threadId: ThreadId, paramName: K, newValue: ThreadReadState[K] | undefined,
|
||||
) {
|
||||
return updateThreadReadState(global, chatId, threadId, { [paramName]: newValue });
|
||||
}
|
||||
|
||||
export function updateTabThread<T extends GlobalState>(
|
||||
global: T, chatId: string, threadId: ThreadId, threadUpdate: Partial<TabThread>,
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
): T {
|
||||
const tabState = selectTabState(global, tabId);
|
||||
const current = tabState.tabThreads[chatId]?.[threadId] || {};
|
||||
|
||||
return updateTabState(global, {
|
||||
tabThreads: {
|
||||
...tabState.tabThreads,
|
||||
[chatId]: {
|
||||
...tabState.tabThreads[chatId],
|
||||
[threadId]: {
|
||||
...current,
|
||||
...threadUpdate,
|
||||
},
|
||||
},
|
||||
},
|
||||
}, tabId);
|
||||
}
|
||||
|
||||
export function updateThreadLocalState<T extends GlobalState>(
|
||||
global: T, chatId: string, threadId: ThreadId, threadUpdate: Partial<ThreadLocalState> | undefined,
|
||||
): T {
|
||||
const currentThread = selectThread(global, chatId, threadId);
|
||||
if (!currentThread) return global;
|
||||
|
||||
if (!threadUpdate && !currentThread.threadInfo) {
|
||||
return updateMessageStore(global, chatId, {
|
||||
threadsById: omit(global.messages.byChatId[chatId]?.threadsById, [threadId]),
|
||||
});
|
||||
}
|
||||
|
||||
const updated: ThreadLocalState = threadUpdate ? {
|
||||
...currentThread.localState,
|
||||
...threadUpdate,
|
||||
} : {};
|
||||
|
||||
return updateMessageStore(global, chatId, {
|
||||
threadsById: {
|
||||
...global.messages.byChatId[chatId]?.threadsById,
|
||||
[threadId]: {
|
||||
...currentThread,
|
||||
localState: updated,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function updateThreadReadState<T extends GlobalState>(
|
||||
global: T, chatId: string, threadId: ThreadId, threadUpdate: Partial<ThreadReadState>,
|
||||
): T {
|
||||
const currentThread = selectThread(global, chatId, threadId);
|
||||
if (!currentThread) return global;
|
||||
|
||||
const updated: ThreadReadState = {
|
||||
...currentThread.readState,
|
||||
...threadUpdate,
|
||||
};
|
||||
|
||||
return updateMessageStore(global, chatId, {
|
||||
threadsById: {
|
||||
...global.messages.byChatId[chatId]?.threadsById,
|
||||
[threadId]: {
|
||||
...currentThread,
|
||||
readState: updated,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function updateThreadInfo<T extends GlobalState>(
|
||||
global: T, update: Partial<ApiThreadInfo> | undefined, doNotUpdateLinked?: boolean,
|
||||
): T {
|
||||
const chatId = update?.isCommentsInfo ? update.originChannelId : update?.chatId;
|
||||
const threadId = update?.isCommentsInfo ? update.originMessageId : update?.threadId;
|
||||
|
||||
if (!chatId || !threadId) {
|
||||
return global;
|
||||
}
|
||||
|
||||
const currentThread = selectThread(global, chatId, threadId);
|
||||
const newThreadInfo = {
|
||||
...currentThread?.threadInfo,
|
||||
...update,
|
||||
} as ApiThreadInfo;
|
||||
|
||||
if (!doNotUpdateLinked) {
|
||||
global = updateLinkedThreadInfo(global, newThreadInfo);
|
||||
}
|
||||
|
||||
return updateThreadInfoInStore(global, chatId, threadId, newThreadInfo);
|
||||
}
|
||||
|
||||
export function updateLinkedThreadInfo<T extends GlobalState>(
|
||||
global: T, update: ApiThreadInfo,
|
||||
): T {
|
||||
if (update.isCommentsInfo || !update.fromChannelId || !update.fromMessageId) {
|
||||
return global;
|
||||
}
|
||||
|
||||
const threadInfo = selectThreadInfo(global, update.fromChannelId, update.fromMessageId);
|
||||
if (!threadInfo) {
|
||||
return global;
|
||||
}
|
||||
|
||||
const valuesToUpdate = pick(update, ['messagesCount', 'lastMessageId', 'recentReplierIds']);
|
||||
const newThreadInfo: ApiThreadInfo = {
|
||||
...threadInfo,
|
||||
...valuesToUpdate,
|
||||
};
|
||||
return updateThreadInfoInStore(global, update.fromChannelId, update.fromMessageId, newThreadInfo);
|
||||
}
|
||||
|
||||
export function updateThreadInfoInStore<T extends GlobalState>(
|
||||
global: T, chatId: string, threadId: ThreadId, update: ApiThreadInfo,
|
||||
): T {
|
||||
const thread = selectThread(global, chatId, threadId);
|
||||
|
||||
const newThread: Thread = {
|
||||
localState: thread?.localState || {},
|
||||
readState: thread?.readState || {},
|
||||
threadInfo: update,
|
||||
};
|
||||
|
||||
return updateMessageStore(global, chatId, {
|
||||
threadsById: {
|
||||
...global.messages.byChatId[chatId]?.threadsById,
|
||||
[threadId]: newThread,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function updateThreadInfoMessagesCount<T extends GlobalState>(
|
||||
global: T, chatId: string, threadId: ThreadId, newCount: number,
|
||||
): T {
|
||||
const threadInfo = selectThreadInfo(global, chatId, threadId);
|
||||
if (!threadInfo) return global;
|
||||
|
||||
const newThreadInfo: ApiThreadInfo = {
|
||||
...threadInfo,
|
||||
messagesCount: newCount,
|
||||
};
|
||||
return updateThreadInfo(global, newThreadInfo);
|
||||
}
|
||||
|
||||
export function updateThreadInfoLastMessageId<T extends GlobalState>(
|
||||
global: T, chatId: string, threadId: ThreadId, newLastMessageId: number | undefined,
|
||||
): T {
|
||||
const threadInfo = selectThreadInfo(global, chatId, threadId);
|
||||
if (!threadInfo) return global;
|
||||
|
||||
const newThreadInfo: ApiThreadInfo = {
|
||||
...threadInfo,
|
||||
lastMessageId: newLastMessageId,
|
||||
};
|
||||
return updateThreadInfo(global, newThreadInfo);
|
||||
}
|
||||
|
||||
export function addUnreadMessageToCounter<T extends GlobalState>(
|
||||
global: T, chatId: string, message: ApiMessage,
|
||||
): T {
|
||||
const threadId = selectThreadIdFromMessage(global, message);
|
||||
const currentReadState = selectThreadReadState(global, chatId, threadId);
|
||||
const newUnreadCount = (currentReadState?.unreadCount || 0) + 1;
|
||||
return replaceThreadReadStateParam(global, chatId, threadId, 'unreadCount', newUnreadCount);
|
||||
}
|
||||
|
||||
export function decrementUnreadCount<T extends GlobalState>(
|
||||
global: T, chatId: string, messageId: number, amount: number,
|
||||
): T {
|
||||
const message = selectChatMessage(global, chatId, messageId);
|
||||
if (!message) return global;
|
||||
|
||||
const threadId = selectThreadIdFromMessage(global, message);
|
||||
const currentReadState = selectThreadReadState(global, chatId, threadId);
|
||||
const newUnreadCount = Math.max(0, (currentReadState?.unreadCount || 0) - amount);
|
||||
return replaceThreadReadStateParam(global, chatId, threadId, 'unreadCount', newUnreadCount);
|
||||
}
|
||||
|
||||
export function updateMainThreadReadStates<T extends GlobalState>(
|
||||
global: T, threadReadStates: Record<string, ThreadReadState>,
|
||||
): T {
|
||||
Object.entries(threadReadStates).forEach(([chatId, readState]) => {
|
||||
global = updateThreadReadState(global, chatId, MAIN_THREAD_ID, readState);
|
||||
});
|
||||
return global;
|
||||
}
|
||||
|
||||
export function updateThreadReadStates<T extends GlobalState>(
|
||||
global: T, chatId: string, threadReadStates: Record<ThreadId, ThreadReadState>,
|
||||
): T {
|
||||
Object.entries(threadReadStates).forEach(([threadId, readState]) => {
|
||||
global = updateThreadReadState(global, chatId, threadId, readState);
|
||||
});
|
||||
return global;
|
||||
}
|
||||
|
||||
export function deleteThread<T extends GlobalState>(
|
||||
global: T, chatId: string, threadId: ThreadId,
|
||||
): T {
|
||||
return updateMessageStore(global, chatId, {
|
||||
threadsById: omit(global.messages.byChatId[chatId]?.threadsById, [threadId]),
|
||||
});
|
||||
}
|
||||
@ -1,15 +1,30 @@
|
||||
import type { ApiTopic } from '../../api/types';
|
||||
import type { ApiTopic, ApiTopicWithState } from '../../api/types';
|
||||
import type { TopicsInfo } from '../../types';
|
||||
import type { GlobalState } from '../types';
|
||||
|
||||
import { buildCollectionByKey, omit, unique } from '../../util/iteratees';
|
||||
import { omit, pick, unique } from '../../util/iteratees';
|
||||
import {
|
||||
selectChat, selectTopic, selectTopics,
|
||||
selectTopicsInfo,
|
||||
} from '../selectors';
|
||||
import { updateThread, updateThreadInfo } from './messages';
|
||||
import {
|
||||
updateThreadInfo,
|
||||
updateThreadLocalState,
|
||||
updateThreadReadState,
|
||||
} from './threads';
|
||||
|
||||
function updateTopicsStore<T extends GlobalState>(
|
||||
const SAFE_MIN_PROPERTIES: (keyof ApiTopic)[] = [
|
||||
'id',
|
||||
'title',
|
||||
'iconColor',
|
||||
'iconEmojiId',
|
||||
'date',
|
||||
'fromId',
|
||||
'isOwner',
|
||||
'isClosed',
|
||||
];
|
||||
|
||||
export function updateTopicsInfo<T extends GlobalState>(
|
||||
global: T, chatId: string, update: Partial<TopicsInfo>,
|
||||
) {
|
||||
const info = global.chats.topicsInfoById[chatId] || {};
|
||||
@ -35,7 +50,7 @@ export function updateListedTopicIds<T extends GlobalState>(
|
||||
global: T, chatId: string, topicIds: number[],
|
||||
): T {
|
||||
const listedIds = selectTopicsInfo(global, chatId)?.listedTopicIds || [];
|
||||
return updateTopicsStore(global, chatId, {
|
||||
return updateTopicsInfo(global, chatId, {
|
||||
listedTopicIds: unique([
|
||||
...listedIds,
|
||||
...topicIds,
|
||||
@ -43,35 +58,6 @@ export function updateListedTopicIds<T extends GlobalState>(
|
||||
});
|
||||
}
|
||||
|
||||
export function updateTopics<T extends GlobalState>(
|
||||
global: T, chatId: string, topicsCount: number, topics: ApiTopic[],
|
||||
): T {
|
||||
const oldTopics = selectTopics(global, chatId);
|
||||
const newTopics = buildCollectionByKey(topics, 'id');
|
||||
|
||||
global = updateTopicsStore(global, chatId, {
|
||||
topicsById: {
|
||||
...oldTopics,
|
||||
...newTopics,
|
||||
},
|
||||
totalCount: topicsCount,
|
||||
});
|
||||
|
||||
topics.forEach((topic) => {
|
||||
global = updateThread(global, chatId, topic.id, {
|
||||
firstMessageId: topic.id,
|
||||
});
|
||||
|
||||
global = updateThreadInfo(global, chatId, topic.id, {
|
||||
lastMessageId: topic.lastMessageId,
|
||||
threadId: topic.id,
|
||||
chatId,
|
||||
});
|
||||
});
|
||||
|
||||
return global;
|
||||
}
|
||||
|
||||
export function updateTopic<T extends GlobalState>(
|
||||
global: T, chatId: string, topicId: number, update: Partial<ApiTopic>,
|
||||
): T {
|
||||
@ -82,29 +68,46 @@ export function updateTopic<T extends GlobalState>(
|
||||
const topic = selectTopic(global, chatId, topicId);
|
||||
const oldTopics = selectTopics(global, chatId);
|
||||
|
||||
const safeUpdate = update.isMin ? pick(update, SAFE_MIN_PROPERTIES) : update;
|
||||
|
||||
const updatedTopic = {
|
||||
...topic,
|
||||
...update,
|
||||
...safeUpdate,
|
||||
} as ApiTopic;
|
||||
|
||||
if (!updatedTopic.id) return global;
|
||||
|
||||
global = updateTopicsStore(global, chatId, {
|
||||
global = updateTopicsInfo(global, chatId, {
|
||||
topicsById: {
|
||||
...oldTopics,
|
||||
[topicId]: updatedTopic,
|
||||
},
|
||||
});
|
||||
|
||||
global = updateThread(global, chatId, updatedTopic.id, {
|
||||
global = updateThreadLocalState(global, chatId, updatedTopic.id, {
|
||||
firstMessageId: updatedTopic.id,
|
||||
});
|
||||
|
||||
global = updateThreadInfo(global, chatId, updatedTopic.id, {
|
||||
lastMessageId: updatedTopic.lastMessageId,
|
||||
threadId: updatedTopic.id,
|
||||
chatId,
|
||||
});
|
||||
return global;
|
||||
}
|
||||
|
||||
export function updateTopicWithState<T extends GlobalState>(
|
||||
global: T, chatId: string, topicWithState: ApiTopicWithState,
|
||||
): T {
|
||||
const topicId = topicWithState.topic.id;
|
||||
|
||||
global = updateTopic(global, chatId, topicId, topicWithState.topic);
|
||||
if (!topicWithState.topic.isMin) {
|
||||
global = updateThreadInfo(global, {
|
||||
isCommentsInfo: false,
|
||||
chatId,
|
||||
threadId: topicId,
|
||||
lastMessageId: topicWithState.lastMessageId,
|
||||
});
|
||||
}
|
||||
if (topicWithState.readState) {
|
||||
global = updateThreadReadState(global, chatId, topicId, topicWithState.readState);
|
||||
}
|
||||
|
||||
return global;
|
||||
}
|
||||
@ -115,25 +118,17 @@ export function deleteTopic<T extends GlobalState>(
|
||||
const topics = selectTopics(global, chatId);
|
||||
if (!topics) return global;
|
||||
|
||||
global = updateTopicsStore(global, chatId, {
|
||||
global = updateTopicsInfo(global, chatId, {
|
||||
topicsById: omit(topics, [topicId]),
|
||||
});
|
||||
|
||||
return global;
|
||||
}
|
||||
|
||||
export function updateTopicLastMessageId<T extends GlobalState>(
|
||||
global: T, chatId: string, threadId: number, lastMessageId: number,
|
||||
) {
|
||||
return updateTopic(global, chatId, threadId, {
|
||||
lastMessageId,
|
||||
});
|
||||
}
|
||||
|
||||
export function replacePinnedTopicIds<T extends GlobalState>(
|
||||
global: T, chatId: string, pinnedTopicIds: number[],
|
||||
) {
|
||||
return updateTopicsStore(global, chatId, {
|
||||
return updateTopicsInfo(global, chatId, {
|
||||
orderedPinnedTopicIds: pinnedTopicIds,
|
||||
});
|
||||
}
|
||||
|
||||
@ -12,9 +12,7 @@ import type {
|
||||
import type {
|
||||
ChatTranslatedMessages,
|
||||
MessageListType,
|
||||
TabThread,
|
||||
TextSummary,
|
||||
Thread,
|
||||
ThreadId,
|
||||
} from '../../types';
|
||||
import type { IAllowedAttachmentOptions } from '../helpers';
|
||||
@ -24,7 +22,8 @@ import type {
|
||||
import { ApiMessageEntityTypes, MAIN_THREAD_ID } from '../../api/types';
|
||||
|
||||
import {
|
||||
ANONYMOUS_USER_ID, GENERAL_TOPIC_ID, SERVICE_NOTIFICATIONS_USER_ID,
|
||||
GENERAL_TOPIC_ID,
|
||||
SERVICE_NOTIFICATIONS_USER_ID,
|
||||
WEB_APP_PLATFORM,
|
||||
} from '../../config';
|
||||
import { IS_TRANSLATION_SUPPORTED } from '../../util/browser/windowEnvironment';
|
||||
@ -86,7 +85,17 @@ import { selectPeer, selectPeerPaidMessagesStars } from './peers';
|
||||
import { selectPeerStory } from './stories';
|
||||
import { selectCustomEmoji, selectIsStickerFavorite } from './symbols';
|
||||
import { selectTabState } from './tabs';
|
||||
import { selectTopic } from './topics';
|
||||
import {
|
||||
selectEditingId,
|
||||
selectEditingScheduledId,
|
||||
selectTabThreadParam,
|
||||
selectThreadByMessage,
|
||||
selectThreadInfo,
|
||||
selectThreadLocalState,
|
||||
selectThreadLocalStateParam,
|
||||
selectThreadReadState,
|
||||
} from './threads';
|
||||
import { selectTopic, selectTopicFromMessage } from './topics';
|
||||
import {
|
||||
selectBot, selectUser, selectUserStatus,
|
||||
} from './users';
|
||||
@ -121,45 +130,8 @@ export function selectChatScheduledMessages<T extends GlobalState>(global: T, ch
|
||||
return global.scheduledMessages.byChatId[chatId]?.byId;
|
||||
}
|
||||
|
||||
export function selectTabThreadParam<T extends GlobalState, K extends keyof TabThread>(
|
||||
global: T,
|
||||
chatId: string,
|
||||
threadId: ThreadId,
|
||||
key: K,
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
) {
|
||||
return selectTabState(global, tabId).tabThreads[chatId]?.[threadId]?.[key];
|
||||
}
|
||||
|
||||
export function selectThreadParam<T extends GlobalState, K extends keyof Thread>(
|
||||
global: T,
|
||||
chatId: string,
|
||||
threadId: ThreadId,
|
||||
key: K,
|
||||
) {
|
||||
return selectThread(global, chatId, threadId)?.[key];
|
||||
}
|
||||
|
||||
export function selectThread<T extends GlobalState>(
|
||||
global: T,
|
||||
chatId: string,
|
||||
threadId: ThreadId,
|
||||
) {
|
||||
const messageInfo = global.messages.byChatId[chatId];
|
||||
if (!messageInfo) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const thread = messageInfo.threadsById[threadId];
|
||||
if (!thread) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return thread;
|
||||
}
|
||||
|
||||
export function selectListedIds<T extends GlobalState>(global: T, chatId: string, threadId: ThreadId) {
|
||||
return selectThreadParam(global, chatId, threadId, 'listedIds');
|
||||
return selectThreadLocalStateParam(global, chatId, threadId, 'listedIds');
|
||||
}
|
||||
|
||||
export function selectOutlyingListByMessageId<T extends GlobalState>(
|
||||
@ -178,7 +150,7 @@ export function selectOutlyingListByMessageId<T extends GlobalState>(
|
||||
export function selectOutlyingLists<T extends GlobalState>(
|
||||
global: T, chatId: string, threadId: ThreadId,
|
||||
) {
|
||||
return selectThreadParam(global, chatId, threadId, 'outlyingLists');
|
||||
return selectThreadLocalStateParam(global, chatId, threadId, 'outlyingLists');
|
||||
}
|
||||
|
||||
export function selectCurrentMessageIds<T extends GlobalState>(
|
||||
@ -205,79 +177,15 @@ export function selectViewportIds<T extends GlobalState>(
|
||||
}
|
||||
|
||||
export function selectPinnedIds<T extends GlobalState>(global: T, chatId: string, threadId: ThreadId) {
|
||||
return selectThreadParam(global, chatId, threadId, 'pinnedIds');
|
||||
return selectThreadLocalStateParam(global, chatId, threadId, 'pinnedIds');
|
||||
}
|
||||
|
||||
export function selectScheduledIds<T extends GlobalState>(global: T, chatId: string, threadId: ThreadId) {
|
||||
return selectThreadParam(global, chatId, threadId, 'scheduledIds');
|
||||
}
|
||||
|
||||
export function selectScrollOffset<T extends GlobalState>(
|
||||
global: T, chatId: string, threadId: ThreadId,
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
) {
|
||||
return selectTabThreadParam(global, chatId, threadId, 'scrollOffset', tabId);
|
||||
}
|
||||
|
||||
export function selectLastScrollOffset<T extends GlobalState>(global: T, chatId: string, threadId: ThreadId) {
|
||||
return selectThreadParam(global, chatId, threadId, 'lastScrollOffset');
|
||||
}
|
||||
|
||||
export function selectEditingId<T extends GlobalState>(global: T, chatId: string, threadId: ThreadId) {
|
||||
return selectThreadParam(global, chatId, threadId, 'editingId');
|
||||
}
|
||||
|
||||
export function selectEditingDraft<T extends GlobalState>(global: T, chatId: string, threadId: ThreadId) {
|
||||
return selectThreadParam(global, chatId, threadId, 'editingDraft');
|
||||
}
|
||||
|
||||
export function selectEditingScheduledId<T extends GlobalState>(global: T, chatId: string) {
|
||||
return selectThreadParam(global, chatId, MAIN_THREAD_ID, 'editingScheduledId');
|
||||
}
|
||||
|
||||
export function selectEditingScheduledDraft<T extends GlobalState>(global: T, chatId: string) {
|
||||
return selectThreadParam(global, chatId, MAIN_THREAD_ID, 'editingScheduledDraft');
|
||||
}
|
||||
|
||||
export function selectDraft<T extends GlobalState>(global: T, chatId: string, threadId: ThreadId) {
|
||||
return selectThreadParam(global, chatId, threadId, 'draft');
|
||||
}
|
||||
|
||||
export function selectNoWebPage<T extends GlobalState>(global: T, chatId: string, threadId: ThreadId) {
|
||||
return selectThreadParam(global, chatId, threadId, 'noWebPage');
|
||||
}
|
||||
|
||||
export function selectThreadInfo<T extends GlobalState>(global: T, chatId: string, threadId: ThreadId) {
|
||||
return selectThreadParam(global, chatId, threadId, 'threadInfo');
|
||||
return selectThreadLocalStateParam(global, chatId, threadId, 'scheduledIds');
|
||||
}
|
||||
|
||||
export function selectFirstMessageId<T extends GlobalState>(global: T, chatId: string, threadId: ThreadId) {
|
||||
return selectThreadParam(global, chatId, threadId, 'firstMessageId');
|
||||
}
|
||||
|
||||
export function selectReplyStack<T extends GlobalState>(
|
||||
global: T, chatId: string, threadId: ThreadId,
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
) {
|
||||
return selectTabThreadParam(global, chatId, threadId, 'replyStack', tabId);
|
||||
}
|
||||
|
||||
export function selectThreadMessagesCount(global: GlobalState, chatId: string, threadId: ThreadId) {
|
||||
const chat = selectChat(global, chatId);
|
||||
const threadInfo = selectThreadInfo(global, chatId, threadId);
|
||||
if (!chat || !threadInfo || threadInfo.messagesCount === undefined) return undefined;
|
||||
// In forum topics first message is ignored, but not in General
|
||||
if (chat.isForum && threadId !== GENERAL_TOPIC_ID) return threadInfo.messagesCount - 1;
|
||||
return threadInfo.messagesCount;
|
||||
}
|
||||
|
||||
export function selectThreadByMessage<T extends GlobalState>(global: T, message: ApiMessage) {
|
||||
const threadId = selectThreadIdFromMessage(global, message);
|
||||
if (!threadId || threadId === MAIN_THREAD_ID) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return global.messages.byChatId[message.chatId].threadsById[threadId];
|
||||
return selectThreadLocalStateParam(global, chatId, threadId, 'firstMessageId');
|
||||
}
|
||||
|
||||
export function selectIsMessageInCurrentMessageList<T extends GlobalState>(
|
||||
@ -391,18 +299,31 @@ export function selectIsMessageFocused<T extends GlobalState>(
|
||||
return focusedId ? focusedId === message.id || focusedId === message.previousLocalId : false;
|
||||
}
|
||||
|
||||
export function selectIsMessageUnread<T extends GlobalState>(global: T, message: ApiMessage) {
|
||||
const { lastReadOutboxMessageId } = selectChat(global, message.chatId) || {};
|
||||
return isMessageLocal(message) || !lastReadOutboxMessageId || lastReadOutboxMessageId < message.id;
|
||||
export function selectIsMessageUnread<T extends GlobalState>(
|
||||
global: T, chatId: string, threadId: ThreadId, messageId: number, messageListType: MessageListType,
|
||||
) {
|
||||
const { lastReadOutboxMessageId } = selectThreadReadState(global, chatId, threadId) || {};
|
||||
const message = selectChatMessage(global, chatId, messageId);
|
||||
if (!message) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return messageListType === 'scheduled' || isMessageLocal(message)
|
||||
|| !lastReadOutboxMessageId || lastReadOutboxMessageId < message.id;
|
||||
}
|
||||
|
||||
export function selectOutgoingStatus<T extends GlobalState>(
|
||||
global: T, message: ApiMessage, isScheduledList = false,
|
||||
global: T, chatId: string, threadId: ThreadId, messageId: number, messageListType: MessageListType,
|
||||
): ApiMessageOutgoingStatus {
|
||||
if (!selectIsMessageUnread(global, message) && !isScheduledList) {
|
||||
if (!selectIsMessageUnread(global, chatId, threadId, messageId, messageListType)) {
|
||||
return 'read';
|
||||
}
|
||||
|
||||
const message = selectChatMessage(global, chatId, messageId);
|
||||
if (!message) {
|
||||
return 'failed'; // Should never happen
|
||||
}
|
||||
|
||||
return getSendingState(message);
|
||||
}
|
||||
|
||||
@ -514,24 +435,15 @@ export function selectFullWebPageFromMessage<T extends GlobalState>(global: T, m
|
||||
return selectFullWebPage(global, message.content.webPage.id);
|
||||
}
|
||||
|
||||
export function selectTopicFromMessage<T extends GlobalState>(global: T, message: ApiMessage) {
|
||||
const { chatId } = message;
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat?.isForum) return undefined;
|
||||
|
||||
const threadId = selectThreadIdFromMessage(global, message);
|
||||
return selectTopic(global, chatId, threadId);
|
||||
}
|
||||
|
||||
const MAX_MESSAGES_TO_DELETE_OWNER_TOPIC = 10;
|
||||
export function selectCanDeleteOwnerTopic<T extends GlobalState>(global: T, chatId: string, topicId: number) {
|
||||
const topic = selectTopic(global, chatId, topicId);
|
||||
if (topic && !topic.isOwner) return false;
|
||||
|
||||
const thread = selectThread(global, chatId, topicId);
|
||||
if (!thread) return false;
|
||||
const localThreadState = selectThreadLocalState(global, chatId, topicId);
|
||||
if (!localThreadState) return false;
|
||||
|
||||
const { listedIds } = thread;
|
||||
const { listedIds } = localThreadState;
|
||||
if (!listedIds
|
||||
// Plus one for root message
|
||||
|| listedIds.length + 1 >= MAX_MESSAGES_TO_DELETE_OWNER_TOPIC) {
|
||||
@ -559,59 +471,6 @@ export function selectCanDeleteTopic<T extends GlobalState>(global: T, chatId: s
|
||||
&& selectCanDeleteOwnerTopic(global, chat.id, topicId));
|
||||
}
|
||||
|
||||
export function selectSavedDialogIdFromMessage<T extends GlobalState>(
|
||||
global: T, message: ApiMessage,
|
||||
): string | undefined {
|
||||
const {
|
||||
chatId, senderId, forwardInfo, savedPeerId,
|
||||
} = message;
|
||||
|
||||
if (savedPeerId) return savedPeerId;
|
||||
|
||||
if (chatId !== global.currentUserId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (forwardInfo?.savedFromPeerId) {
|
||||
return forwardInfo.savedFromPeerId;
|
||||
}
|
||||
|
||||
if (forwardInfo?.fromId) {
|
||||
return forwardInfo.fromId;
|
||||
}
|
||||
|
||||
if (forwardInfo?.hiddenUserName) {
|
||||
return ANONYMOUS_USER_ID;
|
||||
}
|
||||
|
||||
return senderId;
|
||||
}
|
||||
|
||||
export function selectThreadIdFromMessage<T extends GlobalState>(global: T, message: ApiMessage): ThreadId {
|
||||
const savedDialogId = selectSavedDialogIdFromMessage(global, message);
|
||||
if (savedDialogId) {
|
||||
return savedDialogId;
|
||||
}
|
||||
|
||||
const chat = selectChat(global, message.chatId);
|
||||
const { content } = message;
|
||||
const { replyToMsgId, replyToTopId, isForumTopic } = getMessageReplyInfo(message) || {};
|
||||
if (content.action?.type === 'topicCreate') {
|
||||
return message.id;
|
||||
}
|
||||
|
||||
if (!chat?.isForum) {
|
||||
if (chat && isChatBasicGroup(chat)) return MAIN_THREAD_ID;
|
||||
|
||||
if (chat && isChatSuperGroup(chat)) {
|
||||
return replyToTopId || replyToMsgId || MAIN_THREAD_ID;
|
||||
}
|
||||
return MAIN_THREAD_ID;
|
||||
}
|
||||
if (!isForumTopic) return GENERAL_TOPIC_ID;
|
||||
return replyToTopId || replyToMsgId || GENERAL_TOPIC_ID;
|
||||
}
|
||||
|
||||
export function selectCanReplyToMessage<T extends GlobalState>(global: T, message: ApiMessage, threadId: ThreadId) {
|
||||
const chat = selectChat(global, message.chatId);
|
||||
const isRestricted = selectIsChatRestricted(global, message.chatId);
|
||||
@ -917,37 +776,23 @@ export function selectUploadProgress<T extends GlobalState>(global: T, message:
|
||||
}
|
||||
|
||||
export function selectRealLastReadId<T extends GlobalState>(global: T, chatId: string, threadId: ThreadId) {
|
||||
if (threadId === MAIN_THREAD_ID) {
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// `lastReadInboxMessageId` is empty for new chats
|
||||
if (!chat.lastReadInboxMessageId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const lastMessageId = selectChatLastMessageId(global, chatId);
|
||||
|
||||
if (!lastMessageId || chat.unreadCount) {
|
||||
return chat.lastReadInboxMessageId;
|
||||
}
|
||||
|
||||
return lastMessageId;
|
||||
} else {
|
||||
const threadInfo = selectThreadInfo(global, chatId, threadId);
|
||||
if (!threadInfo) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (!threadInfo.lastReadInboxMessageId) {
|
||||
return Number(threadInfo.threadId);
|
||||
}
|
||||
|
||||
// Some previously read messages may be deleted
|
||||
return Math.min(threadInfo.lastReadInboxMessageId, threadInfo.lastMessageId || Infinity);
|
||||
const readState = selectThreadReadState(global, chatId, threadId);
|
||||
if (!readState) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// `lastReadInboxMessageId` is empty for new chats
|
||||
if (!readState.lastReadInboxMessageId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const lastMessageId = selectChatLastMessageId(global, chatId);
|
||||
|
||||
if (!lastMessageId || readState.unreadCount) {
|
||||
return readState.lastReadInboxMessageId;
|
||||
}
|
||||
|
||||
return lastMessageId;
|
||||
}
|
||||
|
||||
export function selectFirstUnreadId<T extends GlobalState>(
|
||||
@ -961,8 +806,9 @@ export function selectFirstUnreadId<T extends GlobalState>(
|
||||
}
|
||||
} else {
|
||||
const threadInfo = selectThreadInfo(global, chatId, threadId);
|
||||
if (!threadInfo
|
||||
|| (threadInfo.lastMessageId !== undefined && threadInfo.lastMessageId === threadInfo.lastReadInboxMessageId)) {
|
||||
const readState = selectThreadReadState(global, chatId, threadId);
|
||||
if (!threadInfo || !readState
|
||||
|| (threadInfo.lastMessageId !== undefined && threadInfo.lastMessageId === readState.lastReadInboxMessageId)) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { GlobalState, TabArgs } from '../types';
|
||||
|
||||
import { DEFAULT_GIFT_PROFILE_FILTER_OPTIONS } from '../../config';
|
||||
import arePropsShallowEqual from '../../util/arePropsShallowEqual';
|
||||
import { areRecordsShallowEqual } from '../../util/areShallowEqual';
|
||||
import { getCurrentTabId } from '../../util/establishMultitabRole';
|
||||
import {
|
||||
getHasAdminRight, isChatAdmin, isChatChannel,
|
||||
@ -96,7 +96,7 @@ export function selectIsGiftProfileFilterDefault<T extends GlobalState>(
|
||||
global: T,
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
) {
|
||||
return arePropsShallowEqual(selectTabState(global, tabId).savedGifts.filter, DEFAULT_GIFT_PROFILE_FILTER_OPTIONS);
|
||||
return areRecordsShallowEqual(selectTabState(global, tabId).savedGifts.filter, DEFAULT_GIFT_PROFILE_FILTER_OPTIONS);
|
||||
}
|
||||
|
||||
export function selectActiveGiftsCollectionId<T extends GlobalState>(
|
||||
|
||||
@ -44,3 +44,7 @@ export function selectShouldHideReadMarks<T extends GlobalState>(global: T) {
|
||||
export function selectSettingsKeys<T extends GlobalState>(global: T) {
|
||||
return global.settings.byKey;
|
||||
}
|
||||
|
||||
export function selectTimezones<T extends GlobalState>(global: T) {
|
||||
return global.timezones?.byId;
|
||||
}
|
||||
|
||||
176
src/global/selectors/threads.ts
Normal file
176
src/global/selectors/threads.ts
Normal file
@ -0,0 +1,176 @@
|
||||
import type { TabThread, ThreadId, ThreadLocalState } from '../../types';
|
||||
import type { GlobalState, TabArgs } from '../types';
|
||||
import { type ApiMessage, MAIN_THREAD_ID } from '../../api/types';
|
||||
|
||||
import { ANONYMOUS_USER_ID, GENERAL_TOPIC_ID } from '../../config';
|
||||
import { getCurrentTabId } from '../../util/establishMultitabRole';
|
||||
import { isChatBasicGroup, isChatSuperGroup } from '../helpers';
|
||||
import { getMessageReplyInfo } from '../helpers/replies';
|
||||
import { selectChat } from './chats';
|
||||
import { selectTabState } from './tabs';
|
||||
|
||||
export function selectTabThreadParam<T extends GlobalState, K extends keyof TabThread>(
|
||||
global: T,
|
||||
chatId: string,
|
||||
threadId: ThreadId,
|
||||
key: K,
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
) {
|
||||
return selectTabState(global, tabId).tabThreads[chatId]?.[threadId]?.[key];
|
||||
}
|
||||
|
||||
export function selectThreadInfo<T extends GlobalState>(global: T, chatId: string, threadId: ThreadId) {
|
||||
return selectThread(global, chatId, threadId)?.threadInfo;
|
||||
}
|
||||
|
||||
export function selectThreadReadState<T extends GlobalState>(global: T, chatId: string, threadId: ThreadId) {
|
||||
return selectThread(global, chatId, threadId)?.readState;
|
||||
}
|
||||
|
||||
export function selectThreadLocalStateParam<T extends GlobalState, K extends keyof ThreadLocalState>(
|
||||
global: T,
|
||||
chatId: string,
|
||||
threadId: ThreadId,
|
||||
key: K,
|
||||
) {
|
||||
return selectThreadLocalState(global, chatId, threadId)?.[key];
|
||||
}
|
||||
|
||||
export function selectThread<T extends GlobalState>(
|
||||
global: T,
|
||||
chatId: string,
|
||||
threadId: ThreadId,
|
||||
) {
|
||||
const messageInfo = global.messages.byChatId[chatId];
|
||||
if (!messageInfo) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const thread = messageInfo.threadsById[threadId];
|
||||
if (!thread) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return thread;
|
||||
}
|
||||
|
||||
export function selectThreadLocalState<T extends GlobalState>(
|
||||
global: T,
|
||||
chatId: string,
|
||||
threadId: ThreadId,
|
||||
) {
|
||||
return selectThread(global, chatId, threadId)?.localState;
|
||||
}
|
||||
|
||||
export function selectThreadMessagesCount(global: GlobalState, chatId: string, threadId: ThreadId) {
|
||||
const chat = selectChat(global, chatId);
|
||||
const threadInfo = selectThreadInfo(global, chatId, threadId);
|
||||
if (!chat || !threadInfo || threadInfo.messagesCount === undefined) return undefined;
|
||||
// In forum topics first message is ignored, but not in General
|
||||
if (chat.isForum && threadId !== GENERAL_TOPIC_ID) return Math.max(threadInfo.messagesCount - 1, 0);
|
||||
return threadInfo.messagesCount;
|
||||
}
|
||||
|
||||
export function selectThreadByMessage<T extends GlobalState>(global: T, message: ApiMessage) {
|
||||
const threadId = selectThreadIdFromMessage(global, message);
|
||||
if (!threadId || threadId === MAIN_THREAD_ID) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return selectThread(global, message.chatId, threadId);
|
||||
}
|
||||
|
||||
export function selectScrollOffset<T extends GlobalState>(
|
||||
global: T, chatId: string, threadId: ThreadId,
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
) {
|
||||
return selectTabThreadParam(global, chatId, threadId, 'scrollOffset', tabId);
|
||||
}
|
||||
|
||||
export function selectLastScrollOffset<T extends GlobalState>(global: T, chatId: string, threadId: ThreadId) {
|
||||
return selectThreadLocalStateParam(global, chatId, threadId, 'lastScrollOffset');
|
||||
}
|
||||
|
||||
export function selectEditingId<T extends GlobalState>(global: T, chatId: string, threadId: ThreadId) {
|
||||
return selectThreadLocalStateParam(global, chatId, threadId, 'editingId');
|
||||
}
|
||||
|
||||
export function selectEditingDraft<T extends GlobalState>(global: T, chatId: string, threadId: ThreadId) {
|
||||
return selectThreadLocalStateParam(global, chatId, threadId, 'editingDraft');
|
||||
}
|
||||
|
||||
export function selectEditingScheduledId<T extends GlobalState>(global: T, chatId: string) {
|
||||
return selectThreadLocalStateParam(global, chatId, MAIN_THREAD_ID, 'editingScheduledId');
|
||||
}
|
||||
|
||||
export function selectEditingScheduledDraft<T extends GlobalState>(global: T, chatId: string) {
|
||||
return selectThreadLocalStateParam(global, chatId, MAIN_THREAD_ID, 'editingScheduledDraft');
|
||||
}
|
||||
|
||||
export function selectDraft<T extends GlobalState>(global: T, chatId: string, threadId: ThreadId) {
|
||||
return selectThreadLocalStateParam(global, chatId, threadId, 'draft');
|
||||
}
|
||||
|
||||
export function selectNoWebPage<T extends GlobalState>(global: T, chatId: string, threadId: ThreadId) {
|
||||
return selectThreadLocalStateParam(global, chatId, threadId, 'noWebPage');
|
||||
}
|
||||
|
||||
export function selectReplyStack<T extends GlobalState>(
|
||||
global: T, chatId: string, threadId: ThreadId,
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
) {
|
||||
return selectTabThreadParam(global, chatId, threadId, 'replyStack', tabId);
|
||||
}
|
||||
|
||||
export function selectSavedDialogIdFromMessage<T extends GlobalState>(
|
||||
global: T, message: ApiMessage,
|
||||
): string | undefined {
|
||||
const {
|
||||
chatId, senderId, forwardInfo, savedPeerId,
|
||||
} = message;
|
||||
|
||||
if (savedPeerId) return savedPeerId;
|
||||
|
||||
if (chatId !== global.currentUserId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (forwardInfo?.savedFromPeerId) {
|
||||
return forwardInfo.savedFromPeerId;
|
||||
}
|
||||
|
||||
if (forwardInfo?.fromId) {
|
||||
return forwardInfo.fromId;
|
||||
}
|
||||
|
||||
if (forwardInfo?.hiddenUserName) {
|
||||
return ANONYMOUS_USER_ID;
|
||||
}
|
||||
|
||||
return senderId;
|
||||
}
|
||||
|
||||
export function selectThreadIdFromMessage<T extends GlobalState>(global: T, message: ApiMessage): ThreadId {
|
||||
const savedDialogId = selectSavedDialogIdFromMessage(global, message);
|
||||
if (savedDialogId) {
|
||||
return savedDialogId;
|
||||
}
|
||||
|
||||
const chat = selectChat(global, message.chatId);
|
||||
const { content } = message;
|
||||
const { replyToMsgId, replyToTopId, isForumTopic } = getMessageReplyInfo(message) || {};
|
||||
if (content.action?.type === 'topicCreate') {
|
||||
return message.id;
|
||||
}
|
||||
|
||||
if (!chat?.isForum) {
|
||||
if (chat && isChatBasicGroup(chat)) return MAIN_THREAD_ID;
|
||||
|
||||
if (chat && isChatSuperGroup(chat)) {
|
||||
return replyToTopId || replyToMsgId || MAIN_THREAD_ID;
|
||||
}
|
||||
return MAIN_THREAD_ID;
|
||||
}
|
||||
if (!isForumTopic) return GENERAL_TOPIC_ID;
|
||||
return replyToTopId || replyToMsgId || GENERAL_TOPIC_ID;
|
||||
}
|
||||
@ -1,6 +1,10 @@
|
||||
import type { ApiMessage } from '../../api/types';
|
||||
import type { ThreadId, TopicsInfo } from '../../types';
|
||||
import type { GlobalState } from '../types';
|
||||
|
||||
import { selectChat } from './chats';
|
||||
import { selectThreadIdFromMessage } from './threads';
|
||||
|
||||
export function selectTopicsInfo<T extends GlobalState>(global: T, chatId: string): TopicsInfo | undefined {
|
||||
return global.chats.topicsInfoById[chatId];
|
||||
}
|
||||
@ -12,3 +16,12 @@ export function selectTopics<T extends GlobalState>(global: T, chatId: string) {
|
||||
export function selectTopic<T extends GlobalState>(global: T, chatId: string, threadId: ThreadId) {
|
||||
return selectTopicsInfo(global, chatId)?.topicsById?.[threadId];
|
||||
}
|
||||
|
||||
export function selectTopicFromMessage<T extends GlobalState>(global: T, message: ApiMessage) {
|
||||
const { chatId } = message;
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat?.isForum) return undefined;
|
||||
|
||||
const threadId = selectThreadIdFromMessage(global, message);
|
||||
return selectTopic(global, chatId, threadId);
|
||||
}
|
||||
|
||||
@ -529,17 +529,13 @@ export interface ActionPayloads {
|
||||
maxId: number;
|
||||
} & WithTabId;
|
||||
markMessagesRead: {
|
||||
chatId: string;
|
||||
messageIds: number[];
|
||||
shouldFetchUnreadReactions?: boolean;
|
||||
} & WithTabId;
|
||||
};
|
||||
loadMessage: {
|
||||
chatId: string;
|
||||
messageId: number;
|
||||
replyOriginForId?: number;
|
||||
threadUpdate?: {
|
||||
lastMessageId: number;
|
||||
isDeleting?: boolean;
|
||||
};
|
||||
};
|
||||
loadMessagesById: {
|
||||
chatId: string;
|
||||
@ -1410,12 +1406,14 @@ export interface ActionPayloads {
|
||||
threadId: ThreadId;
|
||||
type: MessageListType;
|
||||
};
|
||||
fetchUnreadMentions: {
|
||||
loadUnreadMentions: {
|
||||
chatId: string;
|
||||
threadId?: ThreadId;
|
||||
offsetId?: number;
|
||||
};
|
||||
fetchUnreadReactions: {
|
||||
loadUnreadReactions: {
|
||||
chatId: string;
|
||||
threadId?: ThreadId;
|
||||
offsetId?: number;
|
||||
};
|
||||
scheduleForViewsIncrement: {
|
||||
@ -1441,10 +1439,17 @@ export interface ActionPayloads {
|
||||
quickReplyId: number;
|
||||
};
|
||||
animateUnreadReaction: {
|
||||
chatId: string;
|
||||
messageIds: number[];
|
||||
} & WithTabId;
|
||||
focusNextReaction: WithTabId | undefined;
|
||||
focusNextMention: WithTabId | undefined;
|
||||
focusNextReaction: {
|
||||
chatId: string;
|
||||
threadId?: ThreadId;
|
||||
} & WithTabId;
|
||||
focusNextMention: {
|
||||
chatId: string;
|
||||
threadId?: ThreadId;
|
||||
} & WithTabId;
|
||||
readAllReactions: {
|
||||
chatId: string;
|
||||
threadId?: ThreadId;
|
||||
@ -1456,7 +1461,7 @@ export interface ActionPayloads {
|
||||
markMentionsRead: {
|
||||
chatId: string;
|
||||
messageIds: number[];
|
||||
} & WithTabId;
|
||||
};
|
||||
copyMessageLink: {
|
||||
chatId: string;
|
||||
messageId: number;
|
||||
|
||||
@ -1,11 +1,119 @@
|
||||
import { useRef, useSignal } from '../../lib/teact/teact';
|
||||
import { addCallback } from '../../lib/teact/teactn';
|
||||
import { getGlobal } from '../../global';
|
||||
|
||||
import type { GlobalState } from '../../global/types';
|
||||
|
||||
import areShallowEqual from '../../util/areShallowEqual';
|
||||
import { createSignal, type Signal, type SignalSetter } from '../../util/signals';
|
||||
import useDerivedState from '../useDerivedState';
|
||||
import useSelectorSignal from './useSelectorSignal';
|
||||
import useSyncEffect from '../useSyncEffect';
|
||||
|
||||
type Selector<T> = (global: GlobalState) => T;
|
||||
type EqualityFn<T> = (oldValue: T, newValue: T) => boolean;
|
||||
|
||||
export default function useSelector<T>(selector: Selector<T>) {
|
||||
const selectorSignal = useSelectorSignal(selector);
|
||||
return useDerivedState(selectorSignal);
|
||||
interface State<T> {
|
||||
clientsCount: number;
|
||||
getter: Signal<T>;
|
||||
setter: SignalSetter<T>;
|
||||
}
|
||||
|
||||
const bySelector = new Map<Selector<unknown>, State<unknown>>();
|
||||
|
||||
let currentGlobal = getGlobal();
|
||||
addCallback((global: GlobalState) => {
|
||||
currentGlobal = global;
|
||||
|
||||
for (const [selector, { setter }] of bySelector) {
|
||||
setter(selector(global));
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* @param selector - A stable or memoized selector function.
|
||||
*/
|
||||
export function useSelectorSignal<T>(selector: Selector<T>): Signal<T> {
|
||||
let state = bySelector.get(selector) as State<T> | undefined;
|
||||
if (!state) {
|
||||
const [getter, setter] = createSignal(selector(currentGlobal));
|
||||
state = { clientsCount: 0, getter, setter };
|
||||
bySelector.set(selector, state as State<unknown>);
|
||||
}
|
||||
|
||||
useSyncEffect(() => {
|
||||
const currentState = bySelector.get(selector);
|
||||
if (!currentState) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
currentState.clientsCount++;
|
||||
|
||||
// Refresh if selector changed
|
||||
const currentValue = selector(currentGlobal);
|
||||
if (currentValue !== currentState.getter()) {
|
||||
currentState.setter(currentValue);
|
||||
}
|
||||
|
||||
return () => {
|
||||
currentState.clientsCount--;
|
||||
|
||||
if (!currentState.clientsCount) {
|
||||
bySelector.delete(selector);
|
||||
}
|
||||
};
|
||||
}, [selector]);
|
||||
|
||||
return state.getter;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param selector - A stable or memoized selector function.
|
||||
*/
|
||||
export function useSelector<T>(selector: Selector<T>) {
|
||||
const selectorSignal = useSelectorSignal(selector);
|
||||
return useDerivedState(selectorSignal, [selectorSignal, selector]);
|
||||
}
|
||||
|
||||
export function useSelectorSignalWithEquality<T>(
|
||||
selector: Selector<T>,
|
||||
equalityFn: EqualityFn<T>,
|
||||
) {
|
||||
const baseSignal = useSelectorSignal(selector);
|
||||
// Initialize with current value
|
||||
const [signal, setSignal] = useSignal(baseSignal());
|
||||
const lastValueRef = useRef(signal());
|
||||
|
||||
useSyncEffect(() => {
|
||||
const checkForUpdate = () => {
|
||||
const newValue = baseSignal();
|
||||
if (!equalityFn(lastValueRef.current, newValue)) {
|
||||
lastValueRef.current = newValue;
|
||||
setSignal(newValue);
|
||||
}
|
||||
};
|
||||
|
||||
checkForUpdate();
|
||||
|
||||
return baseSignal.subscribe(checkForUpdate);
|
||||
}, [baseSignal, equalityFn, setSignal]);
|
||||
|
||||
return signal;
|
||||
}
|
||||
|
||||
export function useSelectorWithEquality<T>(
|
||||
selector: Selector<T>,
|
||||
equalityFn: EqualityFn<T>,
|
||||
) {
|
||||
const signal = useSelectorSignalWithEquality(selector, equalityFn);
|
||||
return useDerivedState(signal);
|
||||
}
|
||||
|
||||
export function useShallowSelectorSignal<T extends AnyLiteral | undefined>(selector: Selector<T>) {
|
||||
return useSelectorSignalWithEquality(selector, areShallowEqual);
|
||||
}
|
||||
|
||||
export function useShallowSelector<T extends AnyLiteral | undefined>(selector: Selector<T>) {
|
||||
return useSelectorWithEquality(selector, areShallowEqual);
|
||||
}
|
||||
|
||||
export default useSelector;
|
||||
|
||||
@ -1,57 +0,0 @@
|
||||
import { addCallback } from '../../lib/teact/teactn';
|
||||
import { getGlobal } from '../../global';
|
||||
|
||||
import type { GlobalState } from '../../global/types';
|
||||
import type { Signal, SignalSetter } from '../../util/signals';
|
||||
|
||||
import { createSignal } from '../../util/signals';
|
||||
import useSyncEffect from '../useSyncEffect';
|
||||
|
||||
/*
|
||||
This hook is a more performant variation of the standard React `useSelector` hook. It allows to:
|
||||
a) Avoid multiple subscriptions to global updates by leveraging a single selector reference.
|
||||
b) Return a signal instead of forcing a component update right away.
|
||||
*/
|
||||
|
||||
type Selector<T> = (global: GlobalState) => T;
|
||||
|
||||
interface State<T> {
|
||||
clientsCount: number;
|
||||
getter: Signal<T>;
|
||||
setter: SignalSetter<T>;
|
||||
}
|
||||
|
||||
const bySelector = new Map<Selector<unknown>, State<unknown>>();
|
||||
|
||||
addCallback((global: GlobalState) => {
|
||||
for (const [selector, { setter }] of bySelector) {
|
||||
setter(selector(global));
|
||||
}
|
||||
});
|
||||
|
||||
function useSelectorSignal<T>(selector: Selector<T>): Signal<T> {
|
||||
let state = bySelector.get(selector) as State<T> | undefined;
|
||||
if (!state) {
|
||||
const [getter, setter] = createSignal(selector(getGlobal()));
|
||||
state = { clientsCount: 0, getter, setter };
|
||||
bySelector.set(selector, state as State<unknown>);
|
||||
}
|
||||
|
||||
useSyncEffect(() => {
|
||||
const state2 = bySelector.get(selector)!;
|
||||
|
||||
state2.clientsCount++;
|
||||
|
||||
return () => {
|
||||
state2.clientsCount--;
|
||||
|
||||
if (!state2.clientsCount) {
|
||||
bySelector.delete(selector);
|
||||
}
|
||||
};
|
||||
}, [selector]);
|
||||
|
||||
return state.getter;
|
||||
}
|
||||
|
||||
export default useSelectorSignal;
|
||||
@ -1,15 +1,18 @@
|
||||
import { useMemo } from '../lib/teact/teact';
|
||||
import { useCallback, useMemo } from '../lib/teact/teact';
|
||||
import { getActions } from '../global';
|
||||
|
||||
import type { ApiChat, ApiTopic, ApiUser } from '../api/types';
|
||||
import type { MenuItemContextAction } from '../components/ui/ListItem';
|
||||
import type { GlobalState } from '../global/types';
|
||||
import { type ApiChat, type ApiUser, MAIN_THREAD_ID } from '../api/types';
|
||||
|
||||
import { SERVICE_NOTIFICATIONS_USER_ID } from '../config';
|
||||
import { getCanDeleteChat, isChatArchived, isChatChannel, isChatGroup } from '../global/helpers';
|
||||
import { selectThreadReadState } from '../global/selectors/threads';
|
||||
import { IS_TAURI } from '../util/browser/globalEnvironment';
|
||||
import { IS_OPEN_IN_NEW_TAB_SUPPORTED } from '../util/browser/windowEnvironment';
|
||||
import { isUserId } from '../util/entities/ids';
|
||||
import { compact } from '../util/iteratees';
|
||||
import { buildCollectionByCallback, compact } from '../util/iteratees';
|
||||
import useSelector, { useShallowSelector } from './data/useSelector';
|
||||
import useLang from './useLang';
|
||||
|
||||
const useChatContextActions = ({
|
||||
@ -22,7 +25,7 @@ const useChatContextActions = ({
|
||||
isSavedDialog,
|
||||
currentUserId,
|
||||
isPreview,
|
||||
topics,
|
||||
topicIds,
|
||||
handleDelete,
|
||||
handleMute,
|
||||
handleUnmute,
|
||||
@ -38,18 +41,42 @@ const useChatContextActions = ({
|
||||
isSavedDialog?: boolean;
|
||||
currentUserId?: string;
|
||||
isPreview?: boolean;
|
||||
topics?: Record<number, ApiTopic>;
|
||||
topicIds?: number[];
|
||||
handleDelete?: NoneToVoidFunction;
|
||||
handleMute?: NoneToVoidFunction;
|
||||
handleUnmute?: NoneToVoidFunction;
|
||||
handleChatFolderChange: NoneToVoidFunction;
|
||||
handleReport?: NoneToVoidFunction;
|
||||
}, isInSearch = false) => {
|
||||
const {
|
||||
toggleChatPinned,
|
||||
toggleSavedDialogPinned,
|
||||
toggleChatArchived,
|
||||
markChatMessagesRead,
|
||||
markChatUnread,
|
||||
openChatInNewTab,
|
||||
openQuickPreview,
|
||||
} = getActions();
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
const { isSelf } = user || {};
|
||||
const isServiceNotifications = user?.id === SERVICE_NOTIFICATIONS_USER_ID;
|
||||
|
||||
const topicsReadStateSelector = useCallback((global: GlobalState) => {
|
||||
if (!chat?.id) return undefined;
|
||||
return buildCollectionByCallback(topicIds || [], (tId) => (
|
||||
[tId, selectThreadReadState(global, chat?.id, tId)]
|
||||
));
|
||||
}, [chat?.id, topicIds]);
|
||||
const topicsReadStates = useShallowSelector(topicsReadStateSelector);
|
||||
|
||||
const chatReadStateSelector = useCallback((global: GlobalState) => {
|
||||
if (!chat?.id) return undefined;
|
||||
return selectThreadReadState(global, chat.id, MAIN_THREAD_ID);
|
||||
}, [chat?.id]);
|
||||
const chatReadState = useSelector(chatReadStateSelector);
|
||||
|
||||
const deleteTitle = useMemo(() => {
|
||||
if (!chat) return undefined;
|
||||
|
||||
@ -72,21 +99,11 @@ const useChatContextActions = ({
|
||||
return lang('GroupLeaveGroup');
|
||||
}, [chat, isSavedDialog, lang]);
|
||||
|
||||
return useMemo(() => {
|
||||
const preparedActions = useMemo(() => {
|
||||
if (!chat || isPreview) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const {
|
||||
toggleChatPinned,
|
||||
toggleSavedDialogPinned,
|
||||
toggleChatArchived,
|
||||
markChatMessagesRead,
|
||||
markChatUnread,
|
||||
openChatInNewTab,
|
||||
openQuickPreview,
|
||||
} = getActions();
|
||||
|
||||
const actionOpenInNewTab = IS_OPEN_IN_NEW_TAB_SUPPORTED && {
|
||||
title: IS_TAURI ? lang('ChatListOpenInNewWindow') : lang('ChatListOpenInNewTab'),
|
||||
icon: 'open-in-new-tab',
|
||||
@ -97,7 +114,7 @@ const useChatContextActions = ({
|
||||
openChatInNewTab({ chatId: chat.id });
|
||||
}
|
||||
},
|
||||
};
|
||||
} satisfies MenuItemContextAction;
|
||||
|
||||
const actionQuickPreview = !isSavedDialog && !chat.isForum && {
|
||||
title: lang('QuickPreview'),
|
||||
@ -107,7 +124,7 @@ const useChatContextActions = ({
|
||||
id: chat.id,
|
||||
});
|
||||
},
|
||||
};
|
||||
} satisfies MenuItemContextAction;
|
||||
|
||||
const togglePinned = () => {
|
||||
if (isSavedDialog) {
|
||||
@ -117,7 +134,7 @@ const useChatContextActions = ({
|
||||
}
|
||||
};
|
||||
|
||||
const actionPin = isPinned
|
||||
const actionPin: MenuItemContextAction = isPinned
|
||||
? {
|
||||
title: lang('ChatListUnpinFromTop'),
|
||||
icon: 'unpin',
|
||||
@ -129,12 +146,12 @@ const useChatContextActions = ({
|
||||
handler: togglePinned,
|
||||
};
|
||||
|
||||
const actionDelete = {
|
||||
const actionDelete = deleteTitle ? {
|
||||
title: deleteTitle,
|
||||
icon: 'delete',
|
||||
destructive: true,
|
||||
handler: handleDelete,
|
||||
};
|
||||
} satisfies MenuItemContextAction : undefined;
|
||||
|
||||
if (isSavedDialog) {
|
||||
return compact([actionOpenInNewTab, actionQuickPreview, actionPin, actionDelete]) as MenuItemContextAction[];
|
||||
@ -144,9 +161,9 @@ const useChatContextActions = ({
|
||||
title: lang('ChatListContextAddToFolder'),
|
||||
icon: 'folder',
|
||||
handler: handleChatFolderChange,
|
||||
} : undefined;
|
||||
} satisfies MenuItemContextAction : undefined;
|
||||
|
||||
const actionMute = isMuted
|
||||
const actionMute: MenuItemContextAction = isMuted
|
||||
? {
|
||||
title: lang('ChatsUnmute'),
|
||||
icon: 'unmute',
|
||||
@ -164,46 +181,51 @@ const useChatContextActions = ({
|
||||
]) as MenuItemContextAction[];
|
||||
}
|
||||
|
||||
const actionMaskAsRead = (
|
||||
chat.unreadCount || chat.hasUnreadMark || Object.values(topics || {}).some(({ unreadCount }) => unreadCount)
|
||||
)
|
||||
const actionMarkAsRead = (
|
||||
chatReadState?.unreadCount || chatReadState?.hasUnreadMark
|
||||
|| Object.values(topicsReadStates || {}).some((readState) => readState?.unreadCount)
|
||||
) ? {
|
||||
title: lang('ChatListContextMarkAsRead'),
|
||||
icon: 'readchats',
|
||||
handler: () => markChatMessagesRead({ id: chat.id }),
|
||||
} satisfies MenuItemContextAction
|
||||
: undefined;
|
||||
const actionMarkAsUnread = !(chatReadState?.unreadCount || chatReadState?.hasUnreadMark) && !chat.isForum
|
||||
? {
|
||||
title: lang('ChatListContextMaskAsRead'),
|
||||
icon: 'readchats',
|
||||
handler: () => markChatMessagesRead({ id: chat.id }),
|
||||
} : undefined;
|
||||
const actionMarkAsUnread = !(chat.unreadCount || chat.hasUnreadMark) && !chat.isForum
|
||||
? { title: lang('ChatListContextMaskAsUnread'), icon: 'unread', handler: () => markChatUnread({ id: chat.id }) }
|
||||
title: lang('ChatListContextMarkAsUnread'), icon: 'unread', handler: () => markChatUnread({ id: chat.id }),
|
||||
} satisfies MenuItemContextAction
|
||||
: undefined;
|
||||
|
||||
const actionArchive = isChatArchived(chat)
|
||||
const actionArchive: MenuItemContextAction = isChatArchived(chat)
|
||||
? { title: lang('Unarchive'), icon: 'unarchive', handler: () => toggleChatArchived({ id: chat.id }) }
|
||||
: { title: lang('Archive'), icon: 'archive', handler: () => toggleChatArchived({ id: chat.id }) };
|
||||
|
||||
const canReport = handleReport && !user && (isChatChannel(chat) || isChatGroup(chat));
|
||||
const actionReport = canReport
|
||||
? { title: lang('ReportPeerReport'), icon: 'flag', handler: handleReport }
|
||||
? { title: lang('ReportPeerReport'), icon: 'flag', handler: handleReport } satisfies MenuItemContextAction
|
||||
: undefined;
|
||||
|
||||
const isInFolder = folderId !== undefined;
|
||||
|
||||
return compact([
|
||||
return compact<MenuItemContextAction>([
|
||||
actionOpenInNewTab,
|
||||
actionQuickPreview,
|
||||
actionAddToFolder,
|
||||
actionMaskAsRead,
|
||||
actionMarkAsRead,
|
||||
actionMarkAsUnread,
|
||||
actionPin,
|
||||
!isSelf && actionMute,
|
||||
!isSelf && !isServiceNotifications && !isInFolder && actionArchive,
|
||||
actionReport,
|
||||
actionDelete,
|
||||
]) as MenuItemContextAction[];
|
||||
]);
|
||||
}, [
|
||||
chat, user, canChangeFolder, lang, handleChatFolderChange, isPinned, isInSearch, isMuted, currentUserId,
|
||||
handleDelete, handleMute, handleReport, folderId, isSelf, isServiceNotifications, isSavedDialog, deleteTitle,
|
||||
isPreview, topics, handleUnmute,
|
||||
chat, isPreview, lang, isSavedDialog, isPinned, deleteTitle, handleDelete, canChangeFolder,
|
||||
handleChatFolderChange, isMuted, handleUnmute, handleMute, isInSearch, chatReadState, topicsReadStates,
|
||||
handleReport, user, folderId, isSelf, isServiceNotifications, currentUserId,
|
||||
]);
|
||||
|
||||
return preparedActions;
|
||||
};
|
||||
|
||||
export default useChatContextActions;
|
||||
|
||||
@ -1,21 +1,42 @@
|
||||
import { useRef, useUnmountCleanup } from '../lib/teact/teact';
|
||||
|
||||
import { requestMeasure } from '../lib/fasterdom/fasterdom';
|
||||
import { cleanupEffect, isSignal } from '../util/signals';
|
||||
|
||||
export function useSignalEffect(effect: NoneToVoidFunction, dependencies: readonly any[]) {
|
||||
// The is extracted from `useEffectOnce` to run before all effects
|
||||
const isFirstRun = useRef(true);
|
||||
if (isFirstRun.current) {
|
||||
isFirstRun.current = false;
|
||||
// This runs before all effects
|
||||
const prevDepsRef = useRef<readonly unknown[]>();
|
||||
const subscribedEffectRef = useRef<NoneToVoidFunction>();
|
||||
|
||||
dependencies?.forEach((dependency) => {
|
||||
const prevDeps = prevDepsRef.current;
|
||||
const hasChanged = !prevDeps
|
||||
|| dependencies.length !== prevDeps.length
|
||||
|| dependencies.some((dep, i) => dep !== prevDeps[i]);
|
||||
|
||||
if (hasChanged) {
|
||||
if (subscribedEffectRef.current) {
|
||||
cleanupEffect(subscribedEffectRef.current);
|
||||
}
|
||||
|
||||
subscribedEffectRef.current = effect;
|
||||
prevDepsRef.current = dependencies;
|
||||
|
||||
dependencies.forEach((dependency) => {
|
||||
if (isSignal(dependency)) {
|
||||
dependency.subscribe(effect);
|
||||
}
|
||||
});
|
||||
|
||||
const currentEffect = effect;
|
||||
requestMeasure(() => {
|
||||
if (subscribedEffectRef.current !== currentEffect) return;
|
||||
currentEffect();
|
||||
});
|
||||
}
|
||||
|
||||
useUnmountCleanup(() => {
|
||||
cleanupEffect(effect);
|
||||
if (subscribedEffectRef.current) {
|
||||
cleanupEffect(subscribedEffectRef.current);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@
|
||||
import type { ReactElement } from 'react';
|
||||
|
||||
import { DEBUG, DEBUG_MORE } from '../../config';
|
||||
import { logUnequalProps } from '../../util/arePropsShallowEqual';
|
||||
import { logUnequalProps } from '../../util/areShallowEqual';
|
||||
import { incrementOverlayCounter } from '../../util/debugOverlay';
|
||||
import { orderBy } from '../../util/iteratees';
|
||||
import safeExec from '../../util/safeExec';
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { FC, FC_withDebug, Props } from './teact';
|
||||
|
||||
import { DEBUG, DEBUG_MORE } from '../../config';
|
||||
import arePropsShallowEqual, { logUnequalProps } from '../../util/arePropsShallowEqual';
|
||||
import { areRecordsShallowEqual, logUnequalProps } from '../../util/areShallowEqual';
|
||||
import Deferred from '../../util/Deferred';
|
||||
import { handleError } from '../../util/handleError';
|
||||
import { orderBy } from '../../util/iteratees';
|
||||
@ -220,7 +220,7 @@ function updateContainers() {
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(newMappedProps).length && !arePropsShallowEqual(mappedProps!, newMappedProps)) {
|
||||
if (Object.keys(newMappedProps).length && !areRecordsShallowEqual(mappedProps!, newMappedProps)) {
|
||||
if (DEBUG_MORE) {
|
||||
logUnequalProps(
|
||||
mappedProps!,
|
||||
@ -296,7 +296,7 @@ export function withUntypedGlobal<OwnProps extends AnyLiteral>(
|
||||
}
|
||||
|
||||
if (
|
||||
(!container.mappedProps || !arePropsShallowEqual(container.ownProps, props))
|
||||
(!container.mappedProps || !areRecordsShallowEqual(container.ownProps, props))
|
||||
&& activateContainer(container, currentGlobal, props)
|
||||
) {
|
||||
try {
|
||||
|
||||
@ -606,25 +606,47 @@ export interface TabThread {
|
||||
viewportIds?: number[];
|
||||
}
|
||||
|
||||
export interface Thread {
|
||||
export interface ThreadReadState {
|
||||
unreadCount?: number;
|
||||
unreadMentionsCount?: number;
|
||||
unreadReactionsCount?: number;
|
||||
unreadReactions?: number[];
|
||||
unreadMentions?: number[];
|
||||
hasUnreadMark?: boolean;
|
||||
|
||||
lastReadOutboxMessageId?: number;
|
||||
lastReadInboxMessageId?: number;
|
||||
}
|
||||
|
||||
export interface ThreadLocalState {
|
||||
lastScrollOffset?: number;
|
||||
lastViewportIds?: number[];
|
||||
listedIds?: number[];
|
||||
outlyingLists?: number[][];
|
||||
pinnedIds?: number[];
|
||||
scheduledIds?: number[];
|
||||
firstMessageId?: number;
|
||||
|
||||
editingId?: number;
|
||||
editingScheduledId?: number;
|
||||
editingDraft?: ApiFormattedText;
|
||||
editingScheduledDraft?: ApiFormattedText;
|
||||
|
||||
draft?: ApiDraft;
|
||||
|
||||
noWebPage?: boolean;
|
||||
threadInfo?: ApiThreadInfo;
|
||||
firstMessageId?: number;
|
||||
|
||||
typingStatus?: ApiTypingStatus;
|
||||
|
||||
typingDraftIdByRandomId?: Record<string, number>;
|
||||
}
|
||||
|
||||
export interface Thread {
|
||||
localState: ThreadLocalState;
|
||||
threadInfo: ApiThreadInfo;
|
||||
readState: ThreadReadState;
|
||||
}
|
||||
|
||||
export interface ServiceNotification {
|
||||
id: number;
|
||||
message: ApiMessage;
|
||||
|
||||
5
src/types/language.d.ts
vendored
5
src/types/language.d.ts
vendored
@ -1059,8 +1059,8 @@ export interface LangPair {
|
||||
'ChatListPinToTop': undefined;
|
||||
'ChatListOpenInNewWindow': undefined;
|
||||
'ChatListOpenInNewTab': undefined;
|
||||
'ChatListContextMaskAsRead': undefined;
|
||||
'ChatListContextMaskAsUnread': undefined;
|
||||
'ChatListContextMarkAsRead': undefined;
|
||||
'ChatListContextMarkAsUnread': undefined;
|
||||
'ChatListContextAddToFolder': undefined;
|
||||
'Unarchive': undefined;
|
||||
'Archive': undefined;
|
||||
@ -1886,6 +1886,7 @@ export interface LangPair {
|
||||
'SettingsBirthday': undefined;
|
||||
'BotReadTextFromClipboardTitle': undefined;
|
||||
'BotReadTextFromClipboardConfirm': undefined;
|
||||
'ChatInfoForumTopic': undefined;
|
||||
'DiceToastSend': undefined;
|
||||
'ChatTypePrivate': undefined;
|
||||
'ChatTypeGroup': undefined;
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
export default function arePropsShallowEqual(currentProps: AnyLiteral, newProps: AnyLiteral) {
|
||||
export function areRecordsShallowEqual(currentProps: AnyLiteral, newProps: AnyLiteral) {
|
||||
if (currentProps === newProps) {
|
||||
return true;
|
||||
}
|
||||
@ -25,6 +25,27 @@ export default function arePropsShallowEqual(currentProps: AnyLiteral, newProps:
|
||||
return true;
|
||||
}
|
||||
|
||||
export function areArraysShallowEqual(currentProps: unknown[], newProps: unknown[]) {
|
||||
if (currentProps === newProps) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (currentProps.length !== newProps.length) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return currentProps.every((item, i) => item === newProps[i]);
|
||||
}
|
||||
|
||||
export default function areShallowEqual<T extends AnyLiteral | unknown[] | undefined>(oldValue: T, newValue: T) {
|
||||
if (oldValue === newValue) return true;
|
||||
if (oldValue === undefined || newValue === undefined) return false;
|
||||
if (Array.isArray(oldValue) && Array.isArray(newValue)) {
|
||||
return areArraysShallowEqual(oldValue, newValue);
|
||||
}
|
||||
return areRecordsShallowEqual(oldValue, newValue);
|
||||
}
|
||||
|
||||
export function logUnequalProps(currentProps: AnyLiteral, newProps: AnyLiteral, msg: string, debugKey = '') {
|
||||
const currentKeys = Object.keys(currentProps);
|
||||
const currentKeysLength = currentKeys.length;
|
||||
@ -2,11 +2,12 @@ import { onFullyIdle } from '../lib/teact/teact';
|
||||
import { addCallback } from '../lib/teact/teactn';
|
||||
import { addActionHandler, getGlobal } from '../global';
|
||||
|
||||
import type {
|
||||
ApiChat, ApiChatFolder, ApiNotifyPeerType, ApiPeerNotifySettings, ApiUser,
|
||||
} from '../api/types';
|
||||
import type { GlobalState } from '../global/types';
|
||||
import type { CallbackManager } from './callbacks';
|
||||
import {
|
||||
type ApiChat, type ApiChatFolder, type ApiNotifyPeerType, type ApiPeerNotifySettings, type ApiUser,
|
||||
MAIN_THREAD_ID,
|
||||
} from '../api/types';
|
||||
|
||||
import {
|
||||
ALL_FOLDER_ID, ARCHIVED_FOLDER_ID, DEBUG, SAVED_FOLDER_ID, SERVICE_NOTIFICATIONS_USER_ID,
|
||||
@ -19,7 +20,8 @@ import {
|
||||
selectTabState,
|
||||
selectTopics,
|
||||
} from '../global/selectors';
|
||||
import arePropsShallowEqual from './arePropsShallowEqual';
|
||||
import { selectDraft, selectThreadReadState } from '../global/selectors/threads';
|
||||
import { areRecordsShallowEqual } from './areShallowEqual';
|
||||
import { createCallbackManager } from './callbacks';
|
||||
import { areSortedArraysEqual, unique } from './iteratees';
|
||||
import { throttle } from './schedulers';
|
||||
@ -486,7 +488,7 @@ function updateChats(
|
||||
isRemovedFromSaved,
|
||||
);
|
||||
|
||||
if (!areFoldersChanged && currentSummary && arePropsShallowEqual(newSummary, currentSummary)) {
|
||||
if (!areFoldersChanged && currentSummary && areRecordsShallowEqual(newSummary, currentSummary)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -533,16 +535,21 @@ function buildChatSummary<T extends GlobalState>(
|
||||
): ChatSummary {
|
||||
const {
|
||||
id, type, isNotJoined, migratedTo, folderId,
|
||||
unreadCount: chatUnreadCount, unreadMentionsCount: chatUnreadMentionsCount, hasUnreadMark,
|
||||
isForum,
|
||||
} = chat;
|
||||
const draft = selectDraft(global, id, MAIN_THREAD_ID);
|
||||
const isRestricted = selectIsChatRestricted(global, id);
|
||||
const topics = selectTopics(global, chat.id);
|
||||
const chatReadState = selectThreadReadState(global, chat.id, MAIN_THREAD_ID);
|
||||
const {
|
||||
unreadCount: chatUnreadCount, unreadMentionsCount: chatUnreadMentionsCount, hasUnreadMark,
|
||||
} = chatReadState || {};
|
||||
|
||||
const { unreadCount, unreadMentionsCount } = isForum
|
||||
? Object.values(topics || {}).reduce((acc, topic) => {
|
||||
acc.unreadCount += topic.unreadCount;
|
||||
acc.unreadMentionsCount += topic.unreadMentionsCount;
|
||||
const topicReadState = selectThreadReadState(global, chat.id, topic.id);
|
||||
acc.unreadCount += topicReadState?.unreadCount || 0;
|
||||
acc.unreadMentionsCount += topicReadState?.unreadMentionsCount || 0;
|
||||
|
||||
return acc;
|
||||
}, { unreadCount: 0, unreadMentionsCount: 0 })
|
||||
@ -554,7 +561,7 @@ function buildChatSummary<T extends GlobalState>(
|
||||
!lastMessage || lastMessage.content.action?.type === 'historyClear'
|
||||
);
|
||||
|
||||
const orderInAll = Math.max(chat.creationDate || 0, chat.draftDate || 0, lastMessage?.date || 0);
|
||||
const orderInAll = Math.max(chat.creationDate || 0, draft?.date || 0, lastMessage?.date || 0);
|
||||
|
||||
const lastMessageInSaved = selectChatLastMessage(global, chat.id, 'saved');
|
||||
const orderInSaved = lastMessageInSaved?.date || 0;
|
||||
@ -731,7 +738,7 @@ function updateResults(affectedFolderIds: number[]) {
|
||||
const newUnreadCounters = buildFolderUnreadCounters(folderId);
|
||||
if (!wasUnreadCountersChanged) {
|
||||
wasUnreadCountersChanged = (
|
||||
!currentUnreadCounters || !arePropsShallowEqual(newUnreadCounters, currentUnreadCounters)
|
||||
!currentUnreadCounters || !areRecordsShallowEqual(newUnreadCounters, currentUnreadCounters)
|
||||
);
|
||||
}
|
||||
results.unreadCountersByFolderId[folderId] = newUnreadCounters;
|
||||
|
||||
@ -14,7 +14,7 @@ export function buildCollectionByKey<T extends AnyLiteral>(collection: T[], key:
|
||||
}, {});
|
||||
}
|
||||
|
||||
export function buildCollectionByCallback<T extends AnyLiteral, K extends number | string, R>(
|
||||
export function buildCollectionByCallback<T, K extends number | string, R>(
|
||||
collection: T[],
|
||||
callback: (member: T) => [K, R],
|
||||
) {
|
||||
@ -36,6 +36,19 @@ export function mapValues<R, M>(
|
||||
}, {});
|
||||
}
|
||||
|
||||
export function mapTruthyValues<R, M>(
|
||||
byKey: CollectionByKey<M>,
|
||||
callback: (member: M, key: string, index: number, originalByKey: CollectionByKey<M>) => R | Falsy,
|
||||
): CollectionByKey<R> {
|
||||
return Object.keys(byKey).reduce((newByKey: CollectionByKey<R>, key, index) => {
|
||||
const value = callback(byKey[key], key, index, byKey);
|
||||
if (value) {
|
||||
newByKey[key] = value;
|
||||
}
|
||||
return newByKey;
|
||||
}, {});
|
||||
}
|
||||
|
||||
export function pick<T, K extends keyof T>(object: T, keys: K[]) {
|
||||
return keys.reduce((result, key) => {
|
||||
result[key] = object[key];
|
||||
@ -109,7 +122,7 @@ export function uniqueByField<T>(array: T[], field: keyof T): T[] {
|
||||
return [...new Map(array.map((item) => [item[field], item])).values()];
|
||||
}
|
||||
|
||||
export function compact<T>(array: T[]) {
|
||||
export function compact<T>(array: (T | Falsy)[]): T[] {
|
||||
return array.filter(Boolean);
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user