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