Introduce Saved dialogs (#4177)

This commit is contained in:
Alexander Zinchuk 2024-02-06 16:48:54 +01:00
parent 44261055e6
commit c3c71cbc9e
120 changed files with 2185 additions and 1027 deletions

View File

@ -1,7 +1,9 @@
export class ChatAbortController extends AbortController { import type { ThreadId } from '../../types';
private threads = new Map<number, AbortController>();
public getThreadSignal(threadId: number): AbortSignal { export class ChatAbortController extends AbortController {
private threads = new Map<ThreadId, AbortController>();
public getThreadSignal(threadId: ThreadId): AbortSignal {
let controller = this.threads.get(threadId); let controller = this.threads.get(threadId);
if (!controller) { if (!controller) {
controller = new AbortController(); controller = new AbortController();
@ -10,7 +12,7 @@ export class ChatAbortController extends AbortController {
return controller.signal; return controller.signal;
} }
public abortThread(threadId: number, reason?: string): void { public abortThread(threadId: ThreadId, reason?: string): void {
this.threads.get(threadId)?.abort(reason); this.threads.get(threadId)?.abort(reason);
this.threads.delete(threadId); this.threads.delete(threadId);
} }

View File

@ -29,7 +29,8 @@ type Limit =
| 'about_length_limit' | 'about_length_limit'
| 'chatlist_invites_limit' | 'chatlist_invites_limit'
| 'chatlist_joined_limit' | 'chatlist_joined_limit'
| 'recommended_channels_limit'; | 'recommended_channels_limit'
| 'saved_dialogs_pinned_limit';
type LimitKey = `${Limit}_${LimitType}`; type LimitKey = `${Limit}_${LimitType}`;
type LimitsConfig = Record<LimitKey, number>; type LimitsConfig = Record<LimitKey, number>;
@ -124,6 +125,7 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp
chatlistInvites: getLimit(appConfig, 'chatlist_invites_limit', 'chatlistInvites'), chatlistInvites: getLimit(appConfig, 'chatlist_invites_limit', 'chatlistInvites'),
chatlistJoined: getLimit(appConfig, 'chatlist_joined_limit', 'chatlistJoined'), chatlistJoined: getLimit(appConfig, 'chatlist_joined_limit', 'chatlistJoined'),
recommendedChannels: getLimit(appConfig, 'recommended_channels_limit', 'recommendedChannels'), recommendedChannels: getLimit(appConfig, 'recommended_channels_limit', 'recommendedChannels'),
savedDialogsPinned: getLimit(appConfig, 'saved_dialogs_pinned_limit', 'savedDialogsPinned'),
}, },
hash, hash,
areStoriesHidden: appConfig.stories_all_hidden, areStoriesHidden: appConfig.stories_all_hidden,

View File

@ -126,6 +126,20 @@ export function buildApiChatFromDialog(
}; };
} }
export function buildApiChatFromSavedDialog(
dialog: GramJs.SavedDialog,
peerEntity: GramJs.TypeUser | GramJs.TypeChat,
): ApiChat {
const { peer } = dialog;
return {
id: getApiChatIdFromMtpPeer(peer),
type: getApiChatTypeFromPeerEntity(peerEntity),
title: getApiChatTitleFromMtpPeer(peer, peerEntity),
...buildApiChatFieldsFromPeerEntity(peerEntity),
};
}
function buildApiChatPermissions(peerEntity: GramJs.TypeUser | GramJs.TypeChat): { function buildApiChatPermissions(peerEntity: GramJs.TypeUser | GramJs.TypeChat): {
adminRights?: ApiChatAdminRights; adminRights?: ApiChatAdminRights;
currentUserBannedRights?: ApiChatBannedRights; currentUserBannedRights?: ApiChatBannedRights;

View File

@ -266,13 +266,15 @@ function buildApiMessageForwardInfo(fwdFrom: GramJs.MessageFwdHeader, isChatWith
return { return {
date: fwdFrom.date, date: fwdFrom.date,
savedDate: fwdFrom.savedDate,
isImported: fwdFrom.imported, isImported: fwdFrom.imported,
isChannelPost: Boolean(fwdFrom.channelPost), isChannelPost: Boolean(fwdFrom.channelPost),
channelPostId: fwdFrom.channelPost, channelPostId: fwdFrom.channelPost,
isLinkedChannelPost: Boolean(fwdFrom.channelPost && savedFromPeerId && !isChatWithSelf), isLinkedChannelPost: Boolean(fwdFrom.channelPost && savedFromPeerId && !isChatWithSelf),
savedFromPeerId,
fromId,
fromChatId: savedFromPeerId || fromId, fromChatId: savedFromPeerId || fromId,
fromMessageId: fwdFrom.savedFromMsgId || fwdFrom.channelPost, fromMessageId: fwdFrom.savedFromMsgId || fwdFrom.channelPost,
senderUserId: fromId,
hiddenUserName: fwdFrom.fromName, hiddenUserName: fwdFrom.fromName,
postAuthorTitle: fwdFrom.postAuthor, postAuthorTitle: fwdFrom.postAuthor,
}; };
@ -747,6 +749,7 @@ function buildNewPoll(poll: ApiNewPoll, localId: number) {
export function buildLocalMessage( export function buildLocalMessage(
chat: ApiChat, chat: ApiChat,
lastMessageId?: number,
text?: string, text?: string,
entities?: ApiMessageEntity[], entities?: ApiMessageEntity[],
replyInfo?: ApiInputReplyInfo, replyInfo?: ApiInputReplyInfo,
@ -760,7 +763,7 @@ export function buildLocalMessage(
sendAs?: ApiPeer, sendAs?: ApiPeer,
story?: ApiStory | ApiStorySkipped, story?: ApiStory | ApiStorySkipped,
): ApiMessage { ): ApiMessage {
const localId = getNextLocalMessageId(chat.lastMessage?.id); const localId = getNextLocalMessageId(lastMessageId);
const media = attachment && buildUploadingMedia(attachment); const media = attachment && buildUploadingMedia(attachment);
const isChannel = chat.type === 'chatTypeChannel'; const isChannel = chat.type === 'chatTypeChannel';
@ -811,6 +814,7 @@ export function buildLocalForwardedMessage({
noAuthors, noAuthors,
noCaptions, noCaptions,
isCurrentUserPremium, isCurrentUserPremium,
lastMessageId,
}: { }: {
toChat: ApiChat; toChat: ApiChat;
toThreadId?: number; toThreadId?: number;
@ -819,8 +823,9 @@ export function buildLocalForwardedMessage({
noAuthors?: boolean; noAuthors?: boolean;
noCaptions?: boolean; noCaptions?: boolean;
isCurrentUserPremium?: boolean; isCurrentUserPremium?: boolean;
lastMessageId?: number;
}): ApiMessage { }): ApiMessage {
const localId = getNextLocalMessageId(toChat?.lastMessage?.id); const localId = getNextLocalMessageId(lastMessageId);
const { const {
content, content,
chatId: fromChatId, chatId: fromChatId,
@ -874,11 +879,13 @@ export function buildLocalForwardedMessage({
// Forward info doesn't get added when users forwards his own messages, also when forwarding audio // Forward info doesn't get added when users forwards his own messages, also when forwarding audio
...(message.chatId !== currentUserId && !isAudio && !noAuthors && { ...(message.chatId !== currentUserId && !isAudio && !noAuthors && {
forwardInfo: { forwardInfo: {
date: message.date, date: message.forwardInfo?.date || message.date,
savedDate: message.date,
isChannelPost: false, isChannelPost: false,
fromChatId, fromChatId,
fromMessageId, fromMessageId,
senderUserId: senderId, fromId: senderId,
savedFromPeerId: message.chatId,
}, },
}), }),
...(message.chatId === currentUserId && !noAuthors && { forwardInfo: message.forwardInfo }), ...(message.chatId === currentUserId && !noAuthors && { forwardInfo: message.forwardInfo }),

View File

@ -37,6 +37,7 @@ import {
buildApiChatFolderFromSuggested, buildApiChatFolderFromSuggested,
buildApiChatFromDialog, buildApiChatFromDialog,
buildApiChatFromPreview, buildApiChatFromPreview,
buildApiChatFromSavedDialog,
buildApiChatlistExportedInvite, buildApiChatlistExportedInvite,
buildApiChatlistInvite, buildApiChatlistInvite,
buildApiChatReactions, buildApiChatReactions,
@ -84,6 +85,18 @@ type FullChatData = {
isForumAsMessages?: true; isForumAsMessages?: true;
}; };
type ChatListData = {
chatIds: string[];
chats: ApiChat[];
users: ApiUser[];
userStatusesById: Record<string, ApiUserStatus>;
draftsById: Record<string, ApiDraft>;
orderedPinnedIds: string[] | undefined;
totalChatCount: number;
messages: ApiMessage[];
lastMessageByChatId: Record<string, number>;
};
let onUpdate: OnApiUpdate; let onUpdate: OnApiUpdate;
export function init(_onUpdate: OnApiUpdate) { export function init(_onUpdate: OnApiUpdate) {
@ -95,19 +108,18 @@ export async function fetchChats({
offsetDate, offsetDate,
archived, archived,
withPinned, withPinned,
lastLocalServiceMessage, lastLocalServiceMessageId,
}: { }: {
limit: number; limit: number;
offsetDate?: number; offsetDate?: number;
archived?: boolean; archived?: boolean;
withPinned?: boolean; withPinned?: boolean;
lastLocalServiceMessage?: ApiMessage; lastLocalServiceMessageId?: number;
}) { }): Promise<ChatListData | undefined> {
const result = await invokeRequest(new GramJs.messages.GetDialogs({ const result = await invokeRequest(new GramJs.messages.GetDialogs({
offsetPeer: new GramJs.InputPeerEmpty(), offsetPeer: new GramJs.InputPeerEmpty(),
limit, limit,
offsetDate, offsetDate,
folderId: archived ? ARCHIVED_FOLDER_ID : undefined,
...(withPinned && { excludePinned: true }), ...(withPinned && { excludePinned: true }),
})); }));
const resultPinned = withPinned const resultPinned = withPinned
@ -125,12 +137,10 @@ export async function fetchChats({
} }
updateLocalDb(result); updateLocalDb(result);
const lastMessagesByChatId = buildCollectionByKey( const messages = (resultPinned ? resultPinned.messages : [])
(resultPinned ? resultPinned.messages : []).concat(result.messages) .concat(result.messages)
.map(buildApiMessage) .map(buildApiMessage)
.filter(Boolean), .filter(Boolean);
'chatId',
);
dispatchThreadInfoUpdates(result.messages); dispatchThreadInfoUpdates(result.messages);
@ -145,6 +155,7 @@ export async function fetchChats({
const dialogs = (resultPinned ? resultPinned.dialogs : []).concat(result.dialogs); const dialogs = (resultPinned ? resultPinned.dialogs : []).concat(result.dialogs);
const orderedPinnedIds: string[] = []; const orderedPinnedIds: string[] = [];
const lastMessageByChatId: Record<string, number> = {};
dialogs.forEach((dialog) => { dialogs.forEach((dialog) => {
if ( if (
@ -158,6 +169,7 @@ export async function fetchChats({
const peerEntity = peersByKey[getPeerKey(dialog.peer)]; const peerEntity = peersByKey[getPeerKey(dialog.peer)];
const chat = buildApiChatFromDialog(dialog, peerEntity); const chat = buildApiChatFromDialog(dialog, peerEntity);
lastMessageByChatId[chat.id] = dialog.topMessage;
if (dialog.pts) { if (dialog.pts) {
updateChannelState(chat.id, dialog.pts); updateChannelState(chat.id, dialog.pts);
@ -165,12 +177,10 @@ export async function fetchChats({
if ( if (
chat.id === SERVICE_NOTIFICATIONS_USER_ID chat.id === SERVICE_NOTIFICATIONS_USER_ID
&& lastLocalServiceMessage && lastLocalServiceMessageId
&& (!lastMessagesByChatId[chat.id] || lastLocalServiceMessage.date > lastMessagesByChatId[chat.id].date) && (lastLocalServiceMessageId > dialog.topMessage)
) { ) {
chat.lastMessage = lastLocalServiceMessage; lastMessageByChatId[chat.id] = lastLocalServiceMessageId;
} else {
chat.lastMessage = lastMessagesByChatId[chat.id];
} }
chat.isListed = true; chat.isListed = true;
@ -210,6 +220,96 @@ export async function fetchChats({
draftsById, draftsById,
orderedPinnedIds: withPinned ? orderedPinnedIds : undefined, orderedPinnedIds: withPinned ? orderedPinnedIds : undefined,
totalChatCount, totalChatCount,
lastMessageByChatId,
messages,
};
}
export async function fetchSavedChats({
limit,
offsetDate,
withPinned,
}: {
limit: number;
offsetDate?: number;
withPinned?: boolean;
}): Promise<ChatListData | undefined> {
const result = await invokeRequest(new GramJs.messages.GetSavedDialogs({
offsetPeer: new GramJs.InputPeerEmpty(),
limit,
offsetDate,
...(withPinned && { excludePinned: true }),
}));
const resultPinned = withPinned
? await invokeRequest(new GramJs.messages.GetPinnedSavedDialogs())
: undefined;
if (!result || result instanceof GramJs.messages.SavedDialogsNotModified) {
return undefined;
}
const hasPinned = resultPinned && !(resultPinned instanceof GramJs.messages.SavedDialogsNotModified);
if (hasPinned) {
updateLocalDb(resultPinned);
}
updateLocalDb(result);
dispatchThreadInfoUpdates(result.messages);
const messages = (hasPinned ? resultPinned.messages : [])
.concat(result.messages)
.map(buildApiMessage)
.filter(Boolean);
const peersByKey = preparePeers(result);
if (hasPinned) {
Object.assign(peersByKey, preparePeers(resultPinned, peersByKey));
}
const dialogs = (hasPinned ? resultPinned.dialogs : []).concat(result.dialogs);
const chatIds: string[] = [];
const orderedPinnedIds: string[] = [];
const lastMessageByChatId: Record<string, number> = {};
const chats: ApiChat[] = [];
dialogs.forEach((dialog) => {
const peerEntity = peersByKey[getPeerKey(dialog.peer)];
const chat = buildApiChatFromSavedDialog(dialog, peerEntity);
const chatId = getApiChatIdFromMtpPeer(dialog.peer);
chatIds.push(chatId);
if (withPinned && dialog.pinned) {
orderedPinnedIds.push(chatId);
}
lastMessageByChatId[chatId] = dialog.topMessage;
chats.push(chat);
});
const { users, userStatusesById } = buildApiUsersAndStatuses((hasPinned ? resultPinned.users : [])
.concat(result.users));
let totalChatCount: number;
if (result instanceof GramJs.messages.SavedDialogsSlice) {
totalChatCount = result.count;
} else {
totalChatCount = chatIds.length;
}
return {
chatIds,
chats,
users,
userStatusesById,
orderedPinnedIds: withPinned ? orderedPinnedIds : undefined,
totalChatCount,
lastMessageByChatId,
messages,
draftsById: {},
}; };
} }
@ -348,10 +448,7 @@ export async function requestChatUpdate({
? lastLocalMessage ? lastLocalMessage
: lastRemoteMessage; : lastRemoteMessage;
const chatUpdate = { const chatUpdate = buildApiChatFromDialog(dialog, peerEntity);
...buildApiChatFromDialog(dialog, peerEntity),
...(!noLastMessage && { lastMessage }),
};
onUpdate({ onUpdate({
'@type': 'updateChat', '@type': 'updateChat',
@ -359,6 +456,14 @@ export async function requestChatUpdate({
chat: chatUpdate, chat: chatUpdate,
}); });
if (!noLastMessage && lastMessage) {
onUpdate({
'@type': 'updateChatLastMessage',
id,
lastMessage,
});
}
applyState(result.state); applyState(result.state);
scheduleMutedChatUpdate(chatUpdate.id, chatUpdate.muteUntil, onUpdate); scheduleMutedChatUpdate(chatUpdate.id, chatUpdate.muteUntil, onUpdate);
@ -859,6 +964,30 @@ export async function toggleChatPinned({
} }
} }
export async function toggleSavedDialogPinned({
chat, shouldBePinned,
}: {
chat: ApiChat;
shouldBePinned: boolean;
}) {
const { id, accessHash } = chat;
const isActionSuccessful = await invokeRequest(new GramJs.messages.ToggleSavedDialogPin({
peer: new GramJs.InputDialogPeer({
peer: buildInputPeer(id, accessHash),
}),
pinned: shouldBePinned || undefined,
}));
if (isActionSuccessful) {
onUpdate({
'@type': 'updateSavedDialogPinned',
id: chat.id,
isPinned: shouldBePinned,
});
}
}
export function toggleChatArchived({ export function toggleChatArchived({
chat, folderId, chat, folderId,
}: { }: {
@ -1409,7 +1538,8 @@ export function toggleJoinRequest(chat: ApiChat, isEnabled: boolean) {
} }
function preparePeers( function preparePeers(
result: GramJs.messages.Dialogs | GramJs.messages.DialogsSlice | GramJs.messages.PeerDialogs, result: GramJs.messages.Dialogs | GramJs.messages.DialogsSlice | GramJs.messages.PeerDialogs |
GramJs.messages.SavedDialogs | GramJs.messages.SavedDialogsSlice,
currentStore?: Record<string, GramJs.TypeChat | GramJs.TypeUser>, currentStore?: Record<string, GramJs.TypeChat | GramJs.TypeUser>,
) { ) {
const store: Record<string, GramJs.TypeChat | GramJs.TypeUser> = {}; const store: Record<string, GramJs.TypeChat | GramJs.TypeUser> = {};
@ -1439,6 +1569,7 @@ function preparePeers(
function updateLocalDb(result: ( function updateLocalDb(result: (
GramJs.messages.Dialogs | GramJs.messages.DialogsSlice | GramJs.messages.PeerDialogs | GramJs.messages.Dialogs | GramJs.messages.DialogsSlice | GramJs.messages.PeerDialogs |
GramJs.messages.SavedDialogs | GramJs.messages.SavedDialogsSlice |
GramJs.messages.ChatFull | GramJs.contacts.Found | GramJs.messages.ChatFull | GramJs.contacts.Found |
GramJs.contacts.ResolvedPeer | GramJs.channels.ChannelParticipants | GramJs.contacts.ResolvedPeer | GramJs.channels.ChannelParticipants |
GramJs.messages.Chats | GramJs.messages.ChatsSlice | GramJs.TypeUpdates | GramJs.messages.ForumTopics GramJs.messages.Chats | GramJs.messages.ChatsSlice | GramJs.TypeUpdates | GramJs.messages.ForumTopics

View File

@ -6,6 +6,7 @@ import type { TwoFaParams } from '../../../lib/gramjs/client/2fa';
import TelegramClient from '../../../lib/gramjs/client/TelegramClient'; import TelegramClient from '../../../lib/gramjs/client/TelegramClient';
import { Logger as GramJsLogger } from '../../../lib/gramjs/extensions/index'; import { Logger as GramJsLogger } from '../../../lib/gramjs/extensions/index';
import type { ThreadId } from '../../../types';
import type { import type {
ApiInitialArgs, ApiInitialArgs,
ApiMediaFormat, ApiMediaFormat,
@ -222,7 +223,7 @@ type InvokeRequestParams = {
dcId?: number; dcId?: number;
shouldIgnoreErrors?: boolean; shouldIgnoreErrors?: boolean;
abortControllerChatId?: string; abortControllerChatId?: string;
abortControllerThreadId?: number; abortControllerThreadId?: ThreadId;
abortControllerGroup?: 'call'; abortControllerGroup?: 'call';
shouldRetryOnTimeout?: boolean; shouldRetryOnTimeout?: boolean;
}; };
@ -351,7 +352,7 @@ export function getTmpPassword(currentPassword: string, ttl?: number) {
return client.getTmpPassword(currentPassword, ttl); return client.getTmpPassword(currentPassword, ttl);
} }
export function abortChatRequests(params: { chatId: string; threadId?: number }) { export function abortChatRequests(params: { chatId: string; threadId?: ThreadId }) {
const { chatId, threadId } = params; const { chatId, threadId } = params;
const controller = CHAT_ABORT_CONTROLLERS.get(chatId); const controller = CHAT_ABORT_CONTROLLERS.get(chatId);
if (!threadId) { if (!threadId) {

View File

@ -24,7 +24,7 @@ export {
editTopic, toggleForum, fetchTopicById, createTopic, toggleParticipantsHidden, checkChatlistInvite, editTopic, toggleForum, fetchTopicById, createTopic, toggleParticipantsHidden, checkChatlistInvite,
joinChatlistInvite, createChalistInvite, editChatlistInvite, deleteChatlistInvite, fetchChatlistInvites, joinChatlistInvite, createChalistInvite, editChatlistInvite, deleteChatlistInvite, fetchChatlistInvites,
fetchLeaveChatlistSuggestions, leaveChatlist, togglePeerTranslations, setViewForumAsMessages, fetchLeaveChatlistSuggestions, leaveChatlist, togglePeerTranslations, setViewForumAsMessages,
fetchChannelRecommendations, fetchChannelRecommendations, fetchSavedChats, toggleSavedDialogPinned,
} from './chats'; } from './chats';
export { export {

View File

@ -1,5 +1,6 @@
import { Api as GramJs } from '../../../lib/gramjs'; import { Api as GramJs } from '../../../lib/gramjs';
import type { ThreadId } from '../../../types';
import type { import type {
ApiAttachment, ApiAttachment,
ApiChat, ApiChat,
@ -106,21 +107,25 @@ export async function fetchMessages({
chat, chat,
threadId, threadId,
offsetId, offsetId,
isSavedDialog,
...pagination ...pagination
}: { }: {
chat: ApiChat; chat: ApiChat;
threadId?: number; threadId?: ThreadId;
offsetId?: number; offsetId?: number;
isSavedDialog?: boolean;
addOffset?: number; addOffset?: number;
limit: number; limit: number;
}) { }) {
const RequestClass = threadId === MAIN_THREAD_ID ? GramJs.messages.GetHistory : GramJs.messages.GetReplies; const RequestClass = threadId === MAIN_THREAD_ID
? GramJs.messages.GetHistory : isSavedDialog
? GramJs.messages.GetSavedHistory : GramJs.messages.GetReplies;
let result; let result;
try { try {
result = await invokeRequest(new RequestClass({ result = await invokeRequest(new RequestClass({
peer: buildInputPeer(chat.id, chat.accessHash), peer: buildInputPeer(chat.id, chat.accessHash),
...(threadId !== MAIN_THREAD_ID && { ...(threadId !== MAIN_THREAD_ID && !isSavedDialog && {
msgId: Number(threadId), msgId: Number(threadId),
}), }),
...(offsetId && { ...(offsetId && {
@ -241,6 +246,7 @@ let mediaQueue = Promise.resolve();
export function sendMessage( export function sendMessage(
{ {
chat, chat,
lastMessageId,
text, text,
entities, entities,
replyInfo, replyInfo,
@ -281,6 +287,7 @@ export function sendMessage(
) { ) {
const localMessage = buildLocalMessage( const localMessage = buildLocalMessage(
chat, chat,
lastMessageId,
text, text,
entities, entities,
replyInfo, replyInfo,
@ -707,10 +714,10 @@ export async function pinMessage({
})); }));
} }
export async function unpinAllMessages({ chat, threadId }: { chat: ApiChat; threadId?: number }) { export async function unpinAllMessages({ chat, threadId }: { chat: ApiChat; threadId?: ThreadId }) {
await invokeRequest(new GramJs.messages.UnpinAllMessages({ await invokeRequest(new GramJs.messages.UnpinAllMessages({
peer: buildInputPeer(chat.id, chat.accessHash), peer: buildInputPeer(chat.id, chat.accessHash),
...(threadId && { topMsgId: threadId }), ...(threadId && { topMsgId: Number(threadId) }),
})); }));
} }
@ -806,7 +813,7 @@ export async function reportMessages({
export async function sendMessageAction({ export async function sendMessageAction({
peer, threadId, action, peer, threadId, action,
}: { }: {
peer: ApiPeer; threadId?: number; action: ApiSendMessageAction; peer: ApiPeer; threadId?: ThreadId; action: ApiSendMessageAction;
}) { }) {
const gramAction = buildSendMessageAction(action); const gramAction = buildSendMessageAction(action);
if (!gramAction) { if (!gramAction) {
@ -820,7 +827,7 @@ export async function sendMessageAction({
try { try {
const result = await invokeRequest(new GramJs.messages.SetTyping({ const result = await invokeRequest(new GramJs.messages.SetTyping({
peer: buildInputPeer(peer.id, peer.accessHash), peer: buildInputPeer(peer.id, peer.accessHash),
topMsgId: threadId, topMsgId: Number(threadId),
action: gramAction, action: gramAction,
}), { }), {
shouldThrow: true, shouldThrow: true,
@ -837,7 +844,7 @@ export async function sendMessageAction({
export async function markMessageListRead({ export async function markMessageListRead({
chat, threadId, maxId = 0, chat, threadId, maxId = 0,
}: { }: {
chat: ApiChat; threadId: number; maxId?: number; chat: ApiChat; threadId: ThreadId; maxId?: number;
}) { }) {
const isChannel = getEntityTypeById(chat.id) === 'channel'; const isChannel = getEntityTypeById(chat.id) === 'channel';
@ -851,7 +858,7 @@ export async function markMessageListRead({
} else if (isChannel) { } else if (isChannel) {
await invokeRequest(new GramJs.messages.ReadDiscussion({ await invokeRequest(new GramJs.messages.ReadDiscussion({
peer: buildInputPeer(chat.id, chat.accessHash), peer: buildInputPeer(chat.id, chat.accessHash),
msgId: threadId, msgId: Number(threadId),
readMaxId: fixedMaxId, readMaxId: fixedMaxId,
})); }));
} else { } else {
@ -999,12 +1006,13 @@ export async function fetchDiscussionMessage({
} }
export async function searchMessagesLocal({ export async function searchMessagesLocal({
chat, type, query, threadId, minDate, maxDate, ...pagination chat, isSavedDialog, type, query, threadId, minDate, maxDate, ...pagination
}: { }: {
chat: ApiChat; chat: ApiChat;
isSavedDialog?: boolean;
type?: ApiMessageSearchType | ApiGlobalMessageSearchType; type?: ApiMessageSearchType | ApiGlobalMessageSearchType;
query?: string; query?: string;
threadId?: number; threadId?: ThreadId;
offsetId?: number; offsetId?: number;
addOffset?: number; addOffset?: number;
limit: number; limit: number;
@ -1037,9 +1045,12 @@ export async function searchMessagesLocal({
} }
} }
const peer = buildInputPeer(chat.id, chat.accessHash);
const result = await invokeRequest(new GramJs.messages.Search({ const result = await invokeRequest(new GramJs.messages.Search({
peer: buildInputPeer(chat.id, chat.accessHash), peer: isSavedDialog ? new GramJs.InputPeerSelf() : peer,
topMsgId: threadId === MAIN_THREAD_ID ? undefined : threadId, savedPeerId: isSavedDialog ? peer : undefined,
topMsgId: threadId !== MAIN_THREAD_ID && !isSavedDialog ? Number(threadId) : undefined,
filter, filter,
q: query || '', q: query || '',
minDate, minDate,
@ -1288,10 +1299,11 @@ export async function forwardMessages({
noCaptions, noCaptions,
isCurrentUserPremium, isCurrentUserPremium,
wasDrafted, wasDrafted,
lastMessageId,
}: { }: {
fromChat: ApiChat; fromChat: ApiChat;
toChat: ApiChat; toChat: ApiChat;
toThreadId?: number; toThreadId?: ThreadId;
messages: ApiMessage[]; messages: ApiMessage[];
isSilent?: boolean; isSilent?: boolean;
scheduledAt?: number; scheduledAt?: number;
@ -1301,6 +1313,7 @@ export async function forwardMessages({
noCaptions?: boolean; noCaptions?: boolean;
isCurrentUserPremium?: boolean; isCurrentUserPremium?: boolean;
wasDrafted?: boolean; wasDrafted?: boolean;
lastMessageId?: number;
}) { }) {
const messageIds = messages.map(({ id }) => id); const messageIds = messages.map(({ id }) => id);
const randomIds = messages.map(generateRandomBigInt); const randomIds = messages.map(generateRandomBigInt);
@ -1309,12 +1322,13 @@ export async function forwardMessages({
messages.forEach((message, index) => { messages.forEach((message, index) => {
const localMessage = buildLocalForwardedMessage({ const localMessage = buildLocalForwardedMessage({
toChat, toChat,
toThreadId, toThreadId: Number(toThreadId),
message, message,
scheduledAt, scheduledAt,
noAuthors, noAuthors,
noCaptions, noCaptions,
isCurrentUserPremium, isCurrentUserPremium,
lastMessageId,
}); });
localMessages[randomIds[index].toString()] = localMessage; localMessages[randomIds[index].toString()] = localMessage;
@ -1337,7 +1351,7 @@ export async function forwardMessages({
silent: isSilent || undefined, silent: isSilent || undefined,
dropAuthor: noAuthors || undefined, dropAuthor: noAuthors || undefined,
dropMediaCaptions: noCaptions || undefined, dropMediaCaptions: noCaptions || undefined,
...(toThreadId && { topMsgId: toThreadId }), ...(toThreadId && { topMsgId: Number(toThreadId) }),
...(scheduledAt && { scheduleDate: scheduledAt }), ...(scheduledAt && { scheduleDate: scheduledAt }),
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }), ...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
}), { }), {
@ -1434,14 +1448,14 @@ function updateLocalDb(result: (
}); });
} }
export async function fetchPinnedMessages({ chat, threadId }: { chat: ApiChat; threadId: number }) { export async function fetchPinnedMessages({ chat, threadId }: { chat: ApiChat; threadId: ThreadId }) {
const result = await invokeRequest(new GramJs.messages.Search( const result = await invokeRequest(new GramJs.messages.Search(
{ {
peer: buildInputPeer(chat.id, chat.accessHash), peer: buildInputPeer(chat.id, chat.accessHash),
filter: new GramJs.InputMessagesFilterPinned(), filter: new GramJs.InputMessagesFilterPinned(),
q: '', q: '',
limit: PINNED_MESSAGES_LIMIT, limit: PINNED_MESSAGES_LIMIT,
topMsgId: threadId, topMsgId: Number(threadId),
}, },
), { ), {
abortControllerChatId: chat.id, abortControllerChatId: chat.id,

View File

@ -267,6 +267,8 @@ export async function fetchProfilePhotos(user?: ApiUser, chat?: ApiChat) {
}; };
} }
if (chat?.isRestricted) return undefined;
const result = await searchMessagesLocal({ const result = await searchMessagesLocal({
chat: chat!, chat: chat!,
type: 'profilePhoto', type: 'profilePhoto',

View File

@ -597,6 +597,26 @@ export function updater(update: Update) {
ids, ids,
folderId: update.folderId || undefined, folderId: update.folderId || undefined,
}); });
} else if (
update instanceof GramJs.UpdateSavedDialogPinned
&& update.peer instanceof GramJs.DialogPeer
) {
onUpdate({
'@type': 'updateSavedDialogPinned',
id: getApiChatIdFromMtpPeer(update.peer.peer),
isPinned: update.pinned || false,
});
} else if (update instanceof GramJs.UpdatePinnedSavedDialogs) {
const ids = update.order
? update.order
.filter((dp): dp is GramJs.DialogPeer => dp instanceof GramJs.DialogPeer)
.map((dp) => getApiChatIdFromMtpPeer(dp.peer))
: [];
onUpdate({
'@type': 'updatePinnedSavedDialogIds',
ids,
});
} else if (update instanceof GramJs.UpdateFolderPeers) { } else if (update instanceof GramJs.UpdateFolderPeers) {
update.folderPeers.forEach((folderPeer) => { update.folderPeers.forEach((folderPeer) => {
const { folderId, peer } = folderPeer; const { folderId, peer } = folderPeer;

View File

@ -1,6 +1,7 @@
import type { ThreadId } from '../../types';
import type { ApiBotCommand } from './bots'; import type { ApiBotCommand } from './bots';
import type { import type {
ApiChatReactions, ApiMessage, ApiPhoto, ApiStickerSet, ApiChatReactions, ApiPhoto, ApiStickerSet,
} from './messages'; } from './messages';
import type { ApiChatInviteImporter } from './misc'; import type { ApiChatInviteImporter } from './misc';
import type { import type {
@ -21,7 +22,6 @@ export interface ApiChat {
type: ApiChatType; type: ApiChatType;
title: string; title: string;
hasUnreadMark?: boolean; hasUnreadMark?: boolean;
lastMessage?: ApiMessage;
lastReadOutboxMessageId?: number; lastReadOutboxMessageId?: number;
lastReadInboxMessageId?: number; lastReadInboxMessageId?: number;
unreadCount?: number; unreadCount?: number;
@ -48,7 +48,7 @@ export interface ApiChat {
emojiStatus?: ApiEmojiStatus; emojiStatus?: ApiEmojiStatus;
isForum?: boolean; isForum?: boolean;
isForumAsMessages?: true; isForumAsMessages?: true;
topics?: Record<number, ApiTopic>; topics?: Record<ThreadId, ApiTopic>;
listedTopicIds?: number[]; listedTopicIds?: number[];
topicsCount?: number; topicsCount?: number;
orderedPinnedTopicIds?: number[]; orderedPinnedTopicIds?: number[];

View File

@ -1,3 +1,4 @@
import type { ThreadId } from '../../types';
import type { ApiWebDocument } from './bots'; import type { ApiWebDocument } from './bots';
import type { ApiGroupCall, PhoneCallAction } from './calls'; import type { ApiGroupCall, PhoneCallAction } from './calls';
import type { ApiChat } from './chats'; import type { ApiChat } from './chats';
@ -370,12 +371,14 @@ export type ApiInputReplyInfo = ApiInputMessageReplyInfo | ApiInputStoryReplyInf
export interface ApiMessageForwardInfo { export interface ApiMessageForwardInfo {
date: number; date: number;
savedDate?: number;
isImported?: boolean; isImported?: boolean;
isChannelPost: boolean; isChannelPost: boolean;
channelPostId?: number; channelPostId?: number;
isLinkedChannelPost?: boolean; isLinkedChannelPost?: boolean;
fromChatId?: string; fromChatId?: string;
senderUserId?: string; fromId?: string;
savedFromPeerId?: string;
fromMessageId?: number; fromMessageId?: number;
hiddenUserName?: string; hiddenUserName?: string;
postAuthorTitle?: string; postAuthorTitle?: string;
@ -595,14 +598,14 @@ interface ApiBaseThreadInfo {
export interface ApiCommentsInfo extends ApiBaseThreadInfo { export interface ApiCommentsInfo extends ApiBaseThreadInfo {
isCommentsInfo: true; isCommentsInfo: true;
threadId?: number; threadId?: ThreadId;
originChannelId: string; originChannelId: string;
originMessageId: number; originMessageId: number;
} }
export interface ApiMessageThreadInfo extends ApiBaseThreadInfo { export interface ApiMessageThreadInfo extends ApiBaseThreadInfo {
isCommentsInfo: false; isCommentsInfo: false;
threadId: number; threadId: ThreadId;
// For linked messages in discussion // For linked messages in discussion
fromChannelId?: string; fromChannelId?: string;
fromMessageId?: number; fromMessageId?: number;

View File

@ -6,7 +6,7 @@ import type {
VideoRotation, VideoRotation,
VideoState, VideoState,
} from '../../lib/secret-sauce'; } from '../../lib/secret-sauce';
import type { ApiPrivacyKey, PrivacyVisibility } from '../../types'; import type { ApiPrivacyKey, PrivacyVisibility, ThreadId } from '../../types';
import type { ApiBotMenuButton } from './bots'; import type { ApiBotMenuButton } from './bots';
import type { import type {
ApiGroupCall, ApiPhoneCall, ApiGroupCall, ApiPhoneCall,
@ -102,6 +102,12 @@ export type ApiUpdateChat = {
noTopChatsRequest?: boolean; noTopChatsRequest?: boolean;
}; };
export type ApiUpdateChatLastMessage = {
'@type': 'updateChatLastMessage';
id: string;
lastMessage: ApiMessage;
};
export type ApiUpdateChatJoin = { export type ApiUpdateChatJoin = {
'@type': 'updateChatJoin'; '@type': 'updateChatJoin';
id: string; id: string;
@ -126,7 +132,7 @@ export type ApiUpdateChatInbox = {
export type ApiUpdateChatTypingStatus = { export type ApiUpdateChatTypingStatus = {
'@type': 'updateChatTypingStatus'; '@type': 'updateChatTypingStatus';
id: string; id: string;
threadId?: number; threadId?: ThreadId;
typingStatus: ApiTypingStatus | undefined; typingStatus: ApiTypingStatus | undefined;
}; };
@ -170,6 +176,17 @@ export type ApiUpdateChatPinned = {
isPinned: boolean; isPinned: boolean;
}; };
export type ApiUpdatePinnedSavedDialogIds = {
'@type': 'updatePinnedSavedDialogIds';
ids: string[];
};
export type ApiUpdateSavedDialogPinned = {
'@type': 'updateSavedDialogPinned';
id: string;
isPinned: boolean;
};
export type ApiUpdateChatFolder = { export type ApiUpdateChatFolder = {
'@type': 'updateChatFolder'; '@type': 'updateChatFolder';
id: number; id: number;
@ -312,7 +329,7 @@ export type ApiUpdateResetMessages = {
export type ApiUpdateDraftMessage = { export type ApiUpdateDraftMessage = {
'@type': 'draftMessage'; '@type': 'draftMessage';
chatId: string; chatId: string;
threadId?: number; threadId?: ThreadId;
draft?: ApiDraft; draft?: ApiDraft;
}; };
@ -713,7 +730,7 @@ export type ApiUpdate = (
ApiUpdateRecentReactions | ApiUpdateStory | ApiUpdateReadStories | ApiUpdateDeleteStory | ApiUpdateSentStoryReaction | ApiUpdateRecentReactions | ApiUpdateStory | ApiUpdateReadStories | ApiUpdateDeleteStory | ApiUpdateSentStoryReaction |
ApiRequestReconnectApi | ApiRequestSync | ApiUpdateFetchingDifference | ApiUpdateChannelMessages | ApiRequestReconnectApi | ApiRequestSync | ApiUpdateFetchingDifference | ApiUpdateChannelMessages |
ApiUpdateStealthMode | ApiUpdateAttachMenuBots | ApiUpdateNewAuthorization | ApiUpdateGroupInvitePrivacyForbidden | ApiUpdateStealthMode | ApiUpdateAttachMenuBots | ApiUpdateNewAuthorization | ApiUpdateGroupInvitePrivacyForbidden |
ApiUpdateViewForumAsMessages ApiUpdateViewForumAsMessages | ApiUpdateSavedDialogPinned | ApiUpdatePinnedSavedDialogIds | ApiUpdateChatLastMessage
); );
export type OnApiUpdate = (update: ApiUpdate) => void; export type OnApiUpdate = (update: ApiUpdate) => void;

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none"><path fill="#fff" fill-rule="evenodd" d="m27.42 15.209 2.462 3.17a.559.559 0 0 1-.252.871c-.847.292-1.635.669-2.366 1.13-.733.463-1.347.973-1.841 1.53a.563.563 0 0 0 .091.829l.811.576a.558.558 0 0 1-.152.987 35.953 35.953 0 0 0-6.33 2.636c-1.726.923-3.261 1.905-4.606 2.948a.536.536 0 0 1-.51.083.559.559 0 0 1-.335-.708l.071-.202a15.37 15.37 0 0 1 3.017-5.077c1.782-2.005 3.998-3.508 6.648-4.511v-3.139c0-.145.108-.266.25-.278a5.583 5.583 0 0 0 2.682-.907.269.269 0 0 1 .36.062zm-22.102-.103 2.006.626c.446 2.05 1.18 3.841 2.203 5.374.947 1.419 2.268 2.805 3.96 4.159.23.183.278.518.11.76l-.556.793a.539.539 0 0 1-.686.174L5.836 23.69a.563.563 0 0 1-.065-.957l.592-.411a.562.562 0 0 0-.02-.932l-4.085-2.58a.564.564 0 0 1-.101-.863l2.614-2.7a.538.538 0 0 1 .547-.14zm17.107.975c.17.292.265.617.265.96 0 1.272-1.309 2.303-2.923 2.303-1.615 0-2.924-1.031-2.924-2.303 0-.171.024-.338.069-.498a42.01 42.01 0 0 0 5.415-.447zm-12.858.02c1.673.253 3.543.41 5.525.444l.014.054c.035.143.054.29.054.442 0 1.272-1.31 2.303-2.924 2.303-1.614 0-2.923-1.031-2.923-2.303 0-.335.09-.653.254-.94zM19.395 2c1.547 0 2.543 2.451 2.99 7.354 2.903.587 4.804 1.565 4.804 2.673 0 1.796-4.997 3.252-11.161 3.252-6.165 0-11.162-1.456-11.162-3.252 0-1.108 1.903-2.087 4.809-2.674C10.215 4.45 11.218 2 12.685 2c2.237 0 1.38 2.822 3.268 2.822S17.038 2 19.395 2Z" clip-rule="evenodd" style="fill:#000;stroke-width:.805101"/></svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none"><path fill="#fff" fill-rule="evenodd" d="M2.316 10.572c-.303-2.153-.454-3.229-.15-4.11a3.882 3.882 0 0 1 1.443-1.916c.763-.534 1.84-.685 3.992-.988l8.84-1.242c2.152-.303 3.228-.454 4.109-.15a3.881 3.881 0 0 1 1.915 1.443c.515.735.674 1.76.955 3.755h-8.61c-1.04 0-1.9 0-2.6.057-.727.06-1.395.187-2.022.507a5.172 5.172 0 0 0-2.26 2.26c-.32.627-.447 1.295-.507 2.022-.057.7-.057 1.56-.057 2.601v9.033c0 .416 0 .803.004 1.162a3.185 3.185 0 0 1-.906-.16 3.881 3.881 0 0 1-1.916-1.443c-.534-.763-.685-1.84-.988-3.992Zm6.339 4.292c0-2.173 0-3.26.423-4.09a3.881 3.881 0 0 1 1.696-1.696c.83-.423 1.917-.423 4.09-.423h8.926c2.174 0 3.26 0 4.09.423.731.372 1.325.965 1.697 1.696.423.83.423 1.917.423 4.09v8.926c0 2.174 0 3.26-.423 4.091a3.881 3.881 0 0 1-1.696 1.696c-.83.423-1.917.423-4.09.423h-8.927c-2.173 0-3.26 0-4.09-.423a3.881 3.881 0 0 1-1.696-1.696c-.423-.83-.423-1.917-.423-4.09Zm4.851-.388a.97.97 0 0 1 .97-.97h9.703a.97.97 0 0 1 0 1.94h-9.703a.97.97 0 0 1-.97-.97zm0 4.851a.97.97 0 0 1 .97-.97h9.703a.97.97 0 0 1 0 1.94h-9.703a.97.97 0 0 1-.97-.97zm.97 3.881a.97.97 0 1 0 0 1.94h6.792a.97.97 0 0 0 0-1.94z" clip-rule="evenodd" style="fill:#000;stroke-width:.90556"/></svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,6 +1,6 @@
import type { MouseEvent as ReactMouseEvent } from 'react'; import type { MouseEvent as ReactMouseEvent } from 'react';
import type { FC, TeactNode } from '../../lib/teact/teact'; import type { FC, TeactNode } from '../../lib/teact/teact';
import React, { memo, useRef } from '../../lib/teact/teact'; import React, { memo, useMemo, useRef } from '../../lib/teact/teact';
import { getActions } from '../../global'; import { getActions } from '../../global';
import type { import type {
@ -16,6 +16,7 @@ import {
getChatTitle, getChatTitle,
getPeerStoryHtmlId, getPeerStoryHtmlId,
getUserFullName, getUserFullName,
isAnonymousForwardsChat,
isChatWithRepliesBot, isChatWithRepliesBot,
isDeletedUser, isDeletedUser,
isUserId, isUserId,
@ -33,6 +34,7 @@ import useMediaTransition from '../../hooks/useMediaTransition';
import OptimizedVideo from '../ui/OptimizedVideo'; import OptimizedVideo from '../ui/OptimizedVideo';
import AvatarStoryCircle from './AvatarStoryCircle'; import AvatarStoryCircle from './AvatarStoryCircle';
import Icon from './Icon';
import './Avatar.scss'; import './Avatar.scss';
@ -51,6 +53,7 @@ type OwnProps = {
photo?: ApiPhoto; photo?: ApiPhoto;
text?: string; text?: string;
isSavedMessages?: boolean; isSavedMessages?: boolean;
isSavedDialog?: boolean;
withVideo?: boolean; withVideo?: boolean;
withStory?: boolean; withStory?: boolean;
forPremiumPromo?: boolean; forPremiumPromo?: boolean;
@ -72,6 +75,7 @@ const Avatar: FC<OwnProps> = ({
photo, photo,
text, text,
isSavedMessages, isSavedMessages,
isSavedDialog,
withVideo, withVideo,
withStory, withStory,
forPremiumPromo, forPremiumPromo,
@ -94,6 +98,7 @@ const Avatar: FC<OwnProps> = ({
const chat = peer && isPeerChat ? peer as ApiChat : undefined; const chat = peer && isPeerChat ? peer as ApiChat : undefined;
const isDeleted = user && isDeletedUser(user); const isDeleted = user && isDeletedUser(user);
const isReplies = peer && isChatWithRepliesBot(peer.id); const isReplies = peer && isChatWithRepliesBot(peer.id);
const isAnonymousForwards = peer && isAnonymousForwardsChat(peer.id);
const isForum = chat?.isForum; const isForum = chat?.isForum;
let imageHash: string | undefined; let imageHash: string | undefined;
let videoHash: string | undefined; let videoHash: string | undefined;
@ -112,6 +117,26 @@ const Avatar: FC<OwnProps> = ({
} }
} }
const specialIcon = useMemo(() => {
if (isSavedMessages) {
return isSavedDialog ? 'my-notes' : 'avatar-saved-messages';
}
if (isDeleted) {
return 'avatar-deleted-account';
}
if (isReplies) {
return 'reply-filled';
}
if (isAnonymousForwards) {
return 'author-hidden';
}
return undefined;
}, [isAnonymousForwards, isDeleted, isSavedDialog, isReplies, isSavedMessages]);
const imgBlobUrl = useMedia(imageHash, false, ApiMediaFormat.BlobUrl); const imgBlobUrl = useMedia(imageHash, false, ApiMediaFormat.BlobUrl);
const videoBlobUrl = useMedia(videoHash, !shouldLoadVideo, ApiMediaFormat.BlobUrl); const videoBlobUrl = useMedia(videoHash, !shouldLoadVideo, ApiMediaFormat.BlobUrl);
const hasBlobUrl = Boolean(imgBlobUrl || videoBlobUrl); const hasBlobUrl = Boolean(imgBlobUrl || videoBlobUrl);
@ -137,40 +162,13 @@ const Avatar: FC<OwnProps> = ({
let content: TeactNode | undefined; let content: TeactNode | undefined;
const author = user ? getUserFullName(user) : (chat ? getChatTitle(lang, chat) : text); const author = user ? getUserFullName(user) : (chat ? getChatTitle(lang, chat) : text);
if (isSavedMessages) { if (specialIcon) {
content = ( content = (
<i <Icon
className={buildClassName( name={specialIcon}
cn.icon, className={cn.icon}
'icon',
'icon-avatar-saved-messages',
)}
role="img" role="img"
aria-label={author} ariaLabel={author}
/>
);
} else if (isDeleted) {
content = (
<i
className={buildClassName(
cn.icon,
'icon',
'icon-avatar-deleted-account',
)}
role="img"
aria-label={author}
/>
);
} else if (isReplies) {
content = (
<i
className={buildClassName(
cn.icon,
'icon',
'icon-reply-filled',
)}
role="img"
aria-label={author}
/> />
); );
} else if (hasBlobUrl) { } else if (hasBlobUrl) {
@ -214,6 +212,7 @@ const Avatar: FC<OwnProps> = ({
className, className,
getPeerColorClass(peer), getPeerColorClass(peer),
isSavedMessages && 'saved-messages', isSavedMessages && 'saved-messages',
isAnonymousForwards && 'anonymous-forwards',
isDeleted && 'deleted-account', isDeleted && 'deleted-account',
isReplies && 'replies-bot-account', isReplies && 'replies-bot-account',
isForum && 'forum', isForum && 'forum',

View File

@ -43,6 +43,7 @@ import Switcher from '../ui/Switcher';
type OwnProps = { type OwnProps = {
chatOrUserId: string; chatOrUserId: string;
isSavedDialog?: boolean;
forceShowSelf?: boolean; forceShowSelf?: boolean;
}; };
@ -57,11 +58,13 @@ type StateProps =
description?: string; description?: string;
chatInviteLink?: string; chatInviteLink?: string;
topicLink?: string; topicLink?: string;
hasSavedMessages?: boolean;
}; };
const runDebounced = debounce((cb) => cb(), 500, false); const runDebounced = debounce((cb) => cb(), 500, false);
const ChatExtra: FC<OwnProps & StateProps> = ({ const ChatExtra: FC<OwnProps & StateProps> = ({
chatOrUserId,
user, user,
chat, chat,
forceShowSelf, forceShowSelf,
@ -72,6 +75,7 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
description, description,
chatInviteLink, chatInviteLink,
topicLink, topicLink,
hasSavedMessages,
}) => { }) => {
const { const {
loadFullUser, loadFullUser,
@ -79,6 +83,7 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
updateChatMutedState, updateChatMutedState,
updateTopicMutedState, updateTopicMutedState,
loadPeerStories, loadPeerStories,
openSavedDialog,
} = getActions(); } = getActions();
const { const {
@ -151,6 +156,10 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
}); });
}); });
const handleOpenSavedDialog = useLastCallback(() => {
openSavedDialog({ chatId: chatOrUserId });
});
if (!chat || chat.isRestricted || (isSelf && !forceShowSelf)) { if (!chat || chat.isRestricted || (isSelf && !forceShowSelf)) {
return undefined; return undefined;
} }
@ -264,12 +273,17 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
/> />
</ListItem> </ListItem>
)} )}
{hasSavedMessages && (
<ListItem icon="saved-messages" ripple onClick={handleOpenSavedDialog}>
<span>{lang('SavedMessagesTab')}</span>
</ListItem>
)}
</div> </div>
); );
}; };
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global, { chatOrUserId }): StateProps => { (global, { chatOrUserId, isSavedDialog }): StateProps => {
const { countryList: { phoneCodes: phoneCodeList } } = global; const { countryList: { phoneCodes: phoneCodeList } } = global;
const chat = chatOrUserId ? selectChat(global, chatOrUserId) : undefined; const chat = chatOrUserId ? selectChat(global, chatOrUserId) : undefined;
@ -277,7 +291,7 @@ export default memo(withGlobal<OwnProps>(
const isForum = chat?.isForum; const isForum = chat?.isForum;
const isMuted = chat && selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global)); const isMuted = chat && selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global));
const { threadId } = selectCurrentMessageList(global) || {}; const { threadId } = selectCurrentMessageList(global) || {};
const topicId = isForum ? threadId : undefined; const topicId = isForum ? Number(threadId) : undefined;
const chatInviteLink = chat ? selectChatFullInfo(global, chat.id)?.inviteLink : undefined; const chatInviteLink = chat ? selectChatFullInfo(global, chat.id)?.inviteLink : undefined;
let description = user ? selectUserFullInfo(global, user.id)?.bio : undefined; let description = user ? selectUserFullInfo(global, user.id)?.bio : undefined;
if (!description && chat) { if (!description && chat) {
@ -291,6 +305,8 @@ export default memo(withGlobal<OwnProps>(
const topicLink = topicId ? selectTopicLink(global, chatOrUserId, topicId) : undefined; const topicLink = topicId ? selectTopicLink(global, chatOrUserId, topicId) : undefined;
const hasSavedMessages = !isSavedDialog && global.chats.listIds.saved?.includes(chatOrUserId);
return { return {
phoneCodeList, phoneCodeList,
chat, chat,
@ -301,6 +317,7 @@ export default memo(withGlobal<OwnProps>(
chatInviteLink, chatInviteLink,
description, description,
topicLink, topicLink,
hasSavedMessages,
}; };
}, },
)(ChatExtra)); )(ChatExtra));

View File

@ -5,6 +5,7 @@ import React, {
import { getActions } from '../../global'; import { getActions } from '../../global';
import type { ApiChat, ApiTopic } from '../../api/types'; import type { ApiChat, ApiTopic } from '../../api/types';
import type { ThreadId } from '../../types';
import { CHAT_HEIGHT_PX } from '../../config'; import { CHAT_HEIGHT_PX } from '../../config';
import { getCanPostInChat, isUserId } from '../../global/helpers'; import { getCanPostInChat, isUserId } from '../../global/helpers';
@ -41,7 +42,7 @@ export type OwnProps = {
className?: string; className?: string;
loadMore?: NoneToVoidFunction; loadMore?: NoneToVoidFunction;
onSearchChange: (search: string) => void; onSearchChange: (search: string) => void;
onSelectChatOrUser: (chatOrUserId: string, threadId?: number) => void; onSelectChatOrUser: (chatOrUserId: string, threadId?: ThreadId) => void;
onClose: NoneToVoidFunction; onClose: NoneToVoidFunction;
onCloseAnimationEnd?: NoneToVoidFunction; onCloseAnimationEnd?: NoneToVoidFunction;
}; };

View File

@ -30,7 +30,9 @@ import type {
ApiDraft, GlobalState, MessageList, ApiDraft, GlobalState, MessageList,
MessageListType, TabState, MessageListType, TabState,
} from '../../global/types'; } from '../../global/types';
import type { IAnchorPosition, InlineBotSettings, ISettings } from '../../types'; import type {
IAnchorPosition, InlineBotSettings, ISettings, ThreadId,
} from '../../types';
import { MAIN_THREAD_ID } from '../../api/types'; import { MAIN_THREAD_ID } from '../../api/types';
import { import {
@ -164,7 +166,7 @@ type ComposerType = 'messageList' | 'story';
type OwnProps = { type OwnProps = {
type: ComposerType; type: ComposerType;
chatId: string; chatId: string;
threadId: number; threadId: ThreadId;
storyId?: number; storyId?: number;
messageListType: MessageListType; messageListType: MessageListType;
dropAreaState?: string; dropAreaState?: string;

View File

@ -1,12 +1,14 @@
import type { FC } from '../../lib/teact/teact'; import type { FC } from '../../lib/teact/teact';
import React, { memo } from '../../lib/teact/teact'; import React, { memo, useMemo } from '../../lib/teact/teact';
import { getActions } from '../../global'; import { getActions } from '../../global';
import type { ApiChat, ApiPeer, ApiUser } from '../../api/types'; import type { ApiChat, ApiPeer, ApiUser } from '../../api/types';
import type { ObserveFn } from '../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../hooks/useIntersectionObserver';
import { EMOJI_STATUS_LOOP_LIMIT } from '../../config'; import { EMOJI_STATUS_LOOP_LIMIT } from '../../config';
import { getChatTitle, getUserFullName, isUserId } from '../../global/helpers'; import {
getChatTitle, getUserFullName, isAnonymousForwardsChat, isChatWithRepliesBot, isUserId,
} from '../../global/helpers';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import { copyTextToClipboard } from '../../util/clipboard'; import { copyTextToClipboard } from '../../util/clipboard';
import stopEvent from '../../util/stopEvent'; import stopEvent from '../../util/stopEvent';
@ -30,6 +32,7 @@ type OwnProps = {
withEmojiStatus?: boolean; withEmojiStatus?: boolean;
emojiStatusSize?: number; emojiStatusSize?: number;
isSavedMessages?: boolean; isSavedMessages?: boolean;
isSavedDialog?: boolean;
noLoopLimit?: boolean; noLoopLimit?: boolean;
canCopyTitle?: boolean; canCopyTitle?: boolean;
onEmojiStatusClick?: NoneToVoidFunction; onEmojiStatusClick?: NoneToVoidFunction;
@ -44,6 +47,7 @@ const FullNameTitle: FC<OwnProps> = ({
withEmojiStatus, withEmojiStatus,
emojiStatusSize, emojiStatusSize,
isSavedMessages, isSavedMessages,
isSavedDialog,
noLoopLimit, noLoopLimit,
canCopyTitle, canCopyTitle,
onEmojiStatusClick, onEmojiStatusClick,
@ -65,10 +69,26 @@ const FullNameTitle: FC<OwnProps> = ({
showNotification({ message: `${isUser ? 'User' : 'Chat'} name was copied` }); showNotification({ message: `${isUser ? 'User' : 'Chat'} name was copied` });
}); });
if (isSavedMessages) { const specialTitle = useMemo(() => {
if (isSavedMessages) {
return lang(isSavedDialog ? 'MyNotes' : 'SavedMessages');
}
if (isAnonymousForwardsChat(peer.id)) {
return lang('AnonymousForward');
}
if (isChatWithRepliesBot(peer.id)) {
return lang('RepliesTitle');
}
return undefined;
}, [isSavedDialog, isSavedMessages, lang, peer.id]);
if (specialTitle) {
return ( return (
<div className={buildClassName('title', styles.root, className)}> <div className={buildClassName('title', styles.root, className)}>
<h3>{lang('SavedMessages')}</h3> <h3>{specialTitle}</h3>
</div> </div>
); );
} }

View File

@ -3,11 +3,11 @@ import React, { memo, useEffect, useMemo } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global'; import { getActions, withGlobal } from '../../global';
import type { import type {
ApiChat, ApiThreadInfo, ApiTopic, ApiTypingStatus, ApiChat, ApiThreadInfo, ApiTopic, ApiTypingStatus, ApiUser,
} from '../../api/types'; } from '../../api/types';
import type { LangFn } from '../../hooks/useLang'; import type { LangFn } from '../../hooks/useLang';
import type { IconName } from '../../types/icons'; import type { IconName } from '../../types/icons';
import { MediaViewerOrigin, type StoryViewerOrigin } from '../../types'; import { MediaViewerOrigin, type StoryViewerOrigin, type ThreadId } from '../../types';
import { import {
getChatTypeString, getChatTypeString,
@ -20,6 +20,7 @@ import {
selectChatOnlineCount, selectChatOnlineCount,
selectThreadInfo, selectThreadInfo,
selectThreadMessagesCount, selectThreadMessagesCount,
selectUser,
} from '../../global/selectors'; } from '../../global/selectors';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import { REM } from './helpers/mediaDimensions'; import { REM } from './helpers/mediaDimensions';
@ -39,7 +40,7 @@ const TOPIC_ICON_SIZE = 2.5 * REM;
type OwnProps = { type OwnProps = {
chatId: string; chatId: string;
threadId?: number; threadId?: ThreadId;
className?: string; className?: string;
statusIcon?: IconName; statusIcon?: IconName;
typingStatus?: ApiTypingStatus; typingStatus?: ApiTypingStatus;
@ -58,6 +59,7 @@ type OwnProps = {
noStatusOrTyping?: boolean; noStatusOrTyping?: boolean;
withStory?: boolean; withStory?: boolean;
storyViewerOrigin?: StoryViewerOrigin; storyViewerOrigin?: StoryViewerOrigin;
isSavedDialog?: boolean;
onClick?: VoidFunction; onClick?: VoidFunction;
onEmojiStatusClick?: NoneToVoidFunction; onEmojiStatusClick?: NoneToVoidFunction;
}; };
@ -70,6 +72,7 @@ type StateProps =
onlineCount?: number; onlineCount?: number;
areMessagesLoaded: boolean; areMessagesLoaded: boolean;
messagesCount?: number; messagesCount?: number;
self?: ApiUser;
}; };
const GroupChatInfo: FC<OwnProps & StateProps> = ({ const GroupChatInfo: FC<OwnProps & StateProps> = ({
@ -97,6 +100,8 @@ const GroupChatInfo: FC<OwnProps & StateProps> = ({
storyViewerOrigin, storyViewerOrigin,
noEmojiStatus, noEmojiStatus,
emojiStatusSize, emojiStatusSize,
isSavedDialog,
self,
onClick, onClick,
onEmojiStatusClick, onEmojiStatusClick,
}) => { }) => {
@ -199,15 +204,28 @@ const GroupChatInfo: FC<OwnProps & StateProps> = ({
onClick={onClick} onClick={onClick}
> >
{!noAvatar && !isTopic && ( {!noAvatar && !isTopic && (
<Avatar <>
key={chat.id} {isSavedDialog && self && (
size={avatarSize} <Avatar
peer={chat} key="saved-messages"
withStory={withStory} size={avatarSize}
storyViewerOrigin={storyViewerOrigin} peer={self}
storyViewerMode="single-peer" isSavedMessages
onClick={withMediaViewer ? handleAvatarViewerOpen : undefined} className="saved-dialog-avatar"
/> />
)}
<Avatar
key={chat.id}
className={buildClassName(isSavedDialog && 'overlay-avatar')}
size={avatarSize}
peer={chat}
withStory={withStory}
storyViewerOrigin={storyViewerOrigin}
storyViewerMode="single-peer"
isSavedDialog={isSavedDialog}
onClick={withMediaViewer ? handleAvatarViewerOpen : undefined}
/>
</>
)} )}
{isTopic && ( {isTopic && (
<TopicIcon <TopicIcon
@ -224,6 +242,7 @@ const GroupChatInfo: FC<OwnProps & StateProps> = ({
peer={chat} peer={chat}
emojiStatusSize={emojiStatusSize} emojiStatusSize={emojiStatusSize}
withEmojiStatus={!noEmojiStatus} withEmojiStatus={!noEmojiStatus}
isSavedDialog={isSavedDialog}
onEmojiStatusClick={onEmojiStatusClick} onEmojiStatusClick={onEmojiStatusClick}
/> />
)} )}
@ -258,6 +277,7 @@ export default memo(withGlobal<OwnProps>(
const areMessagesLoaded = Boolean(selectChatMessages(global, chatId)); const areMessagesLoaded = Boolean(selectChatMessages(global, chatId));
const topic = threadId ? chat?.topics?.[threadId] : undefined; const topic = threadId ? chat?.topics?.[threadId] : undefined;
const messagesCount = topic && selectThreadMessagesCount(global, chatId, threadId!); const messagesCount = topic && selectThreadMessagesCount(global, chatId, threadId!);
const self = selectUser(global, global.currentUserId!);
return { return {
chat, chat,
@ -266,6 +286,7 @@ export default memo(withGlobal<OwnProps>(
topic, topic,
areMessagesLoaded, areMessagesLoaded,
messagesCount, messagesCount,
self,
}; };
}, },
)(GroupChatInfo)); )(GroupChatInfo));

View File

@ -1,3 +1,4 @@
import type { AriaRole } from 'react';
import React from '../../lib/teact/teact'; import React from '../../lib/teact/teact';
import type { IconName } from '../../types/icons'; import type { IconName } from '../../types/icons';
@ -8,18 +9,24 @@ type OwnProps = {
name: IconName; name: IconName;
className?: string; className?: string;
style?: string; style?: string;
role?: AriaRole;
ariaLabel?: string;
}; };
const Icon = ({ const Icon = ({
name, name,
className, className,
style, style,
role,
ariaLabel,
}: OwnProps) => { }: OwnProps) => {
return ( return (
<i <i
className={buildClassName(`icon icon-${name}`, className)} className={buildClassName(`icon icon-${name}`, className)}
style={style} style={style}
aria-hidden aria-hidden={!ariaLabel}
aria-label={ariaLabel}
role={role}
/> />
); );
}; };

View File

@ -44,6 +44,7 @@ type OwnProps = {
noStatusOrTyping?: boolean; noStatusOrTyping?: boolean;
noRtl?: boolean; noRtl?: boolean;
adminMember?: ApiChatMember; adminMember?: ApiChatMember;
isSavedDialog?: boolean;
className?: string; className?: string;
onEmojiStatusClick?: NoneToVoidFunction; onEmojiStatusClick?: NoneToVoidFunction;
}; };
@ -52,6 +53,7 @@ type StateProps =
{ {
user?: ApiUser; user?: ApiUser;
userStatus?: ApiUserStatus; userStatus?: ApiUserStatus;
self?: ApiUser;
isSavedMessages?: boolean; isSavedMessages?: boolean;
areMessagesLoaded: boolean; areMessagesLoaded: boolean;
}; };
@ -73,7 +75,9 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
noRtl, noRtl,
user, user,
userStatus, userStatus,
self,
isSavedMessages, isSavedMessages,
isSavedDialog,
areMessagesLoaded, areMessagesLoaded,
adminMember, adminMember,
ripple, ripple,
@ -166,6 +170,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
withEmojiStatus={!noEmojiStatus} withEmojiStatus={!noEmojiStatus}
emojiStatusSize={emojiStatusSize} emojiStatusSize={emojiStatusSize}
isSavedMessages={isSavedMessages} isSavedMessages={isSavedMessages}
isSavedDialog={isSavedDialog}
onEmojiStatusClick={onEmojiStatusClick} onEmojiStatusClick={onEmojiStatusClick}
/> />
{customTitle && <span className="custom-title">{customTitle}</span>} {customTitle && <span className="custom-title">{customTitle}</span>}
@ -179,6 +184,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
withEmojiStatus={!noEmojiStatus} withEmojiStatus={!noEmojiStatus}
emojiStatusSize={emojiStatusSize} emojiStatusSize={emojiStatusSize}
isSavedMessages={isSavedMessages} isSavedMessages={isSavedMessages}
isSavedDialog={isSavedDialog}
onEmojiStatusClick={onEmojiStatusClick} onEmojiStatusClick={onEmojiStatusClick}
/> />
); );
@ -186,11 +192,22 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
return ( return (
<div className={buildClassName('ChatInfo', className)} dir={!noRtl && lang.isRtl ? 'rtl' : undefined}> <div className={buildClassName('ChatInfo', className)} dir={!noRtl && lang.isRtl ? 'rtl' : undefined}>
{isSavedDialog && self && (
<Avatar
key="saved-messages"
size={avatarSize}
peer={self}
isSavedMessages
className="saved-dialog-avatar"
/>
)}
<Avatar <Avatar
key={user.id} key={user.id}
size={avatarSize} size={avatarSize}
peer={user} peer={user}
className={buildClassName(isSavedDialog && 'overlay-avatar')}
isSavedMessages={isSavedMessages} isSavedMessages={isSavedMessages}
isSavedDialog={isSavedDialog}
withStory={withStory} withStory={withStory}
storyViewerOrigin={storyViewerOrigin} storyViewerOrigin={storyViewerOrigin}
storyViewerMode="single-peer" storyViewerMode="single-peer"
@ -210,6 +227,7 @@ export default memo(withGlobal<OwnProps>(
const user = selectUser(global, userId); const user = selectUser(global, userId);
const userStatus = selectUserStatus(global, userId); const userStatus = selectUserStatus(global, userId);
const isSavedMessages = !forceShowSelf && user && user.isSelf; const isSavedMessages = !forceShowSelf && user && user.isSelf;
const self = isSavedMessages ? user : selectUser(global, global.currentUserId!);
const areMessagesLoaded = Boolean(selectChatMessages(global, userId)); const areMessagesLoaded = Boolean(selectChatMessages(global, userId));
return { return {
@ -217,6 +235,7 @@ export default memo(withGlobal<OwnProps>(
userStatus, userStatus,
isSavedMessages, isSavedMessages,
areMessagesLoaded, areMessagesLoaded,
self,
}; };
}, },
)(PrivateChatInfo)); )(PrivateChatInfo));

View File

@ -9,7 +9,7 @@ import type { GlobalState } from '../../global/types';
import { MediaViewerOrigin } from '../../types'; import { MediaViewerOrigin } from '../../types';
import { import {
getUserStatus, isChatChannel, isUserId, isUserOnline, getUserStatus, isAnonymousForwardsChat, isChatChannel, isUserId, isUserOnline,
} from '../../global/helpers'; } from '../../global/helpers';
import { import {
selectChat, selectChat,
@ -53,7 +53,6 @@ type StateProps =
user?: ApiUser; user?: ApiUser;
userStatus?: ApiUserStatus; userStatus?: ApiUserStatus;
chat?: ApiChat; chat?: ApiChat;
isSavedMessages?: boolean;
mediaId?: number; mediaId?: number;
avatarOwnerId?: string; avatarOwnerId?: string;
topic?: ApiTopic; topic?: ApiTopic;
@ -75,7 +74,6 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
user, user,
userStatus, userStatus,
chat, chat,
isSavedMessages,
connectionState, connectionState,
mediaId, mediaId,
avatarOwnerId, avatarOwnerId,
@ -107,8 +105,8 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
const slideAnimation = hasSlideAnimation ? (lang.isRtl ? 'slideRtl' : 'slide') : 'none'; const slideAnimation = hasSlideAnimation ? (lang.isRtl ? 'slideRtl' : 'slide') : 'none';
const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0); const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0);
const isFirst = isSavedMessages || photos.length <= 1 || currentPhotoIndex === 0; const isFirst = photos.length <= 1 || currentPhotoIndex === 0;
const isLast = isSavedMessages || photos.length <= 1 || currentPhotoIndex === photos.length - 1; const isLast = photos.length <= 1 || currentPhotoIndex === photos.length - 1;
// Set the current avatar photo to the last selected photo in Media Viewer after it is closed // Set the current avatar photo to the last selected photo in Media Viewer after it is closed
useEffect(() => { useEffect(() => {
@ -227,7 +225,7 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
} }
function renderPhotoTabs() { function renderPhotoTabs() {
if (isSavedMessages || !photos || photos.length <= 1) { if (!photos || photos.length <= 1) {
return undefined; return undefined;
} }
@ -241,7 +239,7 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
} }
function renderPhoto(isActive?: boolean) { function renderPhoto(isActive?: boolean) {
const photo = !isSavedMessages && photos.length > 0 const photo = photos.length > 0
? photos[currentPhotoIndex] ? photos[currentPhotoIndex]
: undefined; : undefined;
const profilePhoto = photo || userPersonalPhoto || userProfilePhoto || chatProfilePhoto || userFallbackPhoto; const profilePhoto = photo || userPersonalPhoto || userProfilePhoto || chatProfilePhoto || userFallbackPhoto;
@ -252,7 +250,6 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
user={user} user={user}
chat={chat} chat={chat}
photo={profilePhoto} photo={profilePhoto}
isSavedMessages={isSavedMessages}
canPlayVideo={Boolean(isActive && canPlayVideo)} canPlayVideo={Boolean(isActive && canPlayVideo)}
onClick={handleProfilePhotoClick} onClick={handleProfilePhotoClick}
/> />
@ -260,6 +257,11 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
} }
function renderStatus() { function renderStatus() {
const peerId = (chatId || userId)!;
const isAnonymousForwards = isAnonymousForwardsChat(peerId);
if (isAnonymousForwards) return undefined;
if (user) { if (user) {
return ( return (
<div className={buildClassName(styles.status, 'status', isUserOnline(user, userStatus) && 'online')}> <div className={buildClassName(styles.status, 'status', isUserOnline(user, userStatus) && 'online')}>
@ -348,26 +350,24 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
peer={(user || chat)!} peer={(user || chat)!}
withEmojiStatus withEmojiStatus
emojiStatusSize={EMOJI_STATUS_SIZE} emojiStatusSize={EMOJI_STATUS_SIZE}
isSavedMessages={isSavedMessages}
onEmojiStatusClick={handleStatusClick} onEmojiStatusClick={handleStatusClick}
noLoopLimit noLoopLimit
canCopyTitle canCopyTitle
/> />
)} )}
{!isSavedMessages && renderStatus()} {renderStatus()}
</div> </div>
</div> </div>
); );
}; };
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global, { userId, forceShowSelf }): StateProps => { (global, { userId }): StateProps => {
const { connectionState } = global; const { connectionState } = global;
const user = selectUser(global, userId); const user = selectUser(global, userId);
const isPrivate = isUserId(userId); const isPrivate = isUserId(userId);
const userStatus = selectUserStatus(global, userId); const userStatus = selectUserStatus(global, userId);
const chat = selectChat(global, userId); const chat = selectChat(global, userId);
const isSavedMessages = !forceShowSelf && user && user.isSelf;
const { mediaId, avatarOwnerId } = selectTabState(global).mediaViewer; const { mediaId, avatarOwnerId } = selectTabState(global).mediaViewer;
const isForum = chat?.isForum; const isForum = chat?.isForum;
const { threadId: currentTopicId } = selectCurrentMessageList(global) || {}; const { threadId: currentTopicId } = selectCurrentMessageList(global) || {};
@ -387,7 +387,6 @@ export default memo(withGlobal<OwnProps>(
userProfilePhoto: userFullInfo?.profilePhoto, userProfilePhoto: userFullInfo?.profilePhoto,
userFallbackPhoto: userFullInfo?.fallbackPhoto, userFallbackPhoto: userFullInfo?.fallbackPhoto,
chatProfilePhoto: chatFullInfo?.profilePhoto, chatProfilePhoto: chatFullInfo?.profilePhoto,
isSavedMessages,
mediaId, mediaId,
avatarOwnerId, avatarOwnerId,
emojiStatusSticker, emojiStatusSticker,

View File

@ -1,5 +1,7 @@
import type { FC, TeactNode } from '../../lib/teact/teact'; import type { FC, TeactNode } from '../../lib/teact/teact';
import React, { memo, useEffect, useRef } from '../../lib/teact/teact'; import React, {
memo, useEffect, useMemo, useRef,
} from '../../lib/teact/teact';
import type { ApiChat, ApiPhoto, ApiUser } from '../../api/types'; import type { ApiChat, ApiPhoto, ApiUser } from '../../api/types';
@ -8,6 +10,7 @@ import {
getChatTitle, getChatTitle,
getUserFullName, getUserFullName,
getVideoAvatarMediaHash, getVideoAvatarMediaHash,
isAnonymousForwardsChat,
isChatWithRepliesBot, isChatWithRepliesBot,
isDeletedUser, isDeletedUser,
isUserId, isUserId,
@ -27,6 +30,7 @@ import useMediaTransition from '../../hooks/useMediaTransition';
import OptimizedVideo from '../ui/OptimizedVideo'; import OptimizedVideo from '../ui/OptimizedVideo';
import Spinner from '../ui/Spinner'; import Spinner from '../ui/Spinner';
import Icon from './Icon';
import './ProfilePhoto.scss'; import './ProfilePhoto.scss';
@ -34,6 +38,7 @@ type OwnProps = {
chat?: ApiChat; chat?: ApiChat;
user?: ApiUser; user?: ApiUser;
isSavedMessages?: boolean; isSavedMessages?: boolean;
isSavedDialog?: boolean;
photo?: ApiPhoto; photo?: ApiPhoto;
canPlayVideo: boolean; canPlayVideo: boolean;
onClick: NoneToVoidFunction; onClick: NoneToVoidFunction;
@ -44,6 +49,7 @@ const ProfilePhoto: FC<OwnProps> = ({
user, user,
photo, photo,
isSavedMessages, isSavedMessages,
isSavedDialog,
canPlayVideo, canPlayVideo,
onClick, onClick,
}) => { }) => {
@ -55,8 +61,9 @@ const ProfilePhoto: FC<OwnProps> = ({
const isDeleted = user && isDeletedUser(user); const isDeleted = user && isDeletedUser(user);
const isRepliesChat = chat && isChatWithRepliesBot(chat.id); const isRepliesChat = chat && isChatWithRepliesBot(chat.id);
const isAnonymousForwards = chat && isAnonymousForwardsChat(chat.id);
const peer = user || chat; const peer = user || chat;
const canHaveMedia = peer && !isSavedMessages && !isDeleted && !isRepliesChat; const canHaveMedia = peer && !isSavedMessages && !isDeleted && !isRepliesChat && !isAnonymousForwards;
const { isVideo } = photo || {}; const { isVideo } = photo || {};
const avatarHash = canHaveMedia && getChatAvatarHash(peer, 'normal'); const avatarHash = canHaveMedia && getChatAvatarHash(peer, 'normal');
@ -84,14 +91,30 @@ const ProfilePhoto: FC<OwnProps> = ({
} }
}, [canPlayVideo]); }, [canPlayVideo]);
const specialIcon = useMemo(() => {
if (isSavedMessages) {
return isSavedDialog ? 'my-notes' : 'avatar-saved-messages';
}
if (isDeleted) {
return 'avatar-deleted-account';
}
if (isRepliesChat) {
return 'reply-filled';
}
if (isAnonymousForwards) {
return 'author-hidden';
}
return undefined;
}, [isAnonymousForwards, isDeleted, isSavedDialog, isRepliesChat, isSavedMessages]);
let content: TeactNode | undefined; let content: TeactNode | undefined;
if (isSavedMessages) { if (specialIcon) {
content = <i className="icon icon-avatar-saved-messages" />; content = <Icon name={specialIcon} role="img" />;
} else if (isDeleted) {
content = <i className="icon icon-avatar-deleted-account" />;
} else if (isRepliesChat) {
content = <i className="icon icon-reply-filled" />;
} else if (hasMedia) { } else if (hasMedia) {
content = ( content = (
<> <>
@ -141,6 +164,7 @@ const ProfilePhoto: FC<OwnProps> = ({
'ProfilePhoto', 'ProfilePhoto',
getPeerColorClass(peer), getPeerColorClass(peer),
isSavedMessages && 'saved-messages', isSavedMessages && 'saved-messages',
isAnonymousForwards && 'anonymous-forwards',
isDeleted && 'deleted-account', isDeleted && 'deleted-account',
isRepliesChat && 'replies-bot-account', isRepliesChat && 'replies-bot-account',
(!isSavedMessages && !hasMedia) && 'no-photo', (!isSavedMessages && !hasMedia) && 'no-photo',

View File

@ -3,6 +3,7 @@ import React, { memo, useMemo, useState } from '../../lib/teact/teact';
import { getGlobal, withGlobal } from '../../global'; import { getGlobal, withGlobal } from '../../global';
import type { ApiChat, ApiChatType } from '../../api/types'; import type { ApiChat, ApiChatType } from '../../api/types';
import type { ThreadId } from '../../types';
import { MAIN_THREAD_ID } from '../../api/types'; import { MAIN_THREAD_ID } from '../../api/types';
import { API_CHAT_TYPES } from '../../config'; import { API_CHAT_TYPES } from '../../config';
@ -11,10 +12,10 @@ import {
filterUsersByName, filterUsersByName,
getCanPostInChat, getCanPostInChat,
isDeletedUser, isDeletedUser,
sortChatIds,
} from '../../global/helpers'; } from '../../global/helpers';
import { filterChatIdsByType } from '../../global/selectors'; import { filterChatIdsByType } from '../../global/selectors';
import { unique } from '../../util/iteratees'; import { unique } from '../../util/iteratees';
import sortChatIds from './helpers/sortChatIds';
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev'; import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
@ -27,7 +28,7 @@ export type OwnProps = {
className?: string; className?: string;
filter?: ApiChatType[]; filter?: ApiChatType[];
loadMore?: NoneToVoidFunction; loadMore?: NoneToVoidFunction;
onSelectRecipient: (peerId: string, threadId?: number) => void; onSelectRecipient: (peerId: string, threadId?: ThreadId) => void;
onClose: NoneToVoidFunction; onClose: NoneToVoidFunction;
onCloseAnimationEnd?: NoneToVoidFunction; onCloseAnimationEnd?: NoneToVoidFunction;
}; };
@ -85,7 +86,7 @@ const RecipientPicker: FC<OwnProps & StateProps> = ({
const sorted = sortChatIds(unique([ const sorted = sortChatIds(unique([
...filterChatsByName(lang, chatIds, chatsById, search, currentUserId), ...filterChatsByName(lang, chatIds, chatsById, search, currentUserId),
...(contactIds && filter.includes('users') ? filterUsersByName(contactIds, usersById, search) : []), ...(contactIds && filter.includes('users') ? filterUsersByName(contactIds, usersById, search) : []),
]), chatsById, undefined, priorityIds); ]), undefined, priorityIds);
return filterChatIdsByType(global, sorted, filter); return filterChatIdsByType(global, sorted, filter);
}, [pinnedIds, currentUserId, activeListIds, search, archivedListIds, lang, chatsById, contactIds, filter, isOpen]); }, [pinnedIds, currentUserId, activeListIds, search, archivedListIds, lang, chatsById, contactIds, filter, isOpen]);

View File

@ -0,0 +1,39 @@
import { getGlobal } from '../../../global';
import { selectChat, selectChatLastMessage } from '../../../global/selectors';
import { orderBy } from '../../../util/iteratees';
const VERIFIED_PRIORITY_BASE = 3e9;
const PINNED_PRIORITY_BASE = 3e8;
export default function sortChatIds(
chatIds: string[],
shouldPrioritizeVerified = false,
priorityIds?: string[],
) {
// Avoid calling sort on every global change
const global = getGlobal();
return orderBy(chatIds, (id) => {
const chat = selectChat(global, id);
if (!chat) {
return 0;
}
let priority = 0;
const lastMessage = selectChatLastMessage(global, id);
if (lastMessage) {
priority += lastMessage.date;
}
if (shouldPrioritizeVerified && chat.isVerified) {
priority += VERIFIED_PRIORITY_BASE; // ~100 years in seconds
}
if (priorityIds && priorityIds.includes(id)) {
priority = Date.now() + PINNED_PRIORITY_BASE + (priorityIds.length - priorityIds.indexOf(id));
}
return priority;
}, 'desc');
}

View File

@ -1,5 +1,5 @@
import type { FC } from '../../../lib/teact/teact'; import type { FC } from '../../../lib/teact/teact';
import React, { memo, useEffect } from '../../../lib/teact/teact'; import React, { memo, useEffect, useMemo } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import type { import type {
@ -29,6 +29,8 @@ import { getMessageReplyInfo } from '../../../global/helpers/replies';
import { import {
selectCanAnimateInterface, selectCanAnimateInterface,
selectChat, selectChat,
selectChatLastMessage,
selectChatLastMessageId,
selectChatMessage, selectChatMessage,
selectCurrentMessageList, selectCurrentMessageList,
selectDraft, selectDraft,
@ -37,6 +39,7 @@ import {
selectNotifyExceptions, selectNotifyExceptions,
selectNotifySettings, selectNotifySettings,
selectOutgoingStatus, selectOutgoingStatus,
selectPeer,
selectTabState, selectTabState,
selectThreadParam, selectThreadParam,
selectTopicFromMessage, selectTopicFromMessage,
@ -49,6 +52,7 @@ import { IS_OPEN_IN_NEW_TAB_SUPPORTED } from '../../../util/windowEnvironment';
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 useFlag from '../../../hooks/useFlag'; import useFlag from '../../../hooks/useFlag';
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver'; import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
import useLastCallback from '../../../hooks/useLastCallback'; import useLastCallback from '../../../hooks/useLastCallback';
@ -76,6 +80,7 @@ type OwnProps = {
animationType: ChatAnimationTypes; animationType: ChatAnimationTypes;
isPinned?: boolean; isPinned?: boolean;
offsetTop: number; offsetTop: number;
isSavedDialog?: boolean;
observeIntersection?: ObserveFn; observeIntersection?: ObserveFn;
onDragEnter?: (chatId: string) => void; onDragEnter?: (chatId: string) => void;
}; };
@ -99,6 +104,9 @@ type StateProps = {
lastMessageTopic?: ApiTopic; lastMessageTopic?: ApiTopic;
typingStatus?: ApiTypingStatus; typingStatus?: ApiTypingStatus;
withInterfaceAnimations?: boolean; withInterfaceAnimations?: boolean;
lastMessageId?: number;
lastMessage?: ApiMessage;
currentUserId: string;
}; };
const Chat: FC<OwnProps & StateProps> = ({ const Chat: FC<OwnProps & StateProps> = ({
@ -127,10 +135,16 @@ const Chat: FC<OwnProps & StateProps> = ({
canChangeFolder, canChangeFolder,
lastMessageTopic, lastMessageTopic,
typingStatus, typingStatus,
lastMessageId,
lastMessage,
isSavedDialog,
currentUserId,
onDragEnter, onDragEnter,
}) => { }) => {
const { const {
openChat, openChat,
openSavedDialog,
toggleChatInfo,
focusLastMessage, focusLastMessage,
loadTopics, loadTopics,
openForumPanel, openForumPanel,
@ -147,7 +161,9 @@ const Chat: FC<OwnProps & StateProps> = ({
const [shouldRenderChatFolderModal, markRenderChatFolderModal, unmarkRenderChatFolderModal] = useFlag(); const [shouldRenderChatFolderModal, markRenderChatFolderModal, unmarkRenderChatFolderModal] = useFlag();
const [shouldRenderReportModal, markRenderReportModal, unmarkRenderReportModal] = useFlag(); const [shouldRenderReportModal, markRenderReportModal, unmarkRenderReportModal] = useFlag();
const { lastMessage, isForum, isForumAsMessages } = chat || {}; const { isForum, isForumAsMessages } = chat || {};
useEnsureMessage(isSavedDialog ? currentUserId : chatId, lastMessageId, lastMessage);
const { renderSubtitle, ref } = useChatListEntry({ const { renderSubtitle, ref } = useChatListEntry({
chat, chat,
@ -164,6 +180,7 @@ const Chat: FC<OwnProps & StateProps> = ({
animationType, animationType,
withInterfaceAnimations, withInterfaceAnimations,
orderDiff, orderDiff,
isSavedDialog,
}); });
const getIsForumPanelClosed = useSelectorSignal(selectIsForumPanelClosed); const getIsForumPanelClosed = useSelectorSignal(selectIsForumPanelClosed);
@ -171,6 +188,15 @@ const Chat: FC<OwnProps & StateProps> = ({
const handleClick = useLastCallback(() => { const handleClick = useLastCallback(() => {
const noForumTopicPanel = isMobile && isForumAsMessages; const noForumTopicPanel = isMobile && isForumAsMessages;
if (isSavedDialog) {
openSavedDialog({ chatId, noForumTopicPanel: true }, { forceOnHeavyAnimation: true });
if (isMobile) {
toggleChatInfo({ force: false });
}
return;
}
if (isForum) { if (isForum) {
if (isForumPanelOpen) { if (isForumPanelOpen) {
closeForumPanel(undefined, { forceOnHeavyAnimation: true }); closeForumPanel(undefined, { forceOnHeavyAnimation: true });
@ -228,6 +254,8 @@ const Chat: FC<OwnProps & StateProps> = ({
isPinned, isPinned,
isMuted, isMuted,
canChangeFolder, canChangeFolder,
isSavedDialog,
currentUserId,
}); });
const isIntersecting = useIsIntersecting(ref, chat ? observeIntersection : undefined); const isIntersecting = useIsIntersecting(ref, chat ? observeIntersection : undefined);
@ -242,6 +270,16 @@ const Chat: FC<OwnProps & StateProps> = ({
const isOnline = user && userStatus && isUserOnline(user, userStatus); const isOnline = user && userStatus && isUserOnline(user, userStatus);
const { hasShownClass: isAvatarOnlineShown } = useShowTransition(isOnline); const { hasShownClass: isAvatarOnlineShown } = useShowTransition(isOnline);
const href = useMemo(() => {
if (!IS_OPEN_IN_NEW_TAB_SUPPORTED) return undefined;
if (isSavedDialog) {
return `#${createLocationHash(currentUserId, 'thread', chatId)}`;
}
return `#${createLocationHash(chatId, 'thread', MAIN_THREAD_ID)}`;
}, [chatId, currentUserId, isSavedDialog]);
if (!chat) { if (!chat) {
return undefined; return undefined;
} }
@ -260,7 +298,7 @@ const Chat: FC<OwnProps & StateProps> = ({
<ListItem <ListItem
ref={ref} ref={ref}
className={className} className={className}
href={IS_OPEN_IN_NEW_TAB_SUPPORTED ? `#${createLocationHash(chatId, 'thread', MAIN_THREAD_ID)}` : undefined} href={href}
style={`top: ${offsetTop}px`} style={`top: ${offsetTop}px`}
ripple={!isForum && !isMobile} ripple={!isForum && !isMobile}
contextActions={contextActions} contextActions={contextActions}
@ -272,6 +310,7 @@ const Chat: FC<OwnProps & StateProps> = ({
<Avatar <Avatar
peer={peer} peer={peer}
isSavedMessages={user?.isSelf} isSavedMessages={user?.isSelf}
isSavedDialog={isSavedDialog}
withStory={!user?.isSelf} withStory={!user?.isSelf}
withStoryGap={isAvatarOnlineShown} withStoryGap={isAvatarOnlineShown}
storyViewerOrigin={StoryViewerOrigin.ChatList} storyViewerOrigin={StoryViewerOrigin.ChatList}
@ -291,21 +330,22 @@ const Chat: FC<OwnProps & StateProps> = ({
peer={peer} peer={peer}
withEmojiStatus withEmojiStatus
isSavedMessages={chatId === user?.id && user?.isSelf} isSavedMessages={chatId === user?.id && user?.isSelf}
isSavedDialog={isSavedDialog}
observeIntersection={observeIntersection} observeIntersection={observeIntersection}
/> />
{isMuted && <i className="icon icon-muted" />} {isMuted && !isSavedDialog && <i className="icon icon-muted" />}
<div className="separator" /> <div className="separator" />
{chat.lastMessage && ( {lastMessage && (
<LastMessageMeta <LastMessageMeta
message={chat.lastMessage} message={lastMessage}
outgoingStatus={lastMessageOutgoingStatus} outgoingStatus={!isSavedDialog ? lastMessageOutgoingStatus : undefined}
draftDate={draft?.date} draftDate={draft?.date}
/> />
)} )}
</div> </div>
<div className="subtitle"> <div className="subtitle">
{renderSubtitle()} {renderSubtitle()}
<ChatBadge chat={chat} isPinned={isPinned} isMuted={isMuted} /> <ChatBadge chat={chat} isPinned={isPinned} isMuted={isMuted} isSavedDialog={isSavedDialog} />
</div> </div>
</div> </div>
{shouldRenderDeleteModal && ( {shouldRenderDeleteModal && (
@ -346,29 +386,34 @@ const Chat: FC<OwnProps & StateProps> = ({
}; };
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global, { chatId }): StateProps => { (global, { chatId, isSavedDialog }): StateProps => {
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
if (!chat) { if (!chat) {
return {}; return {
currentUserId: global.currentUserId!,
};
} }
const { lastMessage } = chat; const lastMessageId = selectChatLastMessageId(global, chatId, isSavedDialog ? 'saved' : 'all');
const { senderId, isOutgoing } = lastMessage || {}; const lastMessage = selectChatLastMessage(global, chatId, isSavedDialog ? 'saved' : 'all');
const { senderId, isOutgoing, forwardInfo } = lastMessage || {};
const actualSenderId = isSavedDialog ? forwardInfo?.fromId : senderId;
const replyToMessageId = lastMessage && getMessageReplyInfo(lastMessage)?.replyToMsgId; const replyToMessageId = lastMessage && getMessageReplyInfo(lastMessage)?.replyToMsgId;
const lastMessageSender = senderId const lastMessageSender = actualSenderId ? selectPeer(global, actualSenderId) : undefined;
? (selectUser(global, senderId) || selectChat(global, senderId)) : undefined;
const lastMessageAction = lastMessage ? getMessageAction(lastMessage) : undefined; const lastMessageAction = lastMessage ? getMessageAction(lastMessage) : undefined;
const actionTargetMessage = lastMessageAction && replyToMessageId const actionTargetMessage = lastMessageAction && replyToMessageId
? selectChatMessage(global, chat.id, replyToMessageId) ? selectChatMessage(global, chat.id, replyToMessageId)
: undefined; : undefined;
const { targetUserIds: actionTargetUserIds, targetChatId: actionTargetChatId } = lastMessageAction || {}; const { targetUserIds: actionTargetUserIds, targetChatId: actionTargetChatId } = lastMessageAction || {};
const privateChatUserId = getPrivateChatUserId(chat); const privateChatUserId = getPrivateChatUserId(chat);
const { const {
chatId: currentChatId, chatId: currentChatId,
threadId: currentThreadId, threadId: currentThreadId,
type: messageListType, type: messageListType,
} = selectCurrentMessageList(global) || {}; } = selectCurrentMessageList(global) || {};
const isSelected = chatId === currentChatId && currentThreadId === MAIN_THREAD_ID; const isSelected = chatId === currentChatId && (isSavedDialog
? chatId === currentThreadId : currentThreadId === MAIN_THREAD_ID);
const isSelectedForum = (chat.isForum && chatId === currentChatId) const isSelectedForum = (chat.isForum && chatId === currentChatId)
|| chatId === selectTabState(global).forumPanelChatId; || chatId === selectTabState(global).forumPanelChatId;
@ -399,6 +444,9 @@ export default memo(withGlobal<OwnProps>(
lastMessageTopic, lastMessageTopic,
typingStatus, typingStatus,
withInterfaceAnimations: selectCanAnimateInterface(global), withInterfaceAnimations: selectCanAnimateInterface(global),
lastMessage,
lastMessageId,
currentUserId: global.currentUserId!,
}; };
}, },
)(Chat)); )(Chat));

View File

@ -21,12 +21,13 @@ type OwnProps = {
wasTopicOpened?: boolean; wasTopicOpened?: boolean;
isPinned?: boolean; isPinned?: boolean;
isMuted?: boolean; isMuted?: boolean;
isSavedDialog?: boolean;
shouldShowOnlyMostImportant?: boolean; shouldShowOnlyMostImportant?: boolean;
forceHidden?: boolean | Signal<boolean>; forceHidden?: boolean | Signal<boolean>;
}; };
const ChatBadge: FC<OwnProps> = ({ const ChatBadge: FC<OwnProps> = ({
topic, chat, isPinned, isMuted, shouldShowOnlyMostImportant, wasTopicOpened, forceHidden, topic, chat, isPinned, isMuted, shouldShowOnlyMostImportant, wasTopicOpened, forceHidden, isSavedDialog,
}) => { }) => {
const { const {
unreadMentionsCount = 0, unreadReactionsCount = 0, unreadMentionsCount = 0, unreadReactionsCount = 0,
@ -64,7 +65,7 @@ const ChatBadge: FC<OwnProps> = ({
|| isTopicUnopened, || isTopicUnopened,
); );
const isUnread = Boolean(unreadCount || hasUnreadMark); const isUnread = Boolean((unreadCount || hasUnreadMark) && !isSavedDialog);
const className = buildClassName( const className = buildClassName(
'ChatBadge', 'ChatBadge',
shouldBeMuted && 'muted', shouldBeMuted && 'muted',
@ -95,16 +96,21 @@ const ChatBadge: FC<OwnProps> = ({
</div> </div>
) : undefined; ) : undefined;
const pinnedElement = isPinned && !unreadCountElement && !unreadMentionsElement && !unreadReactionsElement && ( const pinnedElement = isPinned && (
<div className={className}> <div className={className}>
<i className="icon icon-pinned-chat" /> <i className="icon icon-pinned-chat" />
</div> </div>
); );
const visiblePinnedElement = !unreadCountElement && !unreadMentionsElement && !unreadReactionsElement
&& pinnedElement;
const elements = [ const elements = [
unopenedTopicElement, unreadReactionsElement, unreadMentionsElement, unreadCountElement, pinnedElement, unopenedTopicElement, unreadReactionsElement, unreadMentionsElement, unreadCountElement, visiblePinnedElement,
].filter(Boolean); ].filter(Boolean);
if (isSavedDialog) return pinnedElement;
if (elements.length === 0) return undefined; if (elements.length === 0) return undefined;
if (elements.length === 1) return elements[0]; if (elements.length === 1) return elements[0];

View File

@ -17,6 +17,7 @@ import {
CHAT_HEIGHT_PX, CHAT_HEIGHT_PX,
CHAT_LIST_SLICE, CHAT_LIST_SLICE,
FRESH_AUTH_PERIOD, FRESH_AUTH_PERIOD,
SAVED_FOLDER_ID,
} from '../../../config'; } from '../../../config';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { getOrderKey, getPinnedChatsCount } from '../../../util/folderManager'; import { getOrderKey, getPinnedChatsCount } from '../../../util/folderManager';
@ -41,16 +42,17 @@ import EmptyFolder from './EmptyFolder';
import UnconfirmedSession from './UnconfirmedSession'; import UnconfirmedSession from './UnconfirmedSession';
type OwnProps = { type OwnProps = {
folderType: 'all' | 'archived' | 'folder'; className?: string;
folderType: 'all' | 'archived' | 'saved' | 'folder';
folderId?: number; folderId?: number;
isActive: boolean; isActive: boolean;
canDisplayArchive?: boolean; canDisplayArchive?: boolean;
archiveSettings: GlobalState['archiveSettings']; archiveSettings?: GlobalState['archiveSettings'];
isForumPanelOpen?: boolean; isForumPanelOpen?: boolean;
sessions?: Record<string, ApiSession>; sessions?: Record<string, ApiSession>;
foldersDispatch: FolderEditDispatch; foldersDispatch?: FolderEditDispatch;
onSettingsScreenSelect: (screen: SettingsScreens) => void; onSettingsScreenSelect?: (screen: SettingsScreens) => void;
onLeftColumnContentChange: (content: LeftColumnContent) => void; onLeftColumnContentChange?: (content: LeftColumnContent) => void;
}; };
const INTERSECTION_THROTTLE = 200; const INTERSECTION_THROTTLE = 200;
@ -58,6 +60,7 @@ const DRAG_ENTER_DEBOUNCE = 500;
const RESERVED_HOTKEYS = new Set(['9', '0']); const RESERVED_HOTKEYS = new Set(['9', '0']);
const ChatList: FC<OwnProps> = ({ const ChatList: FC<OwnProps> = ({
className,
folderType, folderType,
folderId, folderId,
isActive, isActive,
@ -82,18 +85,19 @@ const ChatList: FC<OwnProps> = ({
const isArchived = folderType === 'archived'; const isArchived = folderType === 'archived';
const isAllFolder = folderType === 'all'; const isAllFolder = folderType === 'all';
const isSaved = folderType === 'saved';
const resolvedFolderId = ( const resolvedFolderId = (
isAllFolder ? ALL_FOLDER_ID : isArchived ? ARCHIVED_FOLDER_ID : folderId! isAllFolder ? ALL_FOLDER_ID : isArchived ? ARCHIVED_FOLDER_ID : isSaved ? SAVED_FOLDER_ID : folderId!
); );
const shouldDisplayArchive = isAllFolder && canDisplayArchive; const shouldDisplayArchive = isAllFolder && canDisplayArchive && archiveSettings;
const orderedIds = useFolderManagerForOrderedIds(resolvedFolderId); const orderedIds = useFolderManagerForOrderedIds(resolvedFolderId);
usePeerStoriesPolling(orderedIds); usePeerStoriesPolling(orderedIds);
const chatsHeight = (orderedIds?.length || 0) * CHAT_HEIGHT_PX; const chatsHeight = (orderedIds?.length || 0) * CHAT_HEIGHT_PX;
const archiveHeight = shouldDisplayArchive const archiveHeight = shouldDisplayArchive
? archiveSettings.isMinimized ? ARCHIVE_MINIMIZED_HEIGHT : CHAT_HEIGHT_PX : 0; ? archiveSettings?.isMinimized ? ARCHIVE_MINIMIZED_HEIGHT : CHAT_HEIGHT_PX : 0;
const { orderDiffById, getAnimationType } = useOrderDiff(orderedIds); const { orderDiffById, getAnimationType } = useOrderDiff(orderedIds);
@ -125,7 +129,7 @@ const ChatList: FC<OwnProps> = ({
// Support <Cmd>+<Digit> to navigate between chats // Support <Cmd>+<Digit> to navigate between chats
useEffect(() => { useEffect(() => {
if (!isActive || !orderedIds || !IS_APP) { if (!isActive || isSaved || !orderedIds || !IS_APP) {
return undefined; return undefined;
} }
@ -134,13 +138,13 @@ const ChatList: FC<OwnProps> = ({
const [, digit] = e.code.match(/Digit(\d)/) || []; const [, digit] = e.code.match(/Digit(\d)/) || [];
if (!digit || RESERVED_HOTKEYS.has(digit)) return; if (!digit || RESERVED_HOTKEYS.has(digit)) return;
const isArchiveInList = shouldDisplayArchive && !archiveSettings.isMinimized; const isArchiveInList = shouldDisplayArchive && archiveSettings && !archiveSettings.isMinimized;
const shift = isArchiveInList ? -1 : 0; const shift = isArchiveInList ? -1 : 0;
const position = Number(digit) + shift - 1; const position = Number(digit) + shift - 1;
if (isArchiveInList && position === -1) { if (isArchiveInList && position === -1) {
onLeftColumnContentChange(LeftColumnContent.Archived); onLeftColumnContentChange?.(LeftColumnContent.Archived);
return; return;
} }
@ -155,7 +159,10 @@ const ChatList: FC<OwnProps> = ({
return () => { return () => {
document.removeEventListener('keydown', handleKeyDown); document.removeEventListener('keydown', handleKeyDown);
}; };
}, [archiveSettings, isActive, onLeftColumnContentChange, openChat, openNextChat, orderedIds, shouldDisplayArchive]); }, [
archiveSettings, isSaved, isActive, onLeftColumnContentChange, openChat, openNextChat, orderedIds,
shouldDisplayArchive,
]);
const { observe } = useIntersectionObserver({ const { observe } = useIntersectionObserver({
rootRef: containerRef, rootRef: containerRef,
@ -163,7 +170,7 @@ const ChatList: FC<OwnProps> = ({
}); });
const handleArchivedClick = useLastCallback(() => { const handleArchivedClick = useLastCallback(() => {
onLeftColumnContentChange(LeftColumnContent.Archived); onLeftColumnContentChange!(LeftColumnContent.Archived);
closeForumPanel(); closeForumPanel();
}); });
@ -199,7 +206,7 @@ const ChatList: FC<OwnProps> = ({
toggleStoryRibbon({ isShown: false, isArchived }); toggleStoryRibbon({ isShown: false, isArchived });
}); });
const renderedOverflowTrigger = useTopOverscroll(containerRef, handleShowStoryRibbon, handleHideStoryRibbon); const renderedOverflowTrigger = useTopOverscroll(containerRef, handleShowStoryRibbon, handleHideStoryRibbon, isSaved);
function renderChats() { function renderChats() {
const viewportOffset = orderedIds!.indexOf(viewportIds![0]); const viewportOffset = orderedIds!.indexOf(viewportIds![0]);
@ -213,10 +220,11 @@ const ChatList: FC<OwnProps> = ({
return ( return (
<Chat <Chat
key={id} key={id}
teactOrderKey={isPinned ? i : getOrderKey(id)} teactOrderKey={isPinned ? i : getOrderKey(id, isSaved)}
chatId={id} chatId={id}
isPinned={isPinned} isPinned={isPinned}
folderId={folderId} folderId={folderId}
isSavedDialog={isSaved}
animationType={getAnimationType(id)} animationType={getAnimationType(id)}
orderDiff={orderDiffById[id]} orderDiff={orderDiffById[id]}
offsetTop={offsetTop} offsetTop={offsetTop}
@ -229,7 +237,7 @@ const ChatList: FC<OwnProps> = ({
return ( return (
<InfiniteScroll <InfiniteScroll
className={buildClassName('chat-list custom-scroll', isForumPanelOpen && 'forum-panel-open')} className={buildClassName('chat-list custom-scroll', isForumPanelOpen && 'forum-panel-open', className)}
ref={containerRef} ref={containerRef}
items={viewportIds} items={viewportIds}
itemSelector=".ListItem:not(.chat-item-archive)" itemSelector=".ListItem:not(.chat-item-archive)"
@ -257,13 +265,13 @@ const ChatList: FC<OwnProps> = ({
)} )}
{viewportIds?.length ? ( {viewportIds?.length ? (
renderChats() renderChats()
) : viewportIds && !viewportIds.length ? ( ) : viewportIds && !viewportIds.length && !isSaved ? (
( (
<EmptyFolder <EmptyFolder
folderId={folderId} folderId={folderId}
folderType={folderType} folderType={folderType}
foldersDispatch={foldersDispatch} foldersDispatch={foldersDispatch!}
onSettingsScreenSelect={onSettingsScreenSelect} onSettingsScreenSelect={onSettingsScreenSelect!}
/> />
) )
) : ( ) : (

View File

@ -18,7 +18,7 @@ import styles from './EmptyFolder.module.scss';
type OwnProps = { type OwnProps = {
folderId?: number; folderId?: number;
folderType: 'all' | 'archived' | 'folder'; folderType: 'all' | 'archived' | 'saved' | 'folder';
foldersDispatch: FolderEditDispatch; foldersDispatch: FolderEditDispatch;
onSettingsScreenSelect: (screen: SettingsScreens) => void; onSettingsScreenSelect: (screen: SettingsScreens) => void;
}; };

View File

@ -288,7 +288,7 @@ export default memo(withGlobal<OwnProps>(
return { return {
chat, chat,
currentTopicId: chatId === currentChatId ? currentThreadId : undefined, currentTopicId: chatId === currentChatId ? Number(currentThreadId) : undefined,
withInterfaceAnimations: selectCanAnimateInterface(global), withInterfaceAnimations: selectCanAnimateInterface(global),
}; };
}, },

View File

@ -58,6 +58,7 @@ export default function useChatListEntry({
orderDiff, orderDiff,
withInterfaceAnimations, withInterfaceAnimations,
isTopic, isTopic,
isSavedDialog,
}: { }: {
chat?: ApiChat; chat?: ApiChat;
lastMessage?: ApiMessage; lastMessage?: ApiMessage;
@ -71,6 +72,7 @@ export default function useChatListEntry({
actionTargetChatId?: string; actionTargetChatId?: string;
observeIntersection?: ObserveFn; observeIntersection?: ObserveFn;
isTopic?: boolean; isTopic?: boolean;
isSavedDialog?: boolean;
animationType: ChatAnimationTypes; animationType: ChatAnimationTypes;
orderDiff: number; orderDiff: number;
@ -102,14 +104,15 @@ export default function useChatListEntry({
}, [actionTargetUserIds]); }, [actionTargetUserIds]);
const renderLastMessageOrTyping = useCallback(() => { const renderLastMessageOrTyping = useCallback(() => {
if (typingStatus && lastMessage && typingStatus.timestamp > lastMessage.date * 1000) { if (!isSavedDialog && typingStatus && lastMessage && typingStatus.timestamp > lastMessage.date * 1000) {
return <TypingStatus typingStatus={typingStatus} />; return <TypingStatus typingStatus={typingStatus} />;
} }
const isDraftReplyToTopic = draft && draft.replyInfo?.replyToMsgId === lastMessageTopic?.id; const isDraftReplyToTopic = draft && draft.replyInfo?.replyToMsgId === lastMessageTopic?.id;
const isEmptyLocalReply = draft?.replyInfo && !draft.text && draft.isLocal; const isEmptyLocalReply = draft?.replyInfo && !draft.text && draft.isLocal;
const canDisplayDraft = !chat?.isForum && draft && !isEmptyLocalReply && (!isTopic || !isDraftReplyToTopic); const canDisplayDraft = !chat?.isForum && !isSavedDialog && draft && !isEmptyLocalReply
&& (!isTopic || !isDraftReplyToTopic);
if (canDisplayDraft) { if (canDisplayDraft) {
return ( return (
@ -169,7 +172,7 @@ export default function useChatListEntry({
<span className="colon">:</span> <span className="colon">:</span>
</> </>
)} )}
{lastMessage.forwardInfo && (<i className="icon icon-share-filled chat-prefix-icon" />)} {!isSavedDialog && lastMessage.forwardInfo && (<i className="icon icon-share-filled chat-prefix-icon" />)}
{lastMessage.replyInfo?.type === 'story' && (<i className="icon icon-story-reply chat-prefix-icon" />)} {lastMessage.replyInfo?.type === 'story' && (<i className="icon icon-story-reply chat-prefix-icon" />)}
{renderSummary(lang, lastMessage, observeIntersection, mediaBlobUrl || mediaThumbnail, isRoundVideo)} {renderSummary(lang, lastMessage, observeIntersection, mediaBlobUrl || mediaThumbnail, isRoundVideo)}
</p> </p>
@ -177,7 +180,7 @@ export default function useChatListEntry({
}, [ }, [
actionTargetChatId, actionTargetMessage, actionTargetUsers, chat, chatId, draft, isAction, actionTargetChatId, actionTargetMessage, actionTargetUsers, chat, chatId, draft, isAction,
isRoundVideo, isTopic, lang, lastMessage, lastMessageSender, lastMessageTopic, mediaBlobUrl, mediaThumbnail, isRoundVideo, isTopic, lang, lastMessage, lastMessageSender, lastMessageTopic, mediaBlobUrl, mediaThumbnail,
observeIntersection, typingStatus, observeIntersection, typingStatus, isSavedDialog,
]); ]);
function renderSubtitle() { function renderSubtitle() {

View File

@ -2,11 +2,10 @@ import type { FC } from '../../../lib/teact/teact';
import React, { memo, useCallback, useMemo } from '../../../lib/teact/teact'; import React, { memo, useCallback, useMemo } from '../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../global'; import { getActions, getGlobal, withGlobal } from '../../../global';
import type { ApiChat } from '../../../api/types'; import { filterUsersByName, isUserBot } from '../../../global/helpers';
import { filterUsersByName, isUserBot, sortChatIds } from '../../../global/helpers';
import { selectTabState } from '../../../global/selectors'; import { selectTabState } from '../../../global/selectors';
import { unique } from '../../../util/iteratees'; import { unique } from '../../../util/iteratees';
import sortChatIds from '../../common/helpers/sortChatIds';
import useHistoryBack from '../../../hooks/useHistoryBack'; import useHistoryBack from '../../../hooks/useHistoryBack';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
@ -25,7 +24,6 @@ export type OwnProps = {
}; };
type StateProps = { type StateProps = {
chatsById: Record<string, ApiChat>;
localContactIds?: string[]; localContactIds?: string[];
searchQuery?: string; searchQuery?: string;
isSearching?: boolean; isSearching?: boolean;
@ -40,7 +38,6 @@ const NewChatStep1: FC<OwnProps & StateProps> = ({
onSelectedMemberIdsChange, onSelectedMemberIdsChange,
onNextStep, onNextStep,
onReset, onReset,
chatsById,
localContactIds, localContactIds,
searchQuery, searchQuery,
isSearching, isSearching,
@ -80,11 +77,10 @@ const NewChatStep1: FC<OwnProps & StateProps> = ({
return !user.isSelf && (user.canBeInvitedToGroup || !isUserBot(user)); return !user.isSelf && (user.canBeInvitedToGroup || !isUserBot(user));
}), }),
chatsById,
false, false,
selectedMemberIds, selectedMemberIds,
); );
}, [localContactIds, chatsById, searchQuery, localUserIds, globalUserIds, selectedMemberIds]); }, [localContactIds, searchQuery, localUserIds, globalUserIds, selectedMemberIds]);
const handleNextStep = useCallback(() => { const handleNextStep = useCallback(() => {
setGlobalSearchQuery({ query: '' }); setGlobalSearchQuery({ query: '' });
@ -133,7 +129,6 @@ const NewChatStep1: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global): StateProps => { (global): StateProps => {
const { userIds: localContactIds } = global.contactList || {}; const { userIds: localContactIds } = global.contactList || {};
const { byId: chatsById } = global.chats;
const { const {
query: searchQuery, query: searchQuery,
@ -145,7 +140,6 @@ export default memo(withGlobal<OwnProps>(
const { userIds: localUserIds } = localResults || {}; const { userIds: localUserIds } = localResults || {};
return { return {
chatsById,
localContactIds, localContactIds,
searchQuery, searchQuery,
isSearching: fetchingStatus?.chats, isSearching: fetchingStatus?.chats,

View File

@ -11,7 +11,6 @@ import { ALL_FOLDER_ID } from '../../../config';
import { import {
filterChatsByName, filterChatsByName,
filterUsersByName, filterUsersByName,
sortChatIds,
} from '../../../global/helpers'; } from '../../../global/helpers';
import { selectTabState } from '../../../global/selectors'; import { selectTabState } from '../../../global/selectors';
import { getOrderedIds } from '../../../util/folderManager'; import { getOrderedIds } from '../../../util/folderManager';
@ -19,6 +18,7 @@ import { unique } from '../../../util/iteratees';
import { MEMO_EMPTY_ARRAY } from '../../../util/memo'; import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
import { throttle } from '../../../util/schedulers'; import { throttle } from '../../../util/schedulers';
import { renderMessageSummary } from '../../common/helpers/renderMessageText'; import { renderMessageSummary } from '../../common/helpers/renderMessageText';
import sortChatIds from '../../common/helpers/sortChatIds';
import useAppLayout from '../../../hooks/useAppLayout'; import useAppLayout from '../../../hooks/useAppLayout';
import useHorizontalScroll from '../../../hooks/useHorizontalScroll'; import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
@ -147,8 +147,8 @@ const ChatResults: FC<OwnProps & StateProps> = ({
].filter((accountPeerId) => !localPeerIds.includes(accountPeerId))); ].filter((accountPeerId) => !localPeerIds.includes(accountPeerId)));
return [ return [
...sortChatIds(localPeerIds, chatsById, undefined, currentUserId ? [currentUserId] : undefined), ...sortChatIds(localPeerIds, undefined, currentUserId ? [currentUserId] : undefined),
...sortChatIds(accountPeerIds, chatsById), ...sortChatIds(accountPeerIds),
]; ];
}, [searchQuery, currentUserId, contactIds, lang, accountChatIds, accountUserIds, chatsById]); }, [searchQuery, currentUserId, contactIds, lang, accountChatIds, accountUserIds, chatsById]);
@ -161,10 +161,9 @@ const ChatResults: FC<OwnProps & StateProps> = ({
return sortChatIds( return sortChatIds(
unique([...globalChatIds, ...globalUserIds]), unique([...globalChatIds, ...globalUserIds]),
chatsById,
true, true,
); );
}, [chatsById, globalChatIds, globalUserIds, searchQuery]); }, [globalChatIds, globalUserIds, searchQuery]);
const foundMessages = useMemo(() => { const foundMessages = useMemo(() => {
if ((!searchQuery && !searchDate) || !foundIds || foundIds.length === 0) { if ((!searchQuery && !searchDate) || !foundIds || foundIds.length === 0) {

View File

@ -5,6 +5,7 @@ import React, {
import { getActions } from '../../global'; import { getActions } from '../../global';
import type { TabState } from '../../global/types'; import type { TabState } from '../../global/types';
import type { ThreadId } from '../../types';
import useFlag from '../../hooks/useFlag'; import useFlag from '../../hooks/useFlag';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
@ -33,7 +34,7 @@ const DraftRecipientPicker: FC<OwnProps> = ({
} }
}, [isOpen, markIsShown]); }, [isOpen, markIsShown]);
const handleSelectRecipient = useCallback((recipientId: string, threadId?: number) => { const handleSelectRecipient = useCallback((recipientId: string, threadId?: ThreadId) => {
openChatWithDraft({ openChatWithDraft({
chatId: recipientId, chatId: recipientId,
threadId, threadId,

View File

@ -4,6 +4,8 @@ import React, {
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../global'; import { getActions, getGlobal, withGlobal } from '../../global';
import type { ThreadId } from '../../types';
import { getChatTitle, getUserFirstOrLastName, isUserId } from '../../global/helpers'; import { getChatTitle, getUserFirstOrLastName, isUserId } from '../../global/helpers';
import { selectChat, selectTabState, selectUser } from '../../global/selectors'; import { selectChat, selectTabState, selectUser } from '../../global/selectors';
@ -47,7 +49,7 @@ const ForwardRecipientPicker: FC<OwnProps & StateProps> = ({
} }
}, [isOpen, markIsShown]); }, [isOpen, markIsShown]);
const handleSelectRecipient = useCallback((recipientId: string, threadId?: number) => { const handleSelectRecipient = useCallback((recipientId: string, threadId?: ThreadId) => {
const isSelf = recipientId === currentUserId; const isSelf = recipientId === currentUserId;
if (isStory) { if (isStory) {
forwardStory({ toChatId: recipientId }); forwardStory({ toChatId: recipientId });
@ -82,7 +84,7 @@ const ForwardRecipientPicker: FC<OwnProps & StateProps> = ({
forwardToSavedMessages(); forwardToSavedMessages();
showNotification({ message }); showNotification({ message });
} else { } else {
setForwardChatOrTopic({ chatId: recipientId, topicId: threadId }); setForwardChatOrTopic({ chatId: recipientId, topicId: Number(threadId) });
} }
}, [currentUserId, isManyMessages, isStory, lang]); }, [currentUserId, isManyMessages, isStory, lang]);

View File

@ -227,6 +227,7 @@ const Main: FC<OwnProps & StateProps> = ({
isSynced, isSynced,
inviteViaLinkModal, inviteViaLinkModal,
oneTimeMediaModal, oneTimeMediaModal,
currentUserId,
}) => { }) => {
const { const {
initMain, initMain,
@ -420,7 +421,7 @@ const Main: FC<OwnProps & StateProps> = ({
}, []); }, []);
useEffect(() => { useEffect(() => {
const parsedLocationHash = parseLocationHash(); const parsedLocationHash = parseLocationHash(currentUserId);
if (!parsedLocationHash) return; if (!parsedLocationHash) return;
openThread({ openThread({
@ -428,7 +429,7 @@ const Main: FC<OwnProps & StateProps> = ({
threadId: parsedLocationHash.threadId, threadId: parsedLocationHash.threadId,
type: parsedLocationHash.type, type: parsedLocationHash.type,
}); });
}, []); }, [currentUserId]);
// Restore Transition slide class after async rendering // Restore Transition slide class after async rendering
useLayoutEffect(() => { useLayoutEffect(() => {

View File

@ -86,7 +86,9 @@ const PREMIUM_BOTTOM_VIDEOS: string[] = [
'translations', 'translations',
]; ];
type ApiLimitTypeWithoutUpload = Exclude<ApiLimitType, 'uploadMaxFileparts' | 'chatlistInvites' | 'chatlistJoined'>; type ApiLimitTypeWithoutUpload = Exclude<ApiLimitType,
'uploadMaxFileparts' | 'chatlistInvites' | 'chatlistJoined' | 'savedDialogsPinned'
>;
const LIMITS_ORDER: ApiLimitTypeWithoutUpload[] = [ const LIMITS_ORDER: ApiLimitTypeWithoutUpload[] = [
'channels', 'channels',

View File

@ -31,6 +31,7 @@ const LIMIT_DESCRIPTION: Record<ApiLimitTypeWithModal, string> = {
channels: 'LimitReachedCommunities', channels: 'LimitReachedCommunities',
chatlistInvites: 'LimitReachedFolderLinks', chatlistInvites: 'LimitReachedFolderLinks',
chatlistJoined: 'LimitReachedSharedFolders', chatlistJoined: 'LimitReachedSharedFolders',
savedDialogsPinned: 'LimitReachedPinSavedDialogs',
}; };
const LIMIT_DESCRIPTION_BLOCKED: Record<ApiLimitTypeWithModal, string> = { const LIMIT_DESCRIPTION_BLOCKED: Record<ApiLimitTypeWithModal, string> = {
@ -42,6 +43,7 @@ const LIMIT_DESCRIPTION_BLOCKED: Record<ApiLimitTypeWithModal, string> = {
channels: 'LimitReachedCommunitiesLocked', channels: 'LimitReachedCommunitiesLocked',
chatlistInvites: 'LimitReachedFolderLinksLocked', chatlistInvites: 'LimitReachedFolderLinksLocked',
chatlistJoined: 'LimitReachedSharedFoldersLocked', chatlistJoined: 'LimitReachedSharedFoldersLocked',
savedDialogsPinned: 'LimitReachedPinSavedDialogsLocked',
}; };
const LIMIT_DESCRIPTION_PREMIUM: Record<ApiLimitTypeWithModal, string> = { const LIMIT_DESCRIPTION_PREMIUM: Record<ApiLimitTypeWithModal, string> = {
@ -53,6 +55,7 @@ const LIMIT_DESCRIPTION_PREMIUM: Record<ApiLimitTypeWithModal, string> = {
channels: 'LimitReachedCommunitiesPremium', channels: 'LimitReachedCommunitiesPremium',
chatlistInvites: 'LimitReachedFolderLinksPremium', chatlistInvites: 'LimitReachedFolderLinksPremium',
chatlistJoined: 'LimitReachedSharedFoldersPremium', chatlistJoined: 'LimitReachedSharedFoldersPremium',
savedDialogsPinned: 'LimitReachedPinSavedDialogsPremium',
}; };
const LIMIT_ICON: Record<ApiLimitTypeWithModal, IconName> = { const LIMIT_ICON: Record<ApiLimitTypeWithModal, IconName> = {
@ -64,6 +67,7 @@ const LIMIT_ICON: Record<ApiLimitTypeWithModal, IconName> = {
channels: 'chats-badge', channels: 'chats-badge',
chatlistInvites: 'link-badge', chatlistInvites: 'link-badge',
chatlistJoined: 'folder-badge', chatlistJoined: 'folder-badge',
savedDialogsPinned: 'pin-badge',
}; };
const LIMIT_VALUE_FORMATTER: Partial<Record<ApiLimitTypeWithModal, (...args: any[]) => string>> = { const LIMIT_VALUE_FORMATTER: Partial<Record<ApiLimitTypeWithModal, (...args: any[]) => string>> = {

View File

@ -7,7 +7,7 @@ import { getActions, withGlobal } from '../../global';
import type { import type {
ApiMessage, ApiPeer, ApiPhoto, ApiUser, ApiMessage, ApiPeer, ApiPhoto, ApiUser,
} from '../../api/types'; } from '../../api/types';
import { MediaViewerOrigin } from '../../types'; import { MediaViewerOrigin, type ThreadId } from '../../types';
import { ANIMATION_END_DELAY } from '../../config'; import { ANIMATION_END_DELAY } from '../../config';
import { getChatMediaMessageIds, isChatAdmin, isUserId } from '../../global/helpers'; import { getChatMediaMessageIds, isChatAdmin, isUserId } from '../../global/helpers';
@ -57,7 +57,7 @@ import './MediaViewer.scss';
type StateProps = { type StateProps = {
chatId?: string; chatId?: string;
threadId?: number; threadId?: ThreadId;
mediaId?: number; mediaId?: number;
senderId?: string; senderId?: string;
isChatWithSelf?: boolean; isChatWithSelf?: boolean;

View File

@ -5,7 +5,7 @@ import { withGlobal } from '../../global';
import type { import type {
ApiDimensions, ApiMessage, ApiPeer, ApiDimensions, ApiMessage, ApiPeer,
} from '../../api/types'; } from '../../api/types';
import { MediaViewerOrigin } from '../../types'; import { MediaViewerOrigin, type ThreadId } from '../../types';
import { import {
selectChat, selectChatMessage, selectIsMessageProtected, selectScheduledMessage, selectTabState, selectUser, selectChat, selectChatMessage, selectIsMessageProtected, selectScheduledMessage, selectTabState, selectUser,
@ -31,7 +31,7 @@ import './MediaViewerContent.scss';
type OwnProps = { type OwnProps = {
mediaId?: number; mediaId?: number;
chatId?: string; chatId?: string;
threadId?: number; threadId?: ThreadId;
avatarOwnerId?: string; avatarOwnerId?: string;
origin?: MediaViewerOrigin; origin?: MediaViewerOrigin;
isActive?: boolean; isActive?: boolean;
@ -45,7 +45,7 @@ type StateProps = {
chatId?: string; chatId?: string;
mediaId?: number; mediaId?: number;
senderId?: string; senderId?: string;
threadId?: number; threadId?: ThreadId;
avatarOwner?: ApiPeer; avatarOwner?: ApiPeer;
message?: ApiMessage; message?: ApiMessage;
origin?: MediaViewerOrigin; origin?: MediaViewerOrigin;

View File

@ -3,7 +3,7 @@ import React, {
memo, useEffect, useLayoutEffect, useRef, useState, memo, useEffect, useLayoutEffect, useRef, useState,
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import type { MediaViewerOrigin } from '../../types'; import type { MediaViewerOrigin, ThreadId } from '../../types';
import type { RealTouchEvent } from '../../util/captureEvents'; import type { RealTouchEvent } from '../../util/captureEvents';
import { animateNumber, timingFunctions } from '../../util/animation'; import { animateNumber, timingFunctions } from '../../util/animation';
@ -46,7 +46,7 @@ type OwnProps = {
isOpen?: boolean; isOpen?: boolean;
selectMedia: (id?: number) => void; selectMedia: (id?: number) => void;
chatId?: string; chatId?: string;
threadId?: number; threadId?: ThreadId;
avatarOwnerId?: string; avatarOwnerId?: string;
origin?: MediaViewerOrigin; origin?: MediaViewerOrigin;
withAnimation?: boolean; withAnimation?: boolean;

View File

@ -9,7 +9,7 @@ import type {
} from '../../api/types'; } from '../../api/types';
import type { MessageListType } from '../../global/types'; import type { MessageListType } from '../../global/types';
import type { ObserveFn } from '../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../hooks/useIntersectionObserver';
import type { FocusDirection } from '../../types'; import type { FocusDirection, ThreadId } from '../../types';
import type { PinnedIntersectionChangedCallback } from './hooks/usePinnedMessage'; import type { PinnedIntersectionChangedCallback } from './hooks/usePinnedMessage';
import { import {
@ -45,7 +45,7 @@ import ContextMenuContainer from './message/ContextMenuContainer.async';
type OwnProps = { type OwnProps = {
message: ApiMessage; message: ApiMessage;
threadId?: number; threadId?: ThreadId;
messageListType?: MessageListType; messageListType?: MessageListType;
observeIntersectionForReading?: ObserveFn; observeIntersectionForReading?: ObserveFn;
observeIntersectionForLoading?: ObserveFn; observeIntersectionForLoading?: ObserveFn;

View File

@ -6,7 +6,7 @@ import type { ApiSticker, ApiUpdateConnectionStateType } from '../../api/types';
import type { MessageList } from '../../global/types'; import type { MessageList } from '../../global/types';
import { getPeerIdDividend } from '../../global/helpers'; import { getPeerIdDividend } from '../../global/helpers';
import { selectChat, selectCurrentMessageList } from '../../global/selectors'; import { selectChat, selectChatLastMessage, selectCurrentMessageList } from '../../global/selectors';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../hooks/useLastCallback';
@ -101,10 +101,12 @@ export default memo(withGlobal<OwnProps>(
return {}; return {};
} }
const lastMessage = selectChatLastMessage(global, chat.id);
return { return {
sticker, sticker,
lastUnreadMessageId: chat.lastMessage && chat.lastMessage.id !== chat.lastReadInboxMessageId lastUnreadMessageId: lastMessage && lastMessage.id !== chat.lastReadInboxMessageId
? chat.lastMessage.id ? lastMessage.id
: undefined, : undefined,
connectionState: global.connectionState, connectionState: global.connectionState,
currentMessageList: selectCurrentMessageList(global), currentMessageList: selectCurrentMessageList(global),

View File

@ -5,13 +5,14 @@ import React, {
import { getActions, withGlobal } from '../../global'; import { getActions, withGlobal } from '../../global';
import type { MessageListType } from '../../global/types'; import type { MessageListType } from '../../global/types';
import type { IAnchorPosition } from '../../types'; import type { IAnchorPosition, ThreadId } from '../../types';
import { MAIN_THREAD_ID } from '../../api/types'; import { MAIN_THREAD_ID } from '../../api/types';
import { ManagementScreens } from '../../types'; import { ManagementScreens } from '../../types';
import { requestMeasure, requestNextMutation } from '../../lib/fasterdom/fasterdom'; import { requestMeasure, requestNextMutation } from '../../lib/fasterdom/fasterdom';
import { import {
getHasAdminRight, getHasAdminRight,
isAnonymousForwardsChat,
isChatBasicGroup, isChatChannel, isChatSuperGroup, isUserId, isChatBasicGroup, isChatChannel, isChatSuperGroup, isUserId,
} from '../../global/helpers'; } from '../../global/helpers';
import { import {
@ -44,7 +45,7 @@ import HeaderMenuContainer from './HeaderMenuContainer.async';
interface OwnProps { interface OwnProps {
chatId: string; chatId: string;
threadId: number; threadId: ThreadId;
messageListType: MessageListType; messageListType: MessageListType;
canExpandActions: boolean; canExpandActions: boolean;
isForForum?: boolean; isForForum?: boolean;
@ -485,7 +486,8 @@ export default memo(withGlobal<OwnProps>(
(isMainThread || chat.isForum) && (isChannel || isChatSuperGroup(chat)) && chat.isNotJoined, (isMainThread || chat.isForum) && (isChannel || isChatSuperGroup(chat)) && chat.isNotJoined,
); );
const canSearch = isMainThread || isDiscussionThread; const canSearch = isMainThread || isDiscussionThread;
const canCall = ARE_CALLS_SUPPORTED && isUserId(chat.id) && !isChatWithSelf && !bot; const canCall = ARE_CALLS_SUPPORTED && isUserId(chat.id) && !isChatWithSelf && !bot && !chat.isSupport
&& !isAnonymousForwardsChat(chat.id);
const canMute = isMainThread && !isChatWithSelf && !canSubscribe; const canMute = isMainThread && !isChatWithSelf && !canSubscribe;
const canLeave = isMainThread && !canSubscribe; const canLeave = isMainThread && !canSubscribe;
const canEnterVoiceChat = ARE_CALLS_SUPPORTED && isMainThread && chat.isCallActive; const canEnterVoiceChat = ARE_CALLS_SUPPORTED && isMainThread && chat.isCallActive;

View File

@ -5,7 +5,7 @@ import React, {
import { getActions, withGlobal } from '../../global'; import { getActions, withGlobal } from '../../global';
import type { ApiBotCommand, ApiChat } from '../../api/types'; import type { ApiBotCommand, ApiChat } from '../../api/types';
import type { IAnchorPosition } from '../../types'; import type { IAnchorPosition, ThreadId } from '../../types';
import type { IconName } from '../../types/icons'; import type { IconName } from '../../types/icons';
import { MAIN_THREAD_ID } from '../../api/types'; import { MAIN_THREAD_ID } from '../../api/types';
@ -71,7 +71,7 @@ const BOT_BUTTONS: Record<string, { icon: IconName; label: string }> = {
export type OwnProps = { export type OwnProps = {
chatId: string; chatId: string;
threadId: number; threadId: ThreadId;
isOpen: boolean; isOpen: boolean;
withExtraActions: boolean; withExtraActions: boolean;
anchor: IAnchorPosition; anchor: IAnchorPosition;
@ -276,7 +276,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
}); });
const handleEditTopicClick = useLastCallback(() => { const handleEditTopicClick = useLastCallback(() => {
openEditTopicPanel({ chatId, topicId: threadId }); openEditTopicPanel({ chatId, topicId: Number(threadId) });
setShouldCloseFast(!isRightColumnShown); setShouldCloseFast(!isRightColumnShown);
closeMenu(); closeMenu();
}); });

View File

@ -77,7 +77,8 @@
} }
&.select-mode-active, &.select-mode-active,
&.type-pinned { &.type-pinned,
&.saved-dialog {
margin-bottom: 0; margin-bottom: 0;
.last-in-list { .last-in-list {

View File

@ -15,16 +15,19 @@ import type { MessageListType } from '../../global/types';
import type { Signal } from '../../util/signals'; import type { Signal } from '../../util/signals';
import type { PinnedIntersectionChangedCallback } from './hooks/usePinnedMessage'; import type { PinnedIntersectionChangedCallback } from './hooks/usePinnedMessage';
import { MAIN_THREAD_ID } from '../../api/types'; import { MAIN_THREAD_ID } from '../../api/types';
import { LoadMoreDirection } from '../../types'; import { LoadMoreDirection, type ThreadId } from '../../types';
import { import {
ANIMATION_END_DELAY, ANIMATION_END_DELAY,
ANONYMOUS_USER_ID,
MESSAGE_LIST_SLICE, MESSAGE_LIST_SLICE,
SERVICE_NOTIFICATIONS_USER_ID, SERVICE_NOTIFICATIONS_USER_ID,
} from '../../config'; } from '../../config';
import { forceMeasure, requestForcedReflow, requestMeasure } from '../../lib/fasterdom/fasterdom'; import { forceMeasure, requestForcedReflow, requestMeasure } from '../../lib/fasterdom/fasterdom';
import { import {
getIsSavedDialog,
getMessageHtmlId, getMessageHtmlId,
isAnonymousForwardsChat,
isChatChannel, isChatChannel,
isChatGroup, isChatGroup,
isChatWithRepliesBot, isChatWithRepliesBot,
@ -35,6 +38,7 @@ import {
selectBot, selectBot,
selectChat, selectChat,
selectChatFullInfo, selectChatFullInfo,
selectChatLastMessage,
selectChatMessages, selectChatMessages,
selectChatScheduledMessages, selectChatScheduledMessages,
selectCurrentMessageIds, selectCurrentMessageIds,
@ -80,7 +84,7 @@ import './MessageList.scss';
type OwnProps = { type OwnProps = {
chatId: string; chatId: string;
threadId: number; threadId: ThreadId;
type: MessageListType; type: MessageListType;
isComments?: boolean; isComments?: boolean;
canPost: boolean; canPost: boolean;
@ -101,6 +105,7 @@ type StateProps = {
isGroupChat?: boolean; isGroupChat?: boolean;
isChatWithSelf?: boolean; isChatWithSelf?: boolean;
isRepliesChat?: boolean; isRepliesChat?: boolean;
isAnonymousForwards?: boolean;
isCreator?: boolean; isCreator?: boolean;
isBot?: boolean; isBot?: boolean;
isSynced?: boolean; isSynced?: boolean;
@ -119,6 +124,7 @@ type StateProps = {
isServiceNotificationsChat?: boolean; isServiceNotificationsChat?: boolean;
isEmptyThread?: boolean; isEmptyThread?: boolean;
isForum?: boolean; isForum?: boolean;
currentUserId: string;
}; };
const MESSAGE_REACTIONS_POLLING_INTERVAL = 20 * 1000; const MESSAGE_REACTIONS_POLLING_INTERVAL = 20 * 1000;
@ -152,6 +158,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
isReady, isReady,
isChatWithSelf, isChatWithSelf,
isRepliesChat, isRepliesChat,
isAnonymousForwards,
isCreator, isCreator,
isBot, isBot,
messageIds, messageIds,
@ -171,8 +178,9 @@ const MessageList: FC<OwnProps & StateProps> = ({
topic, topic,
noMessageSendingAnimation, noMessageSendingAnimation,
isServiceNotificationsChat, isServiceNotificationsChat,
onPinnedIntersectionChange, currentUserId,
getForceNextPinnedInHeader, getForceNextPinnedInHeader,
onPinnedIntersectionChange,
}) => { }) => {
const { const {
loadViewportMessages, setScrollOffset, loadSponsoredMessages, loadMessageReactions, copyMessagesByIds, loadViewportMessages, setScrollOffset, loadSponsoredMessages, loadMessageReactions, copyMessagesByIds,
@ -199,6 +207,9 @@ const MessageList: FC<OwnProps & StateProps> = ({
const isScrollTopJustUpdatedRef = useRef(false); const isScrollTopJustUpdatedRef = useRef(false);
const shouldAnimateAppearanceRef = useRef(Boolean(lastMessage)); const shouldAnimateAppearanceRef = useRef(Boolean(lastMessage));
const isSavedDialog = getIsSavedDialog(chatId, threadId, currentUserId);
const hasOpenChatButton = isSavedDialog && threadId !== ANONYMOUS_USER_ID;
const areMessagesLoaded = Boolean(messageIds); const areMessagesLoaded = Boolean(messageIds);
useSyncEffect(() => { useSyncEffect(() => {
@ -250,7 +261,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
? groupMessages( ? groupMessages(
orderBy(listedMessages, orderRule), orderBy(listedMessages, orderRule),
memoUnreadDividerBeforeIdRef.current, memoUnreadDividerBeforeIdRef.current,
!isForum ? threadId : undefined, !isForum ? Number(threadId) : undefined,
isChatWithSelf, isChatWithSelf,
) )
: undefined; : undefined;
@ -547,9 +558,9 @@ const MessageList: FC<OwnProps & StateProps> = ({
}, [isSelectModeActive]); }, [isSelectModeActive]);
const isPrivate = Boolean(chatId && isUserId(chatId)); const isPrivate = Boolean(chatId && isUserId(chatId));
const withUsers = Boolean((!isPrivate && !isChannelChat) || isChatWithSelf || isRepliesChat); const withUsers = Boolean((!isPrivate && !isChannelChat) || isChatWithSelf || isRepliesChat || isAnonymousForwards);
const noAvatars = Boolean(!withUsers || isChannelChat); const noAvatars = Boolean(!withUsers || isChannelChat);
const shouldRenderGreeting = isUserId(chatId) && !isChatWithSelf && !isBot const shouldRenderGreeting = isUserId(chatId) && !isChatWithSelf && !isBot && !isAnonymousForwards
&& ( && (
( (
!messageGroups && !lastMessage && messageIds !messageGroups && !lastMessage && messageIds
@ -575,6 +586,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
isSelectModeActive && 'select-mode-active', isSelectModeActive && 'select-mode-active',
isScrolled && 'scrolled', isScrolled && 'scrolled',
!isReady && 'is-animating', !isReady && 'is-animating',
hasOpenChatButton && 'saved-dialog',
); );
const hasMessages = (messageIds && messageGroups) || lastMessage; const hasMessages = (messageIds && messageGroups) || lastMessage;
@ -610,6 +622,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
chatId={chatId} chatId={chatId}
isComments={isComments} isComments={isComments}
isChannelChat={isChannelChat} isChannelChat={isChannelChat}
isSavedDialog={isSavedDialog}
messageIds={messageIds || [lastMessage!.id]} messageIds={messageIds || [lastMessage!.id]}
messageGroups={messageGroups || groupMessages([lastMessage!])} messageGroups={messageGroups || groupMessages([lastMessage!])}
getContainerHeight={getContainerHeight} getContainerHeight={getContainerHeight}
@ -642,9 +655,10 @@ const MessageList: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global, { chatId, threadId, type }): StateProps => { (global, { chatId, threadId, type }): StateProps => {
const currentUserId = global.currentUserId!;
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
if (!chat) { if (!chat) {
return {}; return { currentUserId };
} }
const messageIds = selectCurrentMessageIds(global, chatId, threadId, type); const messageIds = selectCurrentMessageIds(global, chatId, threadId, type);
@ -652,14 +666,17 @@ export default memo(withGlobal<OwnProps>(
? selectChatScheduledMessages(global, chatId) ? selectChatScheduledMessages(global, chatId)
: selectChatMessages(global, chatId); : selectChatMessages(global, chatId);
const isSavedDialog = getIsSavedDialog(chatId, threadId, currentUserId);
if ( if (
threadId !== MAIN_THREAD_ID && !chat?.isForum threadId !== MAIN_THREAD_ID && !isSavedDialog && !chat?.isForum
&& !(messagesById && threadId && messagesById[threadId]) && !(messagesById && threadId && messagesById[Number(threadId)])
) { ) {
return {}; return { currentUserId };
} }
const { isRestricted, restrictionReason, lastMessage } = chat; const { isRestricted, restrictionReason } = chat;
const lastMessage = selectChatLastMessage(global, chatId, isSavedDialog ? 'saved' : 'all');
const focusingId = selectFocusedMessageId(global, chatId); const focusingId = selectFocusedMessageId(global, chatId);
const withLastMessageWhenPreloading = ( const withLastMessageWhenPreloading = (
@ -683,6 +700,7 @@ export default memo(withGlobal<OwnProps>(
isCreator: chat.isCreator, isCreator: chat.isCreator,
isChatWithSelf: selectIsChatWithSelf(global, chatId), isChatWithSelf: selectIsChatWithSelf(global, chatId),
isRepliesChat: isChatWithRepliesBot(chatId), isRepliesChat: isChatWithRepliesBot(chatId),
isAnonymousForwards: isAnonymousForwardsChat(chatId),
isBot: Boolean(chatBot), isBot: Boolean(chatBot),
isSynced: global.isSynced, isSynced: global.isSynced,
messageIds, messageIds,
@ -697,6 +715,7 @@ export default memo(withGlobal<OwnProps>(
isServiceNotificationsChat: chatId === SERVICE_NOTIFICATIONS_USER_ID, isServiceNotificationsChat: chatId === SERVICE_NOTIFICATIONS_USER_ID,
isForum: chat.isForum, isForum: chat.isForum,
isEmptyThread, isEmptyThread,
currentUserId,
...(withLastMessageWhenPreloading && { lastMessage }), ...(withLastMessageWhenPreloading && { lastMessage }),
}; };
}, },

View File

@ -4,6 +4,7 @@ import React, { memo } from '../../lib/teact/teact';
import { getActions } from '../../global'; import { getActions } from '../../global';
import type { MessageListType } from '../../global/types'; import type { MessageListType } from '../../global/types';
import type { ThreadId } from '../../types';
import type { Signal } from '../../util/signals'; import type { Signal } from '../../util/signals';
import type { MessageDateGroup } from './helpers/groupMessages'; import type { MessageDateGroup } from './helpers/groupMessages';
import type { PinnedIntersectionChangedCallback } from './hooks/usePinnedMessage'; import type { PinnedIntersectionChangedCallback } from './hooks/usePinnedMessage';
@ -33,7 +34,7 @@ import MessageListBotInfo from './MessageListBotInfo';
interface OwnProps { interface OwnProps {
isCurrentUserPremium?: boolean; isCurrentUserPremium?: boolean;
chatId: string; chatId: string;
threadId: number; threadId: ThreadId;
messageIds: number[]; messageIds: number[];
messageGroups: MessageDateGroup[]; messageGroups: MessageDateGroup[];
getContainerHeight: Signal<number | undefined>; getContainerHeight: Signal<number | undefined>;
@ -54,6 +55,7 @@ interface OwnProps {
isSchedule: boolean; isSchedule: boolean;
shouldRenderBotInfo?: boolean; shouldRenderBotInfo?: boolean;
noAppearanceAnimation: boolean; noAppearanceAnimation: boolean;
isSavedDialog?: boolean;
onFabToggle: AnyToVoidFunction; onFabToggle: AnyToVoidFunction;
onNotchToggle: AnyToVoidFunction; onNotchToggle: AnyToVoidFunction;
onPinnedIntersectionChange: PinnedIntersectionChangedCallback; onPinnedIntersectionChange: PinnedIntersectionChangedCallback;
@ -85,6 +87,7 @@ const MessageListContent: FC<OwnProps> = ({
isSchedule, isSchedule,
shouldRenderBotInfo, shouldRenderBotInfo,
noAppearanceAnimation, noAppearanceAnimation,
isSavedDialog,
onFabToggle, onFabToggle,
onNotchToggle, onNotchToggle,
onPinnedIntersectionChange, onPinnedIntersectionChange,
@ -92,6 +95,7 @@ const MessageListContent: FC<OwnProps> = ({
const { openHistoryCalendar } = getActions(); const { openHistoryCalendar } = getActions();
const getIsReady = useDerivedSignal(isReady); const getIsReady = useDerivedSignal(isReady);
const areDatesClickable = !isSavedDialog && !isSchedule;
const { const {
observeIntersectionForReading, observeIntersectionForReading,
@ -162,7 +166,7 @@ const MessageListContent: FC<OwnProps> = ({
message={message} message={message}
threadId={threadId} threadId={threadId}
messageListType={type} messageListType={type}
isInsideTopic={Boolean(threadId && threadId !== MAIN_THREAD_ID)} isInsideTopic={Boolean(threadId && threadId !== MAIN_THREAD_ID && !isSavedDialog)}
observeIntersectionForReading={observeIntersectionForReading} observeIntersectionForReading={observeIntersectionForReading}
observeIntersectionForLoading={observeIntersectionForLoading} observeIntersectionForLoading={observeIntersectionForLoading}
observeIntersectionForPlaying={observeIntersectionForPlaying} observeIntersectionForPlaying={observeIntersectionForPlaying}
@ -261,10 +265,10 @@ const MessageListContent: FC<OwnProps> = ({
teactFastList teactFastList
> >
<div <div
className={buildClassName('sticky-date', !isSchedule && 'interactive')} className={buildClassName('sticky-date', areDatesClickable && 'interactive')}
key="date-header" key="date-header"
onMouseDown={preventMessageInputBlur} onMouseDown={preventMessageInputBlur}
onClick={!isSchedule ? () => openHistoryCalendar({ selectedAt: dateGroup.datetime }) : undefined} onClick={areDatesClickable ? () => openHistoryCalendar({ selectedAt: dateGroup.datetime }) : undefined}
> >
<span dir="auto"> <span dir="auto">
{isSchedule && dateGroup.originalDate === SCHEDULED_WHEN_ONLINE && ( {isSchedule && dateGroup.originalDate === SCHEDULED_WHEN_ONLINE && (

View File

@ -254,8 +254,7 @@
.Composer, .Composer,
.MessageSelectToolbar, .MessageSelectToolbar,
.unpin-all-button, .composer-button,
.join-subscribe-button,
.messaging-disabled { .messaging-disabled {
width: 100%; width: 100%;
display: flex; display: flex;
@ -264,8 +263,7 @@
} }
.MessageSelectToolbar-inner, .MessageSelectToolbar-inner,
.unpin-all-button, .composer-button,
.join-subscribe-button,
.messaging-disabled { .messaging-disabled {
.mask-image-disabled & { .mask-image-disabled & {
box-shadow: 0 0.25rem 0.5rem 0.125rem var(--color-default-shadow); box-shadow: 0 0.25rem 0.5rem 0.125rem var(--color-default-shadow);
@ -310,11 +308,11 @@
} }
} }
.join-subscribe-button, .composer-button {
.unpin-all-button {
height: 3.5rem; height: 3.5rem;
transform: scaleX(1); transform: scaleX(1);
transition: transform var(--select-transition), background-color 0.15s, color 0.15s; transition: transform var(--select-transition), background-color 0.15s, color 0.15s;
color: var(--color-primary);
.select-mode-active + .middle-column-footer & { .select-mode-active + .middle-column-footer & {
box-shadow: none; box-shadow: none;

View File

@ -9,11 +9,12 @@ import type {
ActiveEmojiInteraction, ActiveEmojiInteraction,
MessageListType, MessageListType,
} from '../../global/types'; } from '../../global/types';
import type { ThemeKey } from '../../types'; import type { ThemeKey, ThreadId } from '../../types';
import { MAIN_THREAD_ID } from '../../api/types'; import { MAIN_THREAD_ID } from '../../api/types';
import { import {
ANIMATION_END_DELAY, ANIMATION_END_DELAY,
ANONYMOUS_USER_ID,
EDITABLE_INPUT_CSS_SELECTOR, EDITABLE_INPUT_CSS_SELECTOR,
EDITABLE_INPUT_ID, EDITABLE_INPUT_ID,
GENERAL_TOPIC_ID, GENERAL_TOPIC_ID,
@ -29,6 +30,7 @@ import {
getCanPostInChat, getCanPostInChat,
getForumComposerPlaceholder, getForumComposerPlaceholder,
getHasAdminRight, getHasAdminRight,
getIsSavedDialog,
getMessageSendingRestrictionReason, getMessageSendingRestrictionReason,
isChatChannel, isChatChannel,
isChatGroup, isChatGroup,
@ -101,7 +103,7 @@ interface OwnProps {
type StateProps = { type StateProps = {
chatId?: string; chatId?: string;
threadId?: number; threadId?: ThreadId;
isComments?: boolean; isComments?: boolean;
messageListType?: MessageListType; messageListType?: MessageListType;
chat?: ApiChat; chat?: ApiChat;
@ -145,6 +147,8 @@ type StateProps = {
topMessageId?: number; topMessageId?: number;
canUnpin?: boolean; canUnpin?: boolean;
canUnblock?: boolean; canUnblock?: boolean;
isSavedDialog?: boolean;
canShowOpenChatButton?: boolean;
}; };
function isImage(item: DataTransferItem) { function isImage(item: DataTransferItem) {
@ -201,6 +205,8 @@ function MiddleColumn({
topMessageId, topMessageId,
canUnpin, canUnpin,
canUnblock, canUnblock,
isSavedDialog,
canShowOpenChatButton,
}: OwnProps & StateProps) { }: OwnProps & StateProps) {
const { const {
openChat, openChat,
@ -378,6 +384,10 @@ function MiddleColumn({
setIsUnpinModalOpen(false); setIsUnpinModalOpen(false);
}); });
const handleOpenChatFromSaved = useLastCallback(() => {
openChat({ id: String(threadId) });
});
const handleUnpinAllMessages = useLastCallback(() => { const handleUnpinAllMessages = useLastCallback(() => {
unpinAllMessages({ chatId: chatId!, threadId: threadId! }); unpinAllMessages({ chatId: chatId!, threadId: threadId! });
closeUnpinModal(); closeUnpinModal();
@ -465,12 +475,12 @@ function MiddleColumn({
}); });
const isMessagingDisabled = Boolean( const isMessagingDisabled = Boolean(
!isPinnedMessageList && !renderingCanPost && !renderingCanRestartBot && !renderingCanStartBot !isPinnedMessageList && !isSavedDialog && !renderingCanPost && !renderingCanRestartBot && !renderingCanStartBot
&& !renderingCanSubscribe && composerRestrictionMessage, && !renderingCanSubscribe && composerRestrictionMessage,
); );
const withMessageListBottomShift = Boolean( const withMessageListBottomShift = Boolean(
renderingCanRestartBot || renderingCanSubscribe || renderingShouldSendJoinRequest || renderingCanStartBot renderingCanRestartBot || renderingCanSubscribe || renderingShouldSendJoinRequest || renderingCanStartBot
|| isPinnedMessageList || renderingCanUnblock, || isPinnedMessageList || canShowOpenChatButton || renderingCanUnblock,
); );
const withExtraShift = Boolean(isMessagingDisabled || isSelectModeActive || isPinnedMessageList); const withExtraShift = Boolean(isMessagingDisabled || isSelectModeActive || isPinnedMessageList);
@ -563,7 +573,7 @@ function MiddleColumn({
size="tiny" size="tiny"
fluid fluid
color="secondary" color="secondary"
className="unpin-all-button" className="composer-button unpin-all-button"
onClick={handleOpenUnpinModal} onClick={handleOpenUnpinModal}
> >
<i className="icon icon-unpin" /> <i className="icon icon-unpin" />
@ -571,6 +581,19 @@ function MiddleColumn({
</Button> </Button>
</div> </div>
)} )}
{canShowOpenChatButton && (
<div className="middle-column-footer-button-container" dir={lang.isRtl ? 'rtl' : undefined}>
<Button
size="tiny"
fluid
color="secondary"
className="composer-button open-chat-button"
onClick={handleOpenChatFromSaved}
>
<span>{lang('SavedOpenChat')}</span>
</Button>
</div>
)}
{isMessagingDisabled && ( {isMessagingDisabled && (
<div className={messagingDisabledClassName}> <div className={messagingDisabledClassName}>
<div className="messaging-disabled-inner"> <div className="messaging-disabled-inner">
@ -588,7 +611,7 @@ function MiddleColumn({
size="tiny" size="tiny"
fluid fluid
ripple ripple
className="join-subscribe-button" className="composer-button join-subscribe-button"
onClick={handleSubscribeClick} onClick={handleSubscribeClick}
> >
{lang(renderingIsChannel ? 'ProfileJoinChannel' : 'ProfileJoinGroup')} {lang(renderingIsChannel ? 'ProfileJoinChannel' : 'ProfileJoinGroup')}
@ -601,7 +624,7 @@ function MiddleColumn({
size="tiny" size="tiny"
fluid fluid
ripple ripple
className="join-subscribe-button" className="composer-button join-subscribe-button"
onClick={handleSubscribeClick} onClick={handleSubscribeClick}
> >
{lang('ChannelJoinRequest')} {lang('ChannelJoinRequest')}
@ -614,7 +637,7 @@ function MiddleColumn({
size="tiny" size="tiny"
fluid fluid
ripple ripple
className="join-subscribe-button" className="composer-button join-subscribe-button"
onClick={handleStartBot} onClick={handleStartBot}
> >
{lang('BotStart')} {lang('BotStart')}
@ -627,7 +650,7 @@ function MiddleColumn({
size="tiny" size="tiny"
fluid fluid
ripple ripple
className="join-subscribe-button" className="composer-button join-subscribe-button"
onClick={handleRestartBot} onClick={handleRestartBot}
> >
{lang('BotRestart')} {lang('BotRestart')}
@ -640,7 +663,7 @@ function MiddleColumn({
size="tiny" size="tiny"
fluid fluid
ripple ripple
className="join-subscribe-button" className="composer-button join-subscribe-button"
onClick={handleUnblock} onClick={handleUnblock}
> >
{lang('Unblock')} {lang('Unblock')}
@ -763,8 +786,11 @@ export default memo(withGlobal<OwnProps>(
? selectChatMessage(global, audioChatId, audioMessageId) ? selectChatMessage(global, audioChatId, audioMessageId)
: undefined; : undefined;
const isCommentThread = threadId !== MAIN_THREAD_ID && !chat?.isForum; const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
const topMessageId = isCommentThread ? threadId : undefined; const canShowOpenChatButton = isSavedDialog && threadId !== ANONYMOUS_USER_ID;
const isCommentThread = threadId !== MAIN_THREAD_ID && !isSavedDialog && !chat?.isForum;
const topMessageId = isCommentThread ? Number(threadId) : undefined;
const canUnpin = chat && ( const canUnpin = chat && (
isPrivate || ( isPrivate || (
@ -787,7 +813,8 @@ export default memo(withGlobal<OwnProps>(
&& (!chat || canPost) && (!chat || canPost)
&& !isBotNotStarted && !isBotNotStarted
&& !(shouldJoinToSend && chat?.isNotJoined) && !(shouldJoinToSend && chat?.isNotJoined)
&& !shouldBlockSendInForum, && !shouldBlockSendInForum
&& !isSavedDialog,
isPinnedMessageList, isPinnedMessageList,
currentUserBannedRights: chat?.currentUserBannedRights, currentUserBannedRights: chat?.currentUserBannedRights,
defaultBannedRights: chat?.defaultBannedRights, defaultBannedRights: chat?.defaultBannedRights,
@ -807,6 +834,8 @@ export default memo(withGlobal<OwnProps>(
topMessageId, topMessageId,
canUnpin, canUnpin,
canUnblock, canUnblock,
isSavedDialog,
canShowOpenChatButton,
}; };
}, },
)(MiddleColumn)); )(MiddleColumn));

View File

@ -243,6 +243,17 @@
} }
} }
.saved-dialog-avatar {
position: absolute;
}
.overlay-avatar {
margin-left: 2.125rem;
.inner {
outline: 2px solid var(--color-background);
}
}
.status, .status,
.typing-status { .typing-status {
display: inline; display: inline;

View File

@ -10,7 +10,7 @@ import type {
import type { GlobalState, MessageListType } from '../../global/types'; import type { GlobalState, MessageListType } from '../../global/types';
import type { Signal } from '../../util/signals'; import type { Signal } from '../../util/signals';
import { MAIN_THREAD_ID } from '../../api/types'; import { MAIN_THREAD_ID } from '../../api/types';
import { StoryViewerOrigin } from '../../types'; import { StoryViewerOrigin, type ThreadId } from '../../types';
import { import {
EDITABLE_INPUT_CSS_SELECTOR, EDITABLE_INPUT_CSS_SELECTOR,
@ -24,6 +24,7 @@ import {
import { requestMutation } from '../../lib/fasterdom/fasterdom'; import { requestMutation } from '../../lib/fasterdom/fasterdom';
import { import {
getChatTitle, getChatTitle,
getIsSavedDialog,
getMessageKey, getMessageKey,
getSenderTitle, getSenderTitle,
isChatChannel, isChatChannel,
@ -83,7 +84,7 @@ const EMOJI_STATUS_SIZE = 22;
type OwnProps = { type OwnProps = {
chatId: string; chatId: string;
threadId: number; threadId: ThreadId;
messageListType: MessageListType; messageListType: MessageListType;
isComments?: boolean; isComments?: boolean;
isReady?: boolean; isReady?: boolean;
@ -98,6 +99,7 @@ type StateProps = {
pinnedMessageIds?: number[] | number; pinnedMessageIds?: number[] | number;
messagesById?: Record<number, ApiMessage>; messagesById?: Record<number, ApiMessage>;
canUnpin?: boolean; canUnpin?: boolean;
isSavedDialog?: boolean;
topMessageSender?: ApiPeer; topMessageSender?: ApiPeer;
typingStatus?: ApiTypingStatus; typingStatus?: ApiTypingStatus;
isSelectModeActive?: boolean; isSelectModeActive?: boolean;
@ -145,6 +147,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
getCurrentPinnedIndexes, getCurrentPinnedIndexes,
getLoadingPinnedId, getLoadingPinnedId,
emojiStatusSticker, emojiStatusSticker,
isSavedDialog,
onFocusPinnedMessage, onFocusPinnedMessage,
}) => { }) => {
const { const {
@ -352,7 +355,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
function renderInfo() { function renderInfo() {
if (messageListType === 'thread') { if (messageListType === 'thread') {
if (threadId === MAIN_THREAD_ID || chat?.isForum) { if (threadId === MAIN_THREAD_ID || isSavedDialog || chat?.isForum) {
return renderChatInfo(); return renderChatInfo();
} }
} }
@ -377,25 +380,30 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
} }
function renderChatInfo() { function renderChatInfo() {
// TODO Implement count
const savedMessagesStatus = isSavedDialog ? lang('SavedMessages') : undefined;
const realChatId = isSavedDialog ? String(threadId) : chatId;
return ( return (
<> <>
{(isLeftColumnHideable || currentTransitionKey > 0) && renderBackButton(shouldShowCloseButton, true)} {(isLeftColumnHideable || currentTransitionKey > 0) && renderBackButton(shouldShowCloseButton, !isSavedDialog)}
<div <div
className="chat-info-wrapper" className="chat-info-wrapper"
onClick={handleHeaderClick} onClick={handleHeaderClick}
onMouseDown={handleHeaderMouseDown} onMouseDown={handleHeaderMouseDown}
> >
{isUserId(chatId) ? ( {isUserId(realChatId) ? (
<PrivateChatInfo <PrivateChatInfo
key={chatId} key={realChatId}
userId={chatId} userId={realChatId}
typingStatus={typingStatus} typingStatus={typingStatus}
status={connectionStatusText} status={connectionStatusText || savedMessagesStatus}
withDots={Boolean(connectionStatusText)} withDots={Boolean(connectionStatusText)}
withFullInfo withFullInfo
withMediaViewer withMediaViewer
withStory={!isChatWithSelf} withStory={!isChatWithSelf}
withUpdatingStatus withUpdatingStatus
isSavedDialog={isSavedDialog}
storyViewerOrigin={StoryViewerOrigin.MiddleHeaderAvatar} storyViewerOrigin={StoryViewerOrigin.MiddleHeaderAvatar}
emojiStatusSize={EMOJI_STATUS_SIZE} emojiStatusSize={EMOJI_STATUS_SIZE}
noRtl noRtl
@ -403,16 +411,17 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
/> />
) : ( ) : (
<GroupChatInfo <GroupChatInfo
key={chatId} key={realChatId}
chatId={chatId} chatId={realChatId}
threadId={threadId} threadId={!isSavedDialog ? threadId : undefined}
typingStatus={typingStatus} typingStatus={typingStatus}
status={connectionStatusText} status={connectionStatusText || savedMessagesStatus}
withDots={Boolean(connectionStatusText)} withDots={Boolean(connectionStatusText)}
withMediaViewer={threadId === MAIN_THREAD_ID} withMediaViewer={threadId === MAIN_THREAD_ID}
withFullInfo={threadId === MAIN_THREAD_ID} withFullInfo={threadId === MAIN_THREAD_ID}
withUpdatingStatus withUpdatingStatus
withStory withStory
isSavedDialog={isSavedDialog}
storyViewerOrigin={StoryViewerOrigin.MiddleHeaderAvatar} storyViewerOrigin={StoryViewerOrigin.MiddleHeaderAvatar}
emojiStatusSize={EMOJI_STATUS_SIZE} emojiStatusSize={EMOJI_STATUS_SIZE}
onEmojiStatusClick={handleChannelStatusClick} onEmojiStatusClick={handleChannelStatusClick}
@ -552,6 +561,8 @@ export default memo(withGlobal<OwnProps>(
const emojiStatus = chat?.emojiStatus; const emojiStatus = chat?.emojiStatus;
const emojiStatusSticker = emojiStatus && global.customEmojis.byId[emojiStatus.documentId]; const emojiStatusSticker = emojiStatus && global.customEmojis.byId[emojiStatus.documentId];
const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
const state: StateProps = { const state: StateProps = {
typingStatus, typingStatus,
isLeftColumnShown, isLeftColumnShown,
@ -569,6 +580,7 @@ export default memo(withGlobal<OwnProps>(
isFetchingDifference: global.isFetchingDifference, isFetchingDifference: global.isFetchingDifference,
emojiStatusSticker, emojiStatusSticker,
hasButtonInHeader: canStartBot || canRestartBot || canSubscribe || shouldSendJoinRequest, hasButtonInHeader: canStartBot || canRestartBot || canSubscribe || shouldSendJoinRequest,
isSavedDialog,
}; };
const messagesById = selectChatMessages(global, chatId); const messagesById = selectChatMessages(global, chatId);
@ -576,8 +588,8 @@ export default memo(withGlobal<OwnProps>(
return state; return state;
} }
if (threadId !== MAIN_THREAD_ID && !chat?.isForum) { if (threadId !== MAIN_THREAD_ID && !isSavedDialog && !chat?.isForum) {
const pinnedMessageId = threadId; const pinnedMessageId = Number(threadId);
const message = pinnedMessageId ? selectChatMessage(global, chatId, pinnedMessageId) : undefined; const message = pinnedMessageId ? selectChatMessage(global, chatId, pinnedMessageId) : undefined;
const topMessageSender = message ? selectForwardedSender(global, message) : undefined; const topMessageSender = message ? selectForwardedSender(global, message) : undefined;
@ -590,7 +602,7 @@ export default memo(withGlobal<OwnProps>(
}; };
} }
const pinnedMessageIds = selectPinnedIds(global, chatId, threadId); const pinnedMessageIds = !isSavedDialog ? selectPinnedIds(global, chatId, threadId) : undefined;
if (pinnedMessageIds?.length) { if (pinnedMessageIds?.length) {
const firstPinnedMessage = messagesById[pinnedMessageIds[0]]; const firstPinnedMessage = messagesById[pinnedMessageIds[0]];
const { const {

View File

@ -6,6 +6,7 @@ import React, {
import { getActions, withGlobal } from '../../global'; import { getActions, withGlobal } from '../../global';
import type { ApiChat } from '../../api/types'; import type { ApiChat } from '../../api/types';
import type { ThreadId } from '../../types';
import { requestMutation } from '../../lib/fasterdom/fasterdom'; import { requestMutation } from '../../lib/fasterdom/fasterdom';
import { import {
@ -32,7 +33,7 @@ export type OwnProps = {
type StateProps = { type StateProps = {
isActive?: boolean; isActive?: boolean;
chat?: ApiChat; chat?: ApiChat;
threadId?: number; threadId?: ThreadId;
query?: string; query?: string;
totalCount?: number; totalCount?: number;
foundIds?: number[]; foundIds?: number[];

View File

@ -5,7 +5,7 @@ import React, {
import { getActions } from '../../../global'; import { getActions } from '../../../global';
import type { ApiAttachBot } from '../../../api/types'; import type { ApiAttachBot } from '../../../api/types';
import type { IAnchorPosition, ISettings } from '../../../types'; import type { IAnchorPosition, ISettings, ThreadId } from '../../../types';
import useFlag from '../../../hooks/useFlag'; import useFlag from '../../../hooks/useFlag';
import useLang from '../../../hooks/useLang'; import useLang from '../../../hooks/useLang';
@ -20,7 +20,7 @@ type OwnProps = {
theme: ISettings['theme']; theme: ISettings['theme'];
isInSideMenu?: true; isInSideMenu?: true;
chatId?: string; chatId?: string;
threadId?: number; threadId?: ThreadId;
canShowNew?: boolean; canShowNew?: boolean;
onMenuOpened: VoidFunction; onMenuOpened: VoidFunction;
onMenuClosed: VoidFunction; onMenuClosed: VoidFunction;

View File

@ -6,7 +6,7 @@ import React, {
import type { ApiAttachMenuPeerType } from '../../../api/types'; import type { ApiAttachMenuPeerType } from '../../../api/types';
import type { GlobalState } from '../../../global/types'; import type { GlobalState } from '../../../global/types';
import type { ISettings } from '../../../types'; import type { ISettings, ThreadId } from '../../../types';
import { import {
CONTENT_TYPES_WITH_PREVIEW, DEBUG_LOG_FILENAME, SUPPORTED_AUDIO_CONTENT_TYPES, CONTENT_TYPES_WITH_PREVIEW, DEBUG_LOG_FILENAME, SUPPORTED_AUDIO_CONTENT_TYPES,
@ -32,7 +32,7 @@ import './AttachMenu.scss';
export type OwnProps = { export type OwnProps = {
chatId: string; chatId: string;
threadId?: number; threadId?: ThreadId;
isButtonVisible: boolean; isButtonVisible: boolean;
canAttachMedia: boolean; canAttachMedia: boolean;
canAttachPolls: boolean; canAttachPolls: boolean;

View File

@ -8,6 +8,7 @@ import type {
ApiAttachment, ApiChatMember, ApiSticker, ApiAttachment, ApiChatMember, ApiSticker,
} from '../../../api/types'; } from '../../../api/types';
import type { GlobalState } from '../../../global/types'; import type { GlobalState } from '../../../global/types';
import type { ThreadId } from '../../../types';
import type { Signal } from '../../../util/signals'; import type { Signal } from '../../../util/signals';
import { import {
@ -59,7 +60,7 @@ import styles from './AttachmentModal.module.scss';
export type OwnProps = { export type OwnProps = {
chatId: string; chatId: string;
threadId: number; threadId: ThreadId;
attachments: ApiAttachment[]; attachments: ApiAttachment[];
getHtml: Signal<string>; getHtml: Signal<string>;
canShowCustomSendMenu?: boolean; canShowCustomSendMenu?: boolean;

View File

@ -7,7 +7,7 @@ import React, {
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import type { ApiInputMessageReplyInfo } from '../../../api/types'; import type { ApiInputMessageReplyInfo } from '../../../api/types';
import type { IAnchorPosition, ISettings } from '../../../types'; import type { IAnchorPosition, ISettings, ThreadId } from '../../../types';
import type { Signal } from '../../../util/signals'; import type { Signal } from '../../../util/signals';
import { EDITABLE_INPUT_ID } from '../../../config'; import { EDITABLE_INPUT_ID } from '../../../config';
@ -48,7 +48,7 @@ type OwnProps = {
ref?: RefObject<HTMLDivElement>; ref?: RefObject<HTMLDivElement>;
id: string; id: string;
chatId: string; chatId: string;
threadId: number; threadId: ThreadId;
isAttachmentModalInput?: boolean; isAttachmentModalInput?: boolean;
isStoryInput?: boolean; isStoryInput?: boolean;
customEmojiPrefix: string; customEmojiPrefix: string;

View File

@ -6,7 +6,7 @@ import React, {
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import type { ApiChat, ApiSticker, ApiStickerSet } from '../../../api/types'; import type { ApiChat, ApiSticker, ApiStickerSet } from '../../../api/types';
import type { StickerSetOrReactionsSetOrRecent } from '../../../types'; import type { StickerSetOrReactionsSetOrRecent, ThreadId } from '../../../types';
import { import {
CHAT_STICKER_SET_ID, CHAT_STICKER_SET_ID,
@ -48,7 +48,7 @@ import styles from './StickerPicker.module.scss';
type OwnProps = { type OwnProps = {
chatId: string; chatId: string;
threadId?: number; threadId?: ThreadId;
className: string; className: string;
isHidden?: boolean; isHidden?: boolean;
isTranslucent?: boolean; isTranslucent?: boolean;

View File

@ -3,6 +3,7 @@ import React, { memo, useEffect, useRef } from '../../../lib/teact/teact';
import { withGlobal } from '../../../global'; import { withGlobal } from '../../../global';
import type { ApiSticker } from '../../../api/types'; import type { ApiSticker } from '../../../api/types';
import type { ThreadId } from '../../../types';
import { STICKER_SIZE_PICKER } from '../../../config'; import { STICKER_SIZE_PICKER } from '../../../config';
import { selectIsChatWithSelf, selectIsCurrentUserPremium } from '../../../global/selectors'; import { selectIsChatWithSelf, selectIsCurrentUserPremium } from '../../../global/selectors';
@ -21,7 +22,7 @@ import './StickerTooltip.scss';
export type OwnProps = { export type OwnProps = {
chatId: string; chatId: string;
threadId?: number; threadId?: ThreadId;
isOpen: boolean; isOpen: boolean;
onStickerSelect: (sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean) => void; onStickerSelect: (sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean) => void;
onClose: NoneToVoidFunction; onClose: NoneToVoidFunction;

View File

@ -6,6 +6,7 @@ import { withGlobal } from '../../../global';
import type { ApiSticker, ApiVideo } from '../../../api/types'; import type { ApiSticker, ApiVideo } from '../../../api/types';
import type { GlobalActions } from '../../../global'; import type { GlobalActions } from '../../../global';
import type { ThreadId } from '../../../types';
import { requestMutation } from '../../../lib/fasterdom/fasterdom'; import { requestMutation } from '../../../lib/fasterdom/fasterdom';
import { selectIsContextMenuTranslucent, selectTabState } from '../../../global/selectors'; import { selectIsContextMenuTranslucent, selectTabState } from '../../../global/selectors';
@ -35,7 +36,7 @@ const STICKERS_TAB_INDEX = 2;
export type OwnProps = { export type OwnProps = {
chatId: string; chatId: string;
threadId?: number; threadId?: ThreadId;
isOpen: boolean; isOpen: boolean;
canSendStickers?: boolean; canSendStickers?: boolean;
canSendGifs?: boolean; canSendGifs?: boolean;

View File

@ -3,7 +3,7 @@ import React, { memo, useRef, useState } from '../../../lib/teact/teact';
import { getActions } from '../../../global'; import { getActions } from '../../../global';
import type { ApiSticker, ApiVideo } from '../../../api/types'; import type { ApiSticker, ApiVideo } from '../../../api/types';
import type { IAnchorPosition } from '../../../types'; import type { IAnchorPosition, ThreadId } from '../../../types';
import { EDITABLE_INPUT_CSS_SELECTOR, EDITABLE_INPUT_MODAL_CSS_SELECTOR } from '../../../config'; import { EDITABLE_INPUT_CSS_SELECTOR, EDITABLE_INPUT_MODAL_CSS_SELECTOR } from '../../../config';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
@ -21,7 +21,7 @@ const MOBILE_KEYBOARD_HIDE_DELAY_MS = 100;
type OwnProps = { type OwnProps = {
chatId: string; chatId: string;
threadId?: number; threadId?: ThreadId;
isMobile?: boolean; isMobile?: boolean;
isReady?: boolean; isReady?: boolean;
isSymbolMenuOpen?: boolean; isSymbolMenuOpen?: boolean;

View File

@ -5,7 +5,7 @@ import { getActions, withGlobal } from '../../../global';
import type { import type {
ApiFormattedText, ApiMessage, ApiMessageEntityTextUrl, ApiWebPage, ApiFormattedText, ApiMessage, ApiMessageEntityTextUrl, ApiWebPage,
} from '../../../api/types'; } from '../../../api/types';
import type { ISettings } from '../../../types'; import type { ISettings, ThreadId } from '../../../types';
import type { Signal } from '../../../util/signals'; import type { Signal } from '../../../util/signals';
import { ApiMessageEntityTypes } from '../../../api/types'; import { ApiMessageEntityTypes } from '../../../api/types';
@ -29,7 +29,7 @@ import './WebPagePreview.scss';
type OwnProps = { type OwnProps = {
chatId: string; chatId: string;
threadId: number; threadId: ThreadId;
getHtml: Signal<string>; getHtml: Signal<string>;
isDisabled?: boolean; isDisabled?: boolean;
}; };

View File

@ -3,6 +3,7 @@ import { getActions } from '../../../../global';
import type { ApiMessage } from '../../../../api/types'; import type { ApiMessage } from '../../../../api/types';
import type { ApiDraft } from '../../../../global/types'; import type { ApiDraft } from '../../../../global/types';
import type { ThreadId } from '../../../../types';
import type { Signal } from '../../../../util/signals'; import type { Signal } from '../../../../util/signals';
import { ApiMessageEntityTypes } from '../../../../api/types'; import { ApiMessageEntityTypes } from '../../../../api/types';
@ -43,7 +44,7 @@ const useDraft = ({
} : { } : {
draft?: ApiDraft; draft?: ApiDraft;
chatId: string; chatId: string;
threadId: number; threadId: ThreadId;
getHtml: Signal<string>; getHtml: Signal<string>;
setHtml: (html: string) => void; setHtml: (html: string) => void;
editedMessage?: ApiMessage; editedMessage?: ApiMessage;
@ -68,7 +69,7 @@ const useDraft = ({
const isEditing = Boolean(editedMessage); const isEditing = Boolean(editedMessage);
const updateDraft = useLastCallback((prevState: { chatId?: string; threadId?: number } = {}) => { const updateDraft = useLastCallback((prevState: { chatId?: string; threadId?: ThreadId } = {}) => {
if (isDisabled || isEditing || !isTouchedRef.current) return; if (isDisabled || isEditing || !isTouchedRef.current) return;
const html = getHtml(); const html = getHtml();

View File

@ -3,6 +3,7 @@ import { getActions } from '../../../../global';
import type { ApiFormattedText, ApiMessage } from '../../../../api/types'; import type { ApiFormattedText, ApiMessage } from '../../../../api/types';
import type { ApiDraft, MessageListType } from '../../../../global/types'; import type { ApiDraft, MessageListType } from '../../../../global/types';
import type { ThreadId } from '../../../../types';
import type { Signal } from '../../../../util/signals'; import type { Signal } from '../../../../util/signals';
import { ApiMessageEntityTypes } from '../../../../api/types'; import { ApiMessageEntityTypes } from '../../../../api/types';
@ -30,7 +31,7 @@ const useEditing = (
resetComposer: (shouldPreserveInput?: boolean) => void, resetComposer: (shouldPreserveInput?: boolean) => void,
openDeleteModal: () => void, openDeleteModal: () => void,
chatId: string, chatId: string,
threadId: number, threadId: ThreadId,
type: MessageListType, type: MessageListType,
draft?: ApiDraft, draft?: ApiDraft,
editingDraft?: ApiFormattedText, editingDraft?: ApiFormattedText,

View File

@ -85,7 +85,7 @@ export function groupMessages(
|| (lastSenderGroupItem || (lastSenderGroupItem
&& 'mainMessage' in lastSenderGroupItem && lastSenderGroupItem.mainMessage?.id === topMessageId)) && 'mainMessage' in lastSenderGroupItem && lastSenderGroupItem.mainMessage?.id === topMessageId))
&& nextMessage.id !== topMessageId) && nextMessage.id !== topMessageId)
|| (isChatWithSelf && message.forwardInfo?.senderUserId !== nextMessage.forwardInfo?.senderUserId) || (isChatWithSelf && message.forwardInfo?.fromId !== nextMessage.forwardInfo?.fromId)
) { ) {
currentSenderGroup = []; currentSenderGroup = [];
currentDateGroup.senderGroups.push(currentSenderGroup); currentDateGroup.senderGroups.push(currentSenderGroup);

View File

@ -1,6 +1,8 @@
import { useEffect, useRef } from '../../../lib/teact/teact'; import { useEffect, useRef } from '../../../lib/teact/teact';
import { getGlobal } from '../../../global'; import { getGlobal } from '../../../global';
import type { ThreadId } from '../../../types';
import { import {
selectFocusedMessageId, selectFocusedMessageId,
selectListedIds, selectListedIds,
@ -24,7 +26,7 @@ type PinnedIntersectionChangedParams = {
export type PinnedIntersectionChangedCallback = (params: PinnedIntersectionChangedParams) => void; export type PinnedIntersectionChangedCallback = (params: PinnedIntersectionChangedParams) => void;
export default function usePinnedMessage( export default function usePinnedMessage(
chatId?: string, threadId?: number, pinnedIds?: number[], topMessageId?: number, chatId?: string, threadId?: ThreadId, pinnedIds?: number[], topMessageId?: number,
) { ) {
const [getCurrentPinnedIndexes, setCurrentPinnedIndexes] = useSignal<Record<string, number>>({}); const [getCurrentPinnedIndexes, setCurrentPinnedIndexes] = useSignal<Record<string, number>>({});
const [getForceNextPinnedInHeader, setForceNextPinnedInHeader] = useSignal<boolean | undefined>(); const [getForceNextPinnedInHeader, setForceNextPinnedInHeader] = useSignal<boolean | undefined>();

View File

@ -23,7 +23,9 @@ import type {
MessageListType, MessageListType,
} from '../../../global/types'; } from '../../../global/types';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver'; import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import type { FocusDirection, IAlbum, ISettings } from '../../../types'; import type {
FocusDirection, IAlbum, ISettings, ThreadId,
} from '../../../types';
import type { Signal } from '../../../util/signals'; import type { Signal } from '../../../util/signals';
import type { PinnedIntersectionChangedCallback } from '../hooks/usePinnedMessage'; import type { PinnedIntersectionChangedCallback } from '../hooks/usePinnedMessage';
import { MAIN_THREAD_ID } from '../../../api/types'; import { MAIN_THREAD_ID } from '../../../api/types';
@ -32,6 +34,7 @@ import { AudioOrigin } from '../../../types';
import { EMOJI_STATUS_LOOP_LIMIT, GENERAL_TOPIC_ID } from '../../../config'; import { EMOJI_STATUS_LOOP_LIMIT, GENERAL_TOPIC_ID } from '../../../config';
import { import {
areReactionsEmpty, areReactionsEmpty,
getIsSavedDialog,
getMessageContent, getMessageContent,
getMessageCustomShape, getMessageCustomShape,
getMessageHtmlId, getMessageHtmlId,
@ -41,6 +44,7 @@ import {
getSenderTitle, getSenderTitle,
hasMessageText, hasMessageText,
hasMessageTtl, hasMessageTtl,
isAnonymousForwardsChat,
isAnonymousOwnMessage, isAnonymousOwnMessage,
isChatChannel, isChatChannel,
isChatGroup, isChatGroup,
@ -191,7 +195,7 @@ type OwnProps =
noAvatars?: boolean; noAvatars?: boolean;
withAvatar?: boolean; withAvatar?: boolean;
withSenderName?: boolean; withSenderName?: boolean;
threadId: number; threadId: ThreadId;
messageListType: MessageListType; messageListType: MessageListType;
noComments: boolean; noComments: boolean;
noReplies: boolean; noReplies: boolean;
@ -232,6 +236,7 @@ type StateProps = {
isForwarding?: boolean; isForwarding?: boolean;
isChatWithSelf?: boolean; isChatWithSelf?: boolean;
isRepliesChat?: boolean; isRepliesChat?: boolean;
isAnonymousForwards?: boolean;
isChannel?: boolean; isChannel?: boolean;
isGroup?: boolean; isGroup?: boolean;
canReply?: boolean; canReply?: boolean;
@ -243,7 +248,7 @@ type StateProps = {
isSelected?: boolean; isSelected?: boolean;
isGroupSelected?: boolean; isGroupSelected?: boolean;
isDownloading?: boolean; isDownloading?: boolean;
threadId?: number; threadId?: ThreadId;
isPinnedList?: boolean; isPinnedList?: boolean;
isPinned?: boolean; isPinned?: boolean;
canAutoLoadMedia?: boolean; canAutoLoadMedia?: boolean;
@ -274,6 +279,7 @@ type StateProps = {
isConnected: boolean; isConnected: boolean;
isLoadingComments?: boolean; isLoadingComments?: boolean;
shouldWarnAboutSvg?: boolean; shouldWarnAboutSvg?: boolean;
isInSavedDialog?: boolean;
}; };
type MetaPosition = type MetaPosition =
@ -347,6 +353,7 @@ const Message: FC<OwnProps & StateProps> = ({
isForwarding, isForwarding,
isChatWithSelf, isChatWithSelf,
isRepliesChat, isRepliesChat,
isAnonymousForwards,
isChannel, isChannel,
isGroup, isGroup,
canReply, canReply,
@ -387,6 +394,7 @@ const Message: FC<OwnProps & StateProps> = ({
isConnected, isConnected,
getIsMessageListReady, getIsMessageListReady,
shouldWarnAboutSvg, shouldWarnAboutSvg,
isInSavedDialog,
onPinnedIntersectionChange, onPinnedIntersectionChange,
}) => { }) => {
const { const {
@ -483,6 +491,7 @@ const Message: FC<OwnProps & StateProps> = ({
forwardInfo forwardInfo
&& (!isChatWithSelf || isScheduled) && (!isChatWithSelf || isScheduled)
&& !isRepliesChat && !isRepliesChat
&& !isAnonymousForwards
&& !forwardInfo.isLinkedChannelPost && !forwardInfo.isLinkedChannelPost
&& !isCustomShape && !isCustomShape
) || Boolean(message.content.storyData && !message.content.storyData.isMention); ) || Boolean(message.content.storyData && !message.content.storyData.isMention);
@ -500,7 +509,7 @@ const Message: FC<OwnProps & StateProps> = ({
const canForward = isChannel && !isScheduled && message.isForwardingAllowed && !isChatProtected; const canForward = isChannel && !isScheduled && message.isForwardingAllowed && !isChatProtected;
const canFocus = Boolean(isPinnedList const canFocus = Boolean(isPinnedList
|| (forwardInfo || (forwardInfo
&& (forwardInfo.isChannelPost || (isChatWithSelf && !isOwn) || isRepliesChat) && (forwardInfo.isChannelPost || (isChatWithSelf && !isOwn) || isRepliesChat || isAnonymousForwards)
&& forwardInfo.fromMessageId && forwardInfo.fromMessageId
)); ));
@ -520,7 +529,8 @@ const Message: FC<OwnProps & StateProps> = ({
const messageSender = canShowSender ? sender : undefined; const messageSender = canShowSender ? sender : undefined;
const withVoiceTranscription = Boolean(!isTranscriptionHidden && (isTranscriptionError || transcribedText)); const withVoiceTranscription = Boolean(!isTranscriptionHidden && (isTranscriptionError || transcribedText));
const shouldPreferOriginSender = forwardInfo && (isChatWithSelf || isRepliesChat || !messageSender); const shouldPreferOriginSender = forwardInfo
&& (isChatWithSelf || isRepliesChat || isAnonymousForwards || !messageSender);
const avatarPeer = shouldPreferOriginSender ? originSender : messageSender; const avatarPeer = shouldPreferOriginSender ? originSender : messageSender;
const messageColorPeer = originSender || sender; const messageColorPeer = originSender || sender;
const senderPeer = (forwardInfo || message.content.storyData) ? originSender : messageSender; const senderPeer = (forwardInfo || message.content.storyData) ? originSender : messageSender;
@ -914,6 +924,7 @@ const Message: FC<OwnProps & StateProps> = ({
<MessageMeta <MessageMeta
message={message} message={message}
isPinned={isPinned} isPinned={isPinned}
isInSavedDialog={isInSavedDialog}
noReplies={noReplies} noReplies={noReplies}
repliesThreadInfo={repliesThreadInfo} repliesThreadInfo={repliesThreadInfo}
outgoingStatus={outgoingStatus} outgoingStatus={outgoingStatus}
@ -1461,6 +1472,7 @@ export default memo(withGlobal<OwnProps>(
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
const isChatWithSelf = selectIsChatWithSelf(global, chatId); const isChatWithSelf = selectIsChatWithSelf(global, chatId);
const isRepliesChat = isChatWithRepliesBot(chatId); const isRepliesChat = isChatWithRepliesBot(chatId);
const isAnonymousForwards = isAnonymousForwardsChat(chatId);
const isChannel = chat && isChatChannel(chat); const isChannel = chat && isChatChannel(chat);
const isGroup = chat && isChatGroup(chat); const isGroup = chat && isChatGroup(chat);
const chatFullInfo = !isUserId(chatId) ? selectChatFullInfo(global, chatId) : undefined; const chatFullInfo = !isUserId(chatId) ? selectChatFullInfo(global, chatId) : undefined;
@ -1487,11 +1499,12 @@ export default memo(withGlobal<OwnProps>(
const shouldHideReply = replyToMsgId && replyToMsgId === threadId; const shouldHideReply = replyToMsgId && replyToMsgId === threadId;
const replyMessage = replyToMsgId ? selectChatMessage(global, replyToPeerId || chatId, replyToMsgId) : undefined; const replyMessage = replyToMsgId ? selectChatMessage(global, replyToPeerId || chatId, replyToMsgId) : undefined;
const forwardHeader = forwardInfo || replyFrom; const forwardHeader = forwardInfo || replyFrom;
const replyMessageSender = replyMessage ? selectReplySender(global, replyMessage) : forwardHeader && !isRepliesChat const replyMessageSender = replyMessage ? selectReplySender(global, replyMessage)
? selectSenderFromHeader(global, forwardHeader) : undefined; : forwardHeader && !isRepliesChat && !isAnonymousForwards
? selectSenderFromHeader(global, forwardHeader) : undefined;
const replyMessageForwardSender = replyMessage && selectForwardedSender(global, replyMessage); const replyMessageForwardSender = replyMessage && selectForwardedSender(global, replyMessage);
const replyMessageChat = replyToPeerId ? selectChat(global, replyToPeerId) : undefined; const replyMessageChat = replyToPeerId ? selectChat(global, replyToPeerId) : undefined;
const isReplyPrivate = !isRepliesChat && replyMessageChat && !isChatPublic(replyMessageChat) const isReplyPrivate = !isRepliesChat && !isAnonymousForwards && replyMessageChat && !isChatPublic(replyMessageChat)
&& (replyMessageChat.isNotJoined || replyMessageChat.isRestricted); && (replyMessageChat.isNotJoined || replyMessageChat.isRestricted);
const isReplyToTopicStart = replyMessage?.content.action?.type === 'topicCreate'; const isReplyToTopicStart = replyMessage?.content.action?.type === 'topicCreate';
const replyStory = storyReplyId && storyReplyUserId const replyStory = storyReplyId && storyReplyUserId
@ -1554,6 +1567,8 @@ export default memo(withGlobal<OwnProps>(
const hasActiveReactions = Boolean(reactionMessage && activeReactions[getMessageKey(reactionMessage)]?.length); const hasActiveReactions = Boolean(reactionMessage && activeReactions[getMessageKey(reactionMessage)]?.length);
const isInSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
return { return {
theme: selectTheme(global), theme: selectTheme(global),
forceSenderName, forceSenderName,
@ -1578,6 +1593,7 @@ export default memo(withGlobal<OwnProps>(
reactionMessage, reactionMessage,
isChatWithSelf, isChatWithSelf,
isRepliesChat, isRepliesChat,
isAnonymousForwards,
isChannel, isChannel,
isGroup, isGroup,
canReply, canReply,
@ -1621,6 +1637,7 @@ export default memo(withGlobal<OwnProps>(
withStickerEffects: selectPerformanceSettingsValue(global, 'stickerEffects'), withStickerEffects: selectPerformanceSettingsValue(global, 'stickerEffects'),
webPageStory, webPageStory,
isConnected, isConnected,
isInSavedDialog,
isLoadingComments: repliesThreadInfo?.isCommentsInfo isLoadingComments: repliesThreadInfo?.isCommentsInfo
&& loadingThread?.loadingChatId === repliesThreadInfo?.originChannelId && loadingThread?.loadingChatId === repliesThreadInfo?.originChannelId
&& loadingThread?.loadingMessageId === repliesThreadInfo?.originMessageId, && loadingThread?.loadingMessageId === repliesThreadInfo?.originMessageId,

View File

@ -29,6 +29,7 @@ type OwnProps = {
repliesThreadInfo?: ApiThreadInfo; repliesThreadInfo?: ApiThreadInfo;
isTranslated?: boolean; isTranslated?: boolean;
isPinned?: boolean; isPinned?: boolean;
isInSavedDialog?: boolean;
onClick: (e: React.MouseEvent<HTMLDivElement>) => void; onClick: (e: React.MouseEvent<HTMLDivElement>) => void;
onTranslationClick: (e: React.MouseEvent<HTMLDivElement>) => void; onTranslationClick: (e: React.MouseEvent<HTMLDivElement>) => void;
renderQuickReactionButton?: () => TeactNode | undefined; renderQuickReactionButton?: () => TeactNode | undefined;
@ -45,6 +46,7 @@ const MessageMeta: FC<OwnProps> = ({
noReplies, noReplies,
isTranslated, isTranslated,
isPinned, isPinned,
isInSavedDialog,
onClick, onClick,
onTranslationClick, onTranslationClick,
onOpenThread, onOpenThread,
@ -72,7 +74,12 @@ const MessageMeta: FC<OwnProps> = ({
const editDateTime = message.isEdited const editDateTime = message.isEdited
&& formatDateTimeToString(message.editDate! * 1000, lang.code, undefined, lang.timeFormat); && formatDateTimeToString(message.editDate! * 1000, lang.code, undefined, lang.timeFormat);
const forwardedDateTime = message.forwardInfo const forwardedDateTime = message.forwardInfo
&& formatDateTimeToString(message.forwardInfo.date * 1000, lang.code, undefined, lang.timeFormat); && formatDateTimeToString(
(message.forwardInfo.savedDate || message.forwardInfo.date) * 1000,
lang.code,
undefined,
lang.timeFormat,
);
let text = createDateTime; let text = createDateTime;
if (editDateTime) { if (editDateTime) {
@ -137,7 +144,9 @@ const MessageMeta: FC<OwnProps> = ({
</> </>
)} )}
{message.isEdited && `${lang('EditedMessage')} `} {message.isEdited && `${lang('EditedMessage')} `}
{formatTime(lang, message.date * 1000)} {isInSavedDialog
? formatDateTimeToString((message.forwardInfo?.date || message.date) * 1000, lang.code, true)
: formatTime(lang, message.date * 1000)}
</span> </span>
{outgoingStatus && ( {outgoingStatus && (
<MessageOutgoingStatus status={outgoingStatus} /> <MessageOutgoingStatus status={outgoingStatus} />

View File

@ -5,7 +5,7 @@ import type {
ApiMessage, ApiPeer, ApiStory, ApiTopic, ApiUser, ApiMessage, ApiPeer, ApiStory, ApiTopic, ApiUser,
} from '../../../../api/types'; } from '../../../../api/types';
import type { LangFn } from '../../../../hooks/useLang'; import type { LangFn } from '../../../../hooks/useLang';
import type { IAlbum } from '../../../../types'; import type { IAlbum, ThreadId } from '../../../../types';
import { MAIN_THREAD_ID } from '../../../../api/types'; import { MAIN_THREAD_ID } from '../../../../api/types';
import { MediaViewerOrigin } from '../../../../types'; import { MediaViewerOrigin } from '../../../../types';
@ -18,7 +18,7 @@ export default function useInnerHandlers(
selectMessage: (e: React.MouseEvent<HTMLDivElement, MouseEvent>, groupedId?: string) => void, selectMessage: (e: React.MouseEvent<HTMLDivElement, MouseEvent>, groupedId?: string) => void,
message: ApiMessage, message: ApiMessage,
chatId: string, chatId: string,
threadId: number, threadId: ThreadId,
isInDocumentGroup: boolean, isInDocumentGroup: boolean,
asForwarded?: boolean, asForwarded?: boolean,
isScheduled?: boolean, isScheduled?: boolean,

View File

@ -5,15 +5,16 @@ import React, {
import { getActions, getGlobal, withGlobal } from '../../global'; import { getActions, getGlobal, withGlobal } from '../../global';
import type { import type {
ApiChat, ApiChatMember, ApiChatMember,
} from '../../api/types'; } from '../../api/types';
import { NewChatMembersProgress } from '../../types'; import { NewChatMembersProgress } from '../../types';
import { import {
filterUsersByName, isChatChannel, isUserBot, sortChatIds, filterUsersByName, isChatChannel, isUserBot,
} from '../../global/helpers'; } from '../../global/helpers';
import { selectChat, selectChatFullInfo, selectTabState } from '../../global/selectors'; import { selectChat, selectChatFullInfo, selectTabState } from '../../global/selectors';
import { unique } from '../../util/iteratees'; import { unique } from '../../util/iteratees';
import sortChatIds from '../common/helpers/sortChatIds';
import useHistoryBack from '../../hooks/useHistoryBack'; import useHistoryBack from '../../hooks/useHistoryBack';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
@ -36,7 +37,6 @@ type StateProps = {
isChannel?: boolean; isChannel?: boolean;
members?: ApiChatMember[]; members?: ApiChatMember[];
currentUserId?: string; currentUserId?: string;
chatsById: Record<string, ApiChat>;
localContactIds?: string[]; localContactIds?: string[];
searchQuery?: string; searchQuery?: string;
isLoading: boolean; isLoading: boolean;
@ -50,7 +50,6 @@ const AddChatMembers: FC<OwnProps & StateProps> = ({
members, members,
onNextStep, onNextStep,
currentUserId, currentUserId,
chatsById,
localContactIds, localContactIds,
isLoading, isLoading,
searchQuery, searchQuery,
@ -104,11 +103,8 @@ const AddChatMembers: FC<OwnProps & StateProps> = ({
&& (!user || !isUserBot(user) || (!isChannel && user.canBeInvitedToGroup)) && (!user || !isUserBot(user) || (!isChannel && user.canBeInvitedToGroup))
); );
}), }),
chatsById,
); );
}, [ }, [localContactIds, searchQuery, localUserIds, globalUserIds, currentUserId, memberIds, isChannel]);
localContactIds, chatsById, searchQuery, localUserIds, globalUserIds, currentUserId, memberIds, isChannel,
]);
const handleNextStep = useCallback(() => { const handleNextStep = useCallback(() => {
if (selectedMemberIds.length) { if (selectedMemberIds.length) {
@ -154,7 +150,6 @@ export default memo(withGlobal<OwnProps>(
(global, { chatId }): StateProps => { (global, { chatId }): StateProps => {
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
const { userIds: localContactIds } = global.contactList || {}; const { userIds: localContactIds } = global.contactList || {};
const { byId: chatsById } = global.chats;
const { newChatMembersProgress } = selectTabState(global); const { newChatMembersProgress } = selectTabState(global);
const { currentUserId } = global; const { currentUserId } = global;
const isChannel = chat && isChatChannel(chat); const isChannel = chat && isChatChannel(chat);
@ -170,7 +165,6 @@ export default memo(withGlobal<OwnProps>(
isChannel, isChannel,
members: selectChatFullInfo(global, chatId)?.members, members: selectChatFullInfo(global, chatId)?.members,
currentUserId, currentUserId,
chatsById,
localContactIds, localContactIds,
searchQuery, searchQuery,
isSearching: fetchingStatus, isSearching: fetchingStatus,

View File

@ -63,6 +63,10 @@
flex: 1; flex: 1;
} }
.saved-dialogs {
height: 100% !important;
}
.content { .content {
&.empty-list { &.empty-list {
height: 100%; height: 100%;

View File

@ -14,7 +14,7 @@ import type {
ApiUserStatus, ApiUserStatus,
} from '../../api/types'; } from '../../api/types';
import type { import type {
ISettings, ProfileState, ProfileTabType, SharedMediaType, ISettings, ProfileState, ProfileTabType, SharedMediaType, ThreadId,
} from '../../types'; } from '../../types';
import { MAIN_THREAD_ID } from '../../api/types'; import { MAIN_THREAD_ID } from '../../api/types';
import { AudioOrigin, MediaViewerOrigin, NewChatMembersProgress } from '../../types'; import { AudioOrigin, MediaViewerOrigin, NewChatMembersProgress } from '../../types';
@ -26,7 +26,7 @@ import {
SLIDE_TRANSITION_DURATION, SLIDE_TRANSITION_DURATION,
} from '../../config'; } from '../../config';
import { import {
getHasAdminRight, isChatAdmin, isChatChannel, isChatGroup, isUserBot, isUserId, isUserRightBanned, getHasAdminRight, getIsSavedDialog, isChatAdmin, isChatChannel, isChatGroup, isUserBot, isUserId, isUserRightBanned,
} from '../../global/helpers'; } from '../../global/helpers';
import { import {
selectActiveDownloads, selectActiveDownloads,
@ -70,6 +70,7 @@ import NothingFound from '../common/NothingFound';
import PrivateChatInfo from '../common/PrivateChatInfo'; import PrivateChatInfo from '../common/PrivateChatInfo';
import ProfileInfo from '../common/ProfileInfo'; import ProfileInfo from '../common/ProfileInfo';
import WebLink from '../common/WebLink'; import WebLink from '../common/WebLink';
import ChatList from '../left/main/ChatList';
import MediaStory from '../story/MediaStory'; import MediaStory from '../story/MediaStory';
import Button from '../ui/Button'; import Button from '../ui/Button';
import FloatingActionButton from '../ui/FloatingActionButton'; import FloatingActionButton from '../ui/FloatingActionButton';
@ -84,7 +85,7 @@ import './Profile.scss';
type OwnProps = { type OwnProps = {
chatId: string; chatId: string;
topicId?: number; threadId?: ThreadId;
profileState: ProfileState; profileState: ProfileState;
isMobile?: boolean; isMobile?: boolean;
onProfileStateChange: (state: ProfileState) => void; onProfileStateChange: (state: ProfileState) => void;
@ -122,9 +123,16 @@ type StateProps = {
similarChannels?: string[]; similarChannels?: string[];
isCurrentUserPremium?: boolean; isCurrentUserPremium?: boolean;
limitSimilarChannels: number; limitSimilarChannels: number;
isTopicInfo?: boolean;
isSavedDialog?: boolean;
}; };
const TABS = [ type TabProps = {
type: ProfileTabType;
title: string;
};
const TABS: TabProps[] = [
{ type: 'media', title: 'SharedMediaTab2' }, { type: 'media', title: 'SharedMediaTab2' },
{ type: 'documents', title: 'SharedFilesTab2' }, { type: 'documents', title: 'SharedFilesTab2' },
{ type: 'links', title: 'SharedLinksTab2' }, { type: 'links', title: 'SharedLinksTab2' },
@ -136,7 +144,7 @@ const INTERSECTION_THROTTLE = 500;
const Profile: FC<OwnProps & StateProps> = ({ const Profile: FC<OwnProps & StateProps> = ({
chatId, chatId,
topicId, threadId,
profileState, profileState,
onProfileStateChange, onProfileStateChange,
theme, theme,
@ -170,6 +178,8 @@ const Profile: FC<OwnProps & StateProps> = ({
similarChannels, similarChannels,
isCurrentUserPremium, isCurrentUserPremium,
limitSimilarChannels, limitSimilarChannels,
isTopicInfo,
isSavedDialog,
}) => { }) => {
const { const {
setLocalMediaSearchType, setLocalMediaSearchType,
@ -195,29 +205,33 @@ const Profile: FC<OwnProps & StateProps> = ({
const lang = useLang(); const lang = useLang();
const [deletingUserId, setDeletingUserId] = useState<string | undefined>(); const [deletingUserId, setDeletingUserId] = useState<string | undefined>();
const profileId = isSavedDialog ? String(threadId) : (resolvedUserId || chatId);
const isSavedMessages = profileId === currentUserId && !isSavedDialog;
const tabs = useMemo(() => ([ const tabs = useMemo(() => ([
...(hasStoriesTab ? [{ type: 'stories', title: 'ProfileStories' }] : []), ...(isSavedMessages && !isSavedDialog ? [{ type: 'dialogs' as const, title: 'SavedDialogsTab' }] : []),
...(hasStoriesTab && currentUserId === chatId ? [{ type: 'storiesArchive', title: 'ProfileStoriesArchive' }] : []), ...(hasStoriesTab ? [{ type: 'stories' as const, title: 'ProfileStories' }] : []),
...(hasStoriesTab && isSavedMessages ? [{ type: 'storiesArchive' as const, title: 'ProfileStoriesArchive' }] : []),
...(hasMembersTab ? [{ ...(hasMembersTab ? [{
type: 'members', title: isChannel ? 'ChannelSubscribers' : 'GroupMembers', type: 'members' as const, title: isChannel ? 'ChannelSubscribers' : 'GroupMembers',
}] : []), }] : []),
...TABS, ...TABS,
// TODO The filter for voice messages currently does not work // TODO The filter for voice messages currently does not work
// in forum topics. Return it when it's fixed on the server side. // in forum topics. Return it when it's fixed on the server side.
...(!topicId ? [{ type: 'voice', title: 'SharedVoiceTab2' }] : []), ...(!isTopicInfo ? [{ type: 'voice' as const, title: 'SharedVoiceTab2' }] : []),
...(hasCommonChatsTab ? [{ type: 'commonChats', title: 'SharedGroupsTab2' }] : []), ...(hasCommonChatsTab ? [{ type: 'commonChats' as const, title: 'SharedGroupsTab2' }] : []),
...(isChannel && similarChannels?.length ...(isChannel && similarChannels?.length
? [{ type: 'similarChannels', title: 'SimilarChannelsTab' }] ? [{ type: 'similarChannels' as const, title: 'SimilarChannelsTab' }]
: []), : []),
]), [ ]), [
chatId,
currentUserId,
hasCommonChatsTab, hasCommonChatsTab,
hasMembersTab, hasMembersTab,
hasStoriesTab, hasStoriesTab,
isChannel, isChannel,
topicId, isTopicInfo,
similarChannels, similarChannels,
isSavedMessages,
isSavedDialog,
]); ]);
const initialTab = useMemo(() => { const initialTab = useMemo(() => {
@ -269,12 +283,13 @@ const Profile: FC<OwnProps & StateProps> = ({
chatsById, chatsById,
messagesById, messagesById,
foundIds, foundIds,
topicId, threadId,
storyIds, storyIds,
archiveStoryIds, archiveStoryIds,
similarChannels, similarChannels,
); );
const isFirstTab = (hasStoriesTab && resultType === 'stories') const isFirstTab = (isSavedMessages && resultType === 'dialogs')
|| (hasStoriesTab && resultType === 'stories')
|| resultType === 'members' || resultType === 'members'
|| (!hasMembersTab && resultType === 'media'); || (!hasMembersTab && resultType === 'media');
const activeKey = tabs.findIndex(({ type }) => type === resultType); const activeKey = tabs.findIndex(({ type }) => type === resultType);
@ -304,9 +319,7 @@ const Profile: FC<OwnProps & StateProps> = ({
// Update search type when switching tabs or forum topics // Update search type when switching tabs or forum topics
useEffect(() => { useEffect(() => {
setLocalMediaSearchType({ mediaType: tabType as SharedMediaType }); setLocalMediaSearchType({ mediaType: tabType as SharedMediaType });
}, [setLocalMediaSearchType, tabType, topicId]); }, [setLocalMediaSearchType, tabType, threadId]);
const profileId = resolvedUserId || chatId;
useEffect(() => { useEffect(() => {
loadProfilePhotos({ profileId }); loadProfilePhotos({ profileId });
@ -376,7 +389,7 @@ const Profile: FC<OwnProps & StateProps> = ({
} else if (!viewportIds) { } else if (!viewportIds) {
renderingDelay = SLIDE_TRANSITION_DURATION; renderingDelay = SLIDE_TRANSITION_DURATION;
} }
const canRenderContent = useAsyncRendering([chatId, topicId, resultType, renderingActiveTab], renderingDelay); const canRenderContent = useAsyncRendering([chatId, threadId, resultType, renderingActiveTab], renderingDelay);
function getMemberContextAction(memberId: string): MenuItemContextAction[] | undefined { function getMemberContextAction(memberId: string): MenuItemContextAction[] | undefined {
return memberId === currentUserId || !canDeleteMembers ? undefined : [{ return memberId === currentUserId || !canDeleteMembers ? undefined : [{
@ -389,6 +402,12 @@ const Profile: FC<OwnProps & StateProps> = ({
} }
function renderContent() { function renderContent() {
if (resultType === 'dialogs') {
return (
<ChatList className="saved-dialogs" folderType="saved" isActive />
);
}
if (!viewportIds || !canRenderContent || !messagesById) { if (!viewportIds || !canRenderContent || !messagesById) {
const noSpinner = isFirstTab && !canRenderContent; const noSpinner = isFirstTab && !canRenderContent;
const forceRenderHiddenMembers = Boolean(resultType === 'members' && areMembersHidden); const forceRenderHiddenMembers = Boolean(resultType === 'members' && areMembersHidden);
@ -594,7 +613,9 @@ const Profile: FC<OwnProps & StateProps> = ({
onLoadMore={getMore} onLoadMore={getMore}
onScroll={handleScroll} onScroll={handleScroll}
> >
{!noProfileInfo && renderProfileInfo(chatId, resolvedUserId, isRightColumnShown && canRenderContent)} {!noProfileInfo && !isSavedMessages && (
renderProfileInfo(profileId, isRightColumnShown && canRenderContent, isSavedDialog)
)}
{!isRestricted && ( {!isRestricted && (
<div <div
className="shared-media" className="shared-media"
@ -635,29 +656,35 @@ const Profile: FC<OwnProps & StateProps> = ({
); );
}; };
function renderProfileInfo(chatId: string, resolvedUserId: string | undefined, isReady: boolean) { function renderProfileInfo(profileId: string, isReady: boolean, isSavedDialog?: boolean) {
return ( return (
<div className="profile-info"> <div className="profile-info">
<ProfileInfo userId={resolvedUserId || chatId} canPlayVideo={isReady} /> <ProfileInfo userId={profileId} canPlayVideo={isReady} />
<ChatExtra chatOrUserId={resolvedUserId || chatId} /> <ChatExtra chatOrUserId={profileId} isSavedDialog={isSavedDialog} />
</div> </div>
); );
} }
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global, { chatId, topicId, isMobile }): StateProps => { (global, {
chatId, threadId, isMobile,
}): StateProps => {
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
const chatFullInfo = selectChatFullInfo(global, chatId); const chatFullInfo = selectChatFullInfo(global, chatId);
const messagesById = selectChatMessages(global, chatId); const messagesById = selectChatMessages(global, chatId);
const { currentType: mediaSearchType, resultsByType } = selectCurrentMediaSearch(global) || {}; const { currentType: mediaSearchType, resultsByType } = selectCurrentMediaSearch(global) || {};
const { foundIds } = (resultsByType && mediaSearchType && resultsByType[mediaSearchType]) || {}; const { foundIds } = (resultsByType && mediaSearchType && resultsByType[mediaSearchType]) || {};
const isTopicInfo = Boolean(chat?.isForum && threadId && threadId !== MAIN_THREAD_ID);
const { byId: usersById, statusesById: userStatusesById } = global.users; const { byId: usersById, statusesById: userStatusesById } = global.users;
const { byId: chatsById } = global.chats; const { byId: chatsById } = global.chats;
const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
const isGroup = chat && isChatGroup(chat); const isGroup = chat && isChatGroup(chat);
const isChannel = chat && isChatChannel(chat); const isChannel = chat && isChatChannel(chat);
const hasMembersTab = !topicId && (isGroup || (isChannel && isChatAdmin(chat!))); const hasMembersTab = !isTopicInfo && !isSavedDialog && (isGroup || (isChannel && isChatAdmin(chat!)));
const members = chatFullInfo?.members; const members = chatFullInfo?.members;
const adminMembersById = chatFullInfo?.adminMembersById; const adminMembersById = chatFullInfo?.adminMembersById;
const areMembersHidden = hasMembersTab && chat const areMembersHidden = hasMembersTab && chat
@ -675,12 +702,13 @@ export default memo(withGlobal<OwnProps>(
if (isUserId(chatId)) { if (isUserId(chatId)) {
resolvedUserId = chatId; resolvedUserId = chatId;
user = selectUser(global, resolvedUserId); user = selectUser(global, resolvedUserId);
hasCommonChatsTab = user && !user.isSelf && !isUserBot(user); hasCommonChatsTab = user && !user.isSelf && !isUserBot(user) && !isSavedDialog;
} }
const peer = user || chat; const peer = user || chat;
const peerFullInfo = selectPeerFullInfo(global, chatId); const peerFullInfo = selectPeerFullInfo(global, chatId);
const hasStoriesTab = peer && (user?.isSelf || (!peer.areStoriesHidden && peerFullInfo?.hasPinnedStories)); const hasStoriesTab = peer && (user?.isSelf || (!peer.areStoriesHidden && peerFullInfo?.hasPinnedStories))
&& !isSavedDialog;
const peerStories = hasStoriesTab ? selectPeerStories(global, peer.id) : undefined; const peerStories = hasStoriesTab ? selectPeerStories(global, peer.id) : undefined;
const storyIds = peerStories?.pinnedIds; const storyIds = peerStories?.pinnedIds;
const storyByIds = peerStories?.byId; const storyByIds = peerStories?.byId;
@ -714,6 +742,8 @@ export default memo(withGlobal<OwnProps>(
shouldWarnAboutSvg: global.settings.byKey.shouldWarnAboutSvg, shouldWarnAboutSvg: global.settings.byKey.shouldWarnAboutSvg,
similarChannels, similarChannels,
isCurrentUserPremium, isCurrentUserPremium,
isTopicInfo,
isSavedDialog,
limitSimilarChannels: selectPremiumLimit(global, 'recommendedChannels'), limitSimilarChannels: selectPremiumLimit(global, 'recommendedChannels'),
...(hasMembersTab && members && { members, adminMembersById }), ...(hasMembersTab && members && { members, adminMembersById }),
...(hasCommonChatsTab && user && { commonChatIds: user.commonChats?.ids }), ...(hasCommonChatsTab && user && { commonChatIds: user.commonChats?.ids }),

View File

@ -2,15 +2,19 @@ import type { FC } from '../../lib/teact/teact';
import React, { memo, useEffect, useState } from '../../lib/teact/teact'; import React, { memo, useEffect, useState } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global'; import { getActions, withGlobal } from '../../global';
import type { ProfileTabType } from '../../types'; import type { ProfileTabType, ThreadId } from '../../types';
import { MAIN_THREAD_ID } from '../../api/types';
import { import {
ManagementScreens, NewChatMembersProgress, ProfileState, RightColumnContent, ManagementScreens, NewChatMembersProgress, ProfileState, RightColumnContent,
} from '../../types'; } from '../../types';
import { ANIMATION_END_DELAY, MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN } from '../../config'; import { ANIMATION_END_DELAY, MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN } from '../../config';
import { getIsSavedDialog } from '../../global/helpers';
import { import {
selectAreActiveChatsLoaded, selectChat, selectCurrentMessageList, selectRightColumnContentKey, selectTabState, selectAreActiveChatsLoaded,
selectCurrentMessageList,
selectIsChatWithSelf,
selectRightColumnContentKey,
selectTabState,
} from '../../global/selectors'; } from '../../global/selectors';
import captureEscKeyListener from '../../util/captureEscKeyListener'; import captureEscKeyListener from '../../util/captureEscKeyListener';
@ -45,12 +49,14 @@ interface OwnProps {
type StateProps = { type StateProps = {
contentKey?: RightColumnContent; contentKey?: RightColumnContent;
chatId?: string; chatId?: string;
threadId?: number; threadId?: ThreadId;
isInsideTopic?: boolean; isInsideTopic?: boolean;
isChatSelected: boolean; isChatSelected: boolean;
shouldSkipHistoryAnimations?: boolean; shouldSkipHistoryAnimations?: boolean;
nextManagementScreen?: ManagementScreens; nextManagementScreen?: ManagementScreens;
nextProfileTab?: ProfileTabType; nextProfileTab?: ProfileTabType;
isSavedMessages?: boolean;
isSavedDialog?: boolean;
}; };
const ANIMATION_DURATION = 450 + ANIMATION_END_DELAY; const ANIMATION_DURATION = 450 + ANIMATION_END_DELAY;
@ -69,11 +75,12 @@ const RightColumn: FC<OwnProps & StateProps> = ({
chatId, chatId,
threadId, threadId,
isMobile, isMobile,
isInsideTopic,
isChatSelected, isChatSelected,
shouldSkipHistoryAnimations, shouldSkipHistoryAnimations,
nextManagementScreen, nextManagementScreen,
nextProfileTab, nextProfileTab,
isSavedMessages,
isSavedDialog,
}) => { }) => {
const { const {
toggleChatInfo, toggleChatInfo,
@ -97,7 +104,9 @@ const RightColumn: FC<OwnProps & StateProps> = ({
} = getActions(); } = getActions();
const { width: windowWidth } = useWindowSize(); const { width: windowWidth } = useWindowSize();
const [profileState, setProfileState] = useState<ProfileState>(ProfileState.Profile); const [profileState, setProfileState] = useState<ProfileState>(
isSavedMessages && !isSavedDialog ? ProfileState.SavedDialogs : ProfileState.Profile,
);
const [managementScreen, setManagementScreen] = useState<ManagementScreens>(ManagementScreens.Initial); const [managementScreen, setManagementScreen] = useState<ManagementScreens>(ManagementScreens.Initial);
const [selectedChatMemberId, setSelectedChatMemberId] = useState<string | undefined>(); const [selectedChatMemberId, setSelectedChatMemberId] = useState<string | undefined>();
const [isPromotedByCurrentUser, setIsPromotedByCurrentUser] = useState<boolean | undefined>(); const [isPromotedByCurrentUser, setIsPromotedByCurrentUser] = useState<boolean | undefined>();
@ -129,7 +138,7 @@ const RightColumn: FC<OwnProps & StateProps> = ({
setNewChatMembersDialogState({ newChatMembersProgress: NewChatMembersProgress.Closed }); setNewChatMembersDialogState({ newChatMembersProgress: NewChatMembersProgress.Closed });
break; break;
case RightColumnContent.ChatInfo: case RightColumnContent.ChatInfo:
if (isScrolledDown && shouldScrollUp) { if (isScrolledDown && shouldScrollUp && !isSavedMessages) {
setProfileState(ProfileState.Profile); setProfileState(ProfileState.Profile);
break; break;
} }
@ -253,12 +262,14 @@ const RightColumn: FC<OwnProps & StateProps> = ({
}, [isOverlaying]); }, [isOverlaying]);
// We need to clear profile state and management screen state, when changing chats // We need to clear profile state and management screen state, when changing chats
useLayoutEffectWithPrevDeps(([prevChatId]) => { useLayoutEffectWithPrevDeps(([prevChatId, prevThreadId]) => {
if (prevChatId !== chatId) { if (prevChatId !== chatId || prevThreadId !== threadId) {
setProfileState(ProfileState.Profile); setProfileState(
isSavedMessages && !isSavedDialog ? ProfileState.SavedDialogs : ProfileState.Profile,
);
setManagementScreen(ManagementScreens.Initial); setManagementScreen(ManagementScreens.Initial);
} }
}, [chatId]); }, [chatId, threadId, isSavedDialog, isSavedMessages]);
useHistoryBack({ useHistoryBack({
isActive: isChatSelected && ( isActive: isChatSelected && (
@ -289,9 +300,9 @@ const RightColumn: FC<OwnProps & StateProps> = ({
case RightColumnContent.ChatInfo: case RightColumnContent.ChatInfo:
return ( return (
<Profile <Profile
key={`profile_${chatId!}`} key={`profile_${chatId!}_${threadId}`}
chatId={chatId!} chatId={chatId!}
topicId={isInsideTopic ? threadId : undefined} threadId={threadId}
profileState={profileState} profileState={profileState}
isMobile={isMobile} isMobile={isMobile}
onProfileStateChange={setProfileState} onProfileStateChange={setProfileState}
@ -400,18 +411,20 @@ export default memo(withGlobal<OwnProps>(
const areActiveChatsLoaded = selectAreActiveChatsLoaded(global); const areActiveChatsLoaded = selectAreActiveChatsLoaded(global);
const { management, shouldSkipHistoryAnimations, nextProfileTab } = selectTabState(global); const { management, shouldSkipHistoryAnimations, nextProfileTab } = selectTabState(global);
const nextManagementScreen = chatId ? management.byChatId[chatId]?.nextScreen : undefined; const nextManagementScreen = chatId ? management.byChatId[chatId]?.nextScreen : undefined;
const isForum = chatId ? selectChat(global, chatId)?.isForum : undefined;
const isInsideTopic = isForum && Boolean(threadId && threadId !== MAIN_THREAD_ID); const isSavedMessages = chatId ? selectIsChatWithSelf(global, chatId) : undefined;
const isSavedDialog = chatId ? getIsSavedDialog(chatId, threadId, global.currentUserId) : undefined;
return { return {
contentKey: selectRightColumnContentKey(global, isMobile), contentKey: selectRightColumnContentKey(global, isMobile),
chatId, chatId,
threadId, threadId,
isInsideTopic,
isChatSelected: Boolean(chatId && areActiveChatsLoaded), isChatSelected: Boolean(chatId && areActiveChatsLoaded),
shouldSkipHistoryAnimations, shouldSkipHistoryAnimations,
nextManagementScreen, nextManagementScreen,
nextProfileTab, nextProfileTab,
isSavedMessages,
isSavedDialog,
}; };
}, },
)(RightColumn)); )(RightColumn));

View File

@ -23,13 +23,27 @@
} }
} }
h3 { .title {
margin-bottom: 0; margin-bottom: 0;
font-size: 1.25rem; font-size: 1.25rem;
font-weight: 500; font-weight: 500;
margin-left: 1.375rem; margin-left: 1.375rem;
} }
.header {
margin-left: 1.375rem;
.title {
font-size: 1rem;
margin-left: 0;
}
.subtitle {
color: var(--color-text-secondary);
font-size: 0.875rem;
}
}
.tools { .tools {
display: flex; display: flex;
margin-left: auto; margin-left: auto;

View File

@ -4,9 +4,9 @@ import { getActions, withGlobal } from '../../global';
import type { ApiExportedInvite } from '../../api/types'; import type { ApiExportedInvite } from '../../api/types';
import { MAIN_THREAD_ID } from '../../api/types'; import { MAIN_THREAD_ID } from '../../api/types';
import { ManagementScreens, ProfileState } from '../../types'; import { ManagementScreens, ProfileState, type ThreadId } from '../../types';
import { ANIMATION_END_DELAY } from '../../config'; import { ANIMATION_END_DELAY, SAVED_FOLDER_ID } from '../../config';
import { import {
getCanAddContact, getCanManageTopic, isChatChannel, isUserBot, isUserId, getCanAddContact, getCanManageTopic, isChatChannel, isUserBot, isUserId,
} from '../../global/helpers'; } from '../../global/helpers';
@ -17,6 +17,7 @@ import {
selectCurrentGifSearch, selectCurrentGifSearch,
selectCurrentStickerSearch, selectCurrentStickerSearch,
selectCurrentTextSearch, selectCurrentTextSearch,
selectIsChatWithSelf,
selectTabState, selectTabState,
selectUser, selectUser,
} from '../../global/selectors'; } from '../../global/selectors';
@ -28,6 +29,7 @@ import useAppLayout from '../../hooks/useAppLayout';
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev'; import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
import useElectronDrag from '../../hooks/useElectronDrag'; import useElectronDrag from '../../hooks/useElectronDrag';
import useFlag from '../../hooks/useFlag'; import useFlag from '../../hooks/useFlag';
import { useFolderManagerForChatsCount } from '../../hooks/useFolderManager';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../hooks/useLastCallback';
@ -41,7 +43,7 @@ import './RightHeader.scss';
type OwnProps = { type OwnProps = {
chatId?: string; chatId?: string;
threadId?: number; threadId?: ThreadId;
isColumnOpen?: boolean; isColumnOpen?: boolean;
isProfile?: boolean; isProfile?: boolean;
isSearch?: boolean; isSearch?: boolean;
@ -58,7 +60,7 @@ type OwnProps = {
isAddingChatMembers?: boolean; isAddingChatMembers?: boolean;
profileState?: ProfileState; profileState?: ProfileState;
managementScreen?: ManagementScreens; managementScreen?: ManagementScreens;
onClose: () => void; onClose: (shouldScrollUp?: boolean) => void;
onScreenSelect: (screen: ManagementScreens) => void; onScreenSelect: (screen: ManagementScreens) => void;
}; };
@ -79,6 +81,7 @@ type StateProps = {
canEditBot?: boolean; canEditBot?: boolean;
isInsideTopic?: boolean; isInsideTopic?: boolean;
canEditTopic?: boolean; canEditTopic?: boolean;
isSavedMessages?: boolean;
}; };
const COLUMN_ANIMATION_DURATION = 450 + ANIMATION_END_DELAY; const COLUMN_ANIMATION_DURATION = 450 + ANIMATION_END_DELAY;
@ -121,6 +124,7 @@ enum HeaderContent {
ManageJoinRequests, ManageJoinRequests,
CreateTopic, CreateTopic,
EditTopic, EditTopic,
SavedDialogs,
} }
const RightHeader: FC<OwnProps & StateProps> = ({ const RightHeader: FC<OwnProps & StateProps> = ({
@ -157,6 +161,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
isBot, isBot,
isInsideTopic, isInsideTopic,
canEditTopic, canEditTopic,
isSavedMessages,
onClose, onClose,
onScreenSelect, onScreenSelect,
canEditBot, canEditBot,
@ -178,6 +183,8 @@ const RightHeader: FC<OwnProps & StateProps> = ({
const [isDeleteDialogOpen, openDeleteDialog, closeDeleteDialog] = useFlag(); const [isDeleteDialogOpen, openDeleteDialog, closeDeleteDialog] = useFlag();
const { isMobile } = useAppLayout(); const { isMobile } = useAppLayout();
const foldersChatCount = useFolderManagerForChatsCount();
const handleEditInviteClick = useLastCallback(() => { const handleEditInviteClick = useLastCallback(() => {
setEditingExportedInvite({ chatId: chatId!, invite: currentInviteInfo! }); setEditingExportedInvite({ chatId: chatId!, invite: currentInviteInfo! });
onScreenSelect(ManagementScreens.EditInvite); onScreenSelect(ManagementScreens.EditInvite);
@ -211,7 +218,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
const toggleEditTopic = useLastCallback(() => { const toggleEditTopic = useLastCallback(() => {
if (!chatId || !threadId) return; if (!chatId || !threadId) return;
openEditTopicPanel({ chatId, topicId: threadId }); openEditTopicPanel({ chatId, topicId: Number(threadId) });
}); });
const handleToggleManagement = useLastCallback(() => { const handleToggleManagement = useLastCallback(() => {
@ -222,6 +229,10 @@ const RightHeader: FC<OwnProps & StateProps> = ({
toggleStatistics(); toggleStatistics();
}); });
const handleClose = useLastCallback(() => {
onClose(!isSavedMessages);
});
const [shouldSkipTransition, setShouldSkipTransition] = useState(!isColumnOpen); const [shouldSkipTransition, setShouldSkipTransition] = useState(!isColumnOpen);
useEffect(() => { useEffect(() => {
@ -240,6 +251,8 @@ const RightHeader: FC<OwnProps & StateProps> = ({
HeaderContent.MemberList HeaderContent.MemberList
) : profileState === ProfileState.StoryList ? ( ) : profileState === ProfileState.StoryList ? (
HeaderContent.StoryList HeaderContent.StoryList
) : profileState === ProfileState.SavedDialogs ? (
HeaderContent.SavedDialogs
) : -1 // Never reached ) : -1 // Never reached
) : isSearch ? ( ) : isSearch ? (
HeaderContent.Search HeaderContent.Search
@ -310,6 +323,10 @@ const RightHeader: FC<OwnProps & StateProps> = ({
const renderingContentKey = useCurrentOrPrev(contentKey, true) ?? -1; const renderingContentKey = useCurrentOrPrev(contentKey, true) ?? -1;
function getHeaderTitle() { function getHeaderTitle() {
if (isSavedMessages) {
return lang('SavedMessages');
}
if (isInsideTopic) { if (isInsideTopic) {
return lang('AccDescrTopic'); return lang('AccDescrTopic');
} }
@ -332,7 +349,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
switch (renderingContentKey) { switch (renderingContentKey) {
case HeaderContent.PollResults: case HeaderContent.PollResults:
return <h3>{lang('PollResults')}</h3>; return <h3 className="title">{lang('PollResults')}</h3>;
case HeaderContent.Search: case HeaderContent.Search:
return ( return (
<> <>
@ -354,39 +371,39 @@ const RightHeader: FC<OwnProps & StateProps> = ({
</> </>
); );
case HeaderContent.AddingMembers: case HeaderContent.AddingMembers:
return <h3>{lang(isChannel ? 'ChannelAddSubscribers' : 'GroupAddMembers')}</h3>; return <h3 className="title">{lang(isChannel ? 'ChannelAddSubscribers' : 'GroupAddMembers')}</h3>;
case HeaderContent.ManageInitial: case HeaderContent.ManageInitial:
return <h3>{lang('Edit')}</h3>; return <h3 className="title">{lang('Edit')}</h3>;
case HeaderContent.ManageChatPrivacyType: case HeaderContent.ManageChatPrivacyType:
return <h3>{lang(isChannel ? 'ChannelTypeHeader' : 'GroupTypeHeader')}</h3>; return <h3 className="title">{lang(isChannel ? 'ChannelTypeHeader' : 'GroupTypeHeader')}</h3>;
case HeaderContent.ManageDiscussion: case HeaderContent.ManageDiscussion:
return <h3>{lang('Discussion')}</h3>; return <h3 className="title">{lang('Discussion')}</h3>;
case HeaderContent.ManageChatAdministrators: case HeaderContent.ManageChatAdministrators:
return <h3>{lang('ChannelAdministrators')}</h3>; return <h3 className="title">{lang('ChannelAdministrators')}</h3>;
case HeaderContent.ManageGroupRecentActions: case HeaderContent.ManageGroupRecentActions:
return <h3>{lang('Group.Info.AdminLog')}</h3>; return <h3 className="title">{lang('Group.Info.AdminLog')}</h3>;
case HeaderContent.ManageGroupAdminRights: case HeaderContent.ManageGroupAdminRights:
return <h3>{lang('EditAdminRights')}</h3>; return <h3 className="title">{lang('EditAdminRights')}</h3>;
case HeaderContent.ManageGroupNewAdminRights: case HeaderContent.ManageGroupNewAdminRights:
return <h3>{lang('SetAsAdmin')}</h3>; return <h3 className="title">{lang('SetAsAdmin')}</h3>;
case HeaderContent.ManageGroupPermissions: case HeaderContent.ManageGroupPermissions:
return <h3>{lang('ChannelPermissions')}</h3>; return <h3 className="title">{lang('ChannelPermissions')}</h3>;
case HeaderContent.ManageGroupRemovedUsers: case HeaderContent.ManageGroupRemovedUsers:
return <h3>{lang('BlockedUsers')}</h3>; return <h3 className="title">{lang('BlockedUsers')}</h3>;
case HeaderContent.ManageChannelRemovedUsers: case HeaderContent.ManageChannelRemovedUsers:
return <h3>{lang('ChannelBlockedUsers')}</h3>; return <h3 className="title">{lang('ChannelBlockedUsers')}</h3>;
case HeaderContent.ManageGroupUserPermissionsCreate: case HeaderContent.ManageGroupUserPermissionsCreate:
return <h3>{lang('ChannelAddException')}</h3>; return <h3 className="title">{lang('ChannelAddException')}</h3>;
case HeaderContent.ManageGroupUserPermissions: case HeaderContent.ManageGroupUserPermissions:
return <h3>{lang('UserRestrictions')}</h3>; return <h3 className="title">{lang('UserRestrictions')}</h3>;
case HeaderContent.ManageInvites: case HeaderContent.ManageInvites:
return <h3>{lang('lng_group_invite_title')}</h3>; return <h3 className="title">{lang('lng_group_invite_title')}</h3>;
case HeaderContent.ManageEditInvite: case HeaderContent.ManageEditInvite:
return <h3>{isEditingInvite ? lang('EditLink') : lang('NewLink')}</h3>; return <h3 className="title">{isEditingInvite ? lang('EditLink') : lang('NewLink')}</h3>;
case HeaderContent.ManageInviteInfo: case HeaderContent.ManageInviteInfo:
return ( return (
<> <>
<h3>{lang('InviteLink')}</h3> <h3 className="title">{lang('InviteLink')}</h3>
<section className="tools"> <section className="tools">
{currentInviteInfo && !currentInviteInfo.isRevoked && ( {currentInviteInfo && !currentInviteInfo.isRevoked && (
<Button <Button
@ -425,9 +442,9 @@ const RightHeader: FC<OwnProps & StateProps> = ({
</> </>
); );
case HeaderContent.ManageJoinRequests: case HeaderContent.ManageJoinRequests:
return <h3>{isChannel ? lang('SubscribeRequests') : lang('MemberRequests')}</h3>; return <h3 className="title">{isChannel ? lang('SubscribeRequests') : lang('MemberRequests')}</h3>;
case HeaderContent.ManageGroupAddAdmins: case HeaderContent.ManageGroupAddAdmins:
return <h3>{lang('Channel.Management.AddModerator')}</h3>; return <h3 className="title">{lang('Channel.Management.AddModerator')}</h3>;
case HeaderContent.StickerSearch: case HeaderContent.StickerSearch:
return ( return (
<SearchInput <SearchInput
@ -447,32 +464,40 @@ const RightHeader: FC<OwnProps & StateProps> = ({
/> />
); );
case HeaderContent.Statistics: case HeaderContent.Statistics:
return <h3>{lang(isChannel ? 'ChannelStats.Title' : 'GroupStats.Title')}</h3>; return <h3 className="title">{lang(isChannel ? 'ChannelStats.Title' : 'GroupStats.Title')}</h3>;
case HeaderContent.MessageStatistics: case HeaderContent.MessageStatistics:
return <h3>{lang('Stats.MessageTitle')}</h3>; return <h3 className="title">{lang('Stats.MessageTitle')}</h3>;
case HeaderContent.StoryStatistics: case HeaderContent.StoryStatistics:
return <h3>{lang('Stats.StoryTitle')}</h3>; return <h3 className="title">{lang('Stats.StoryTitle')}</h3>;
case HeaderContent.BoostStatistics: case HeaderContent.BoostStatistics:
return <h3>{lang('Boosts')}</h3>; return <h3 className="title">{lang('Boosts')}</h3>;
case HeaderContent.SharedMedia: case HeaderContent.SharedMedia:
return <h3>{lang('SharedMedia')}</h3>; return <h3 className="title">{lang('SharedMedia')}</h3>;
case HeaderContent.ManageChannelSubscribers: case HeaderContent.ManageChannelSubscribers:
return <h3>{lang('ChannelSubscribers')}</h3>; return <h3 className="title">{lang('ChannelSubscribers')}</h3>;
case HeaderContent.MemberList: case HeaderContent.MemberList:
case HeaderContent.ManageGroupMembers: case HeaderContent.ManageGroupMembers:
return <h3>{lang('GroupMembers')}</h3>; return <h3 className="title">{lang('GroupMembers')}</h3>;
case HeaderContent.StoryList: case HeaderContent.StoryList:
return <h3>{lang(isSelf ? 'Settings.MyStories' : 'PeerInfo.PaneStories')}</h3>; return <h3 className="title">{lang(isSelf ? 'Settings.MyStories' : 'PeerInfo.PaneStories')}</h3>;
case HeaderContent.SavedDialogs:
return (
<div className="header">
<h3 className="title">{lang('SavedMessagesTab')}</h3>
<div className="subtitle">{lang('Chats', foldersChatCount[SAVED_FOLDER_ID])}</div>
</div>
);
case HeaderContent.ManageReactions: case HeaderContent.ManageReactions:
return <h3>{lang('Reactions')}</h3>; return <h3 className="title">{lang('Reactions')}</h3>;
case HeaderContent.CreateTopic: case HeaderContent.CreateTopic:
return <h3>{lang('NewTopic')}</h3>; return <h3 className="title">{lang('NewTopic')}</h3>;
case HeaderContent.EditTopic: case HeaderContent.EditTopic:
return <h3>{lang('EditTopic')}</h3>; return <h3 className="title">{lang('EditTopic')}</h3>;
default: default:
return ( return (
<> <>
<h3>{getHeaderTitle()} <h3 className="title">
{getHeaderTitle()}
</h3> </h3>
<section className="tools"> <section className="tools">
{canAddContact && ( {canAddContact && (
@ -536,15 +561,16 @@ const RightHeader: FC<OwnProps & StateProps> = ({
} }
} }
const isBackButton = ( const isBackButton = isMobile || (
isMobile !isSavedMessages && (
|| contentKey === HeaderContent.SharedMedia contentKey === HeaderContent.SharedMedia
|| contentKey === HeaderContent.MemberList || contentKey === HeaderContent.MemberList
|| contentKey === HeaderContent.StoryList || contentKey === HeaderContent.StoryList
|| contentKey === HeaderContent.AddingMembers || contentKey === HeaderContent.AddingMembers
|| contentKey === HeaderContent.MessageStatistics || contentKey === HeaderContent.MessageStatistics
|| contentKey === HeaderContent.StoryStatistics || contentKey === HeaderContent.StoryStatistics
|| isManagement || isManagement
)
); );
const buttonClassName = buildClassName( const buttonClassName = buildClassName(
@ -564,7 +590,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
round round
color="translucent" color="translucent"
size="smaller" size="smaller"
onClick={onClose} onClick={handleClose}
ariaLabel={isBackButton ? lang('Common.Back') : lang('Common.Close')} ariaLabel={isBackButton ? lang('Common.Back') : lang('Common.Close')}
> >
<div className={buttonClassName} /> <div className={buttonClassName} />
@ -594,6 +620,7 @@ export default withGlobal<OwnProps>(
const topic = isInsideTopic ? chat.topics?.[threadId!] : undefined; const topic = isInsideTopic ? chat.topics?.[threadId!] : undefined;
const canEditTopic = isInsideTopic && topic && getCanManageTopic(chat, topic); const canEditTopic = isInsideTopic && topic && getCanManageTopic(chat, topic);
const isBot = user && isUserBot(user); const isBot = user && isUserBot(user);
const isSavedMessages = chatId ? selectIsChatWithSelf(global, chatId) : undefined;
const canEditBot = isBot && user?.canEditBot; const canEditBot = isBot && user?.canEditBot;
const canAddContact = user && getCanAddContact(user); const canAddContact = user && getCanAddContact(user);
@ -621,6 +648,7 @@ export default withGlobal<OwnProps>(
gifSearchQuery, gifSearchQuery,
isEditingInvite, isEditingInvite,
currentInviteInfo, currentInviteInfo,
isSavedMessages,
shouldSkipHistoryAnimations: tabState.shouldSkipHistoryAnimations, shouldSkipHistoryAnimations: tabState.shouldSkipHistoryAnimations,
canEditBot, canEditBot,
}; };

View File

@ -6,12 +6,15 @@ import React, {
import { getActions, getGlobal, withGlobal } from '../../global'; import { getActions, getGlobal, withGlobal } from '../../global';
import type { ApiMessage, ApiPeer } from '../../api/types'; import type { ApiMessage, ApiPeer } from '../../api/types';
import type { ThreadId } from '../../types';
import { REPLIES_USER_ID } from '../../config';
import { import {
selectChat,
selectChatMessages, selectChatMessages,
selectCurrentTextSearch, selectCurrentTextSearch,
selectUser, selectForwardedSender,
selectIsChatWithSelf,
selectSender,
} from '../../global/selectors'; } from '../../global/selectors';
import { disableDirectTextInput, enableDirectTextInput } from '../../util/directInputManager'; import { disableDirectTextInput, enableDirectTextInput } from '../../util/directInputManager';
import { MEMO_EMPTY_ARRAY } from '../../util/memo'; import { MEMO_EMPTY_ARRAY } from '../../util/memo';
@ -32,7 +35,7 @@ import './RightSearch.scss';
export type OwnProps = { export type OwnProps = {
chatId: string; chatId: string;
threadId: number; threadId: ThreadId;
onClose: NoneToVoidFunction; onClose: NoneToVoidFunction;
isActive: boolean; isActive: boolean;
}; };
@ -42,6 +45,7 @@ type StateProps = {
query?: string; query?: string;
totalCount?: number; totalCount?: number;
foundIds?: number[]; foundIds?: number[];
isSavedMessages?: boolean;
}; };
const RightSearch: FC<OwnProps & StateProps> = ({ const RightSearch: FC<OwnProps & StateProps> = ({
@ -52,6 +56,7 @@ const RightSearch: FC<OwnProps & StateProps> = ({
query, query,
totalCount, totalCount,
foundIds, foundIds,
isSavedMessages,
onClose, onClose,
}) => { }) => {
const { const {
@ -97,15 +102,11 @@ const RightSearch: FC<OwnProps & StateProps> = ({
const global = getGlobal(); const global = getGlobal();
let senderPeer = message.senderId const originalSender = (isSavedMessages || chatId === REPLIES_USER_ID)
? selectUser(global, message.senderId) || selectChat(global, message.senderId) ? selectForwardedSender(global, message) : undefined;
: undefined; const messageSender = selectSender(global, message);
if (!senderPeer && message.forwardInfo) { const senderPeer = originalSender || messageSender;
const { isChannelPost, fromChatId } = message.forwardInfo;
const originalSender = isChannelPost && fromChatId ? selectChat(global, fromChatId) : undefined;
if (originalSender) senderPeer = originalSender;
}
if (!senderPeer) { if (!senderPeer) {
return undefined; return undefined;
@ -113,11 +114,11 @@ const RightSearch: FC<OwnProps & StateProps> = ({
return { return {
message, message,
senderPeer: senderPeer!, senderPeer,
onClick: () => focusMessage({ chatId, threadId, messageId: id }), onClick: () => focusMessage({ chatId, threadId, messageId: id }),
}; };
}).filter(Boolean); }).filter(Boolean);
}, [query, viewportIds, messagesById, focusMessage, chatId, threadId]); }, [query, viewportIds, messagesById, isSavedMessages, chatId, threadId]);
const handleKeyDown = useKeyboardListNavigation(containerRef, true, (index) => { const handleKeyDown = useKeyboardListNavigation(containerRef, true, (index) => {
const foundResult = viewportResults?.[index === -1 ? 0 : index]; const foundResult = viewportResults?.[index === -1 ? 0 : index];
@ -197,11 +198,14 @@ export default memo(withGlobal<OwnProps>(
const { query, results } = selectCurrentTextSearch(global) || {}; const { query, results } = selectCurrentTextSearch(global) || {};
const { totalCount, foundIds } = results || {}; const { totalCount, foundIds } = results || {};
const isSavedMessages = selectIsChatWithSelf(global, chatId);
return { return {
messagesById, messagesById,
query, query,
totalCount, totalCount,
foundIds, foundIds,
isSavedMessages,
}; };
}, },
)(RightSearch)); )(RightSearch));

View File

@ -1,6 +1,6 @@
import { useEffect } from '../../../lib/teact/teact'; import { useEffect } from '../../../lib/teact/teact';
import { ProfileState } from '../../../types'; import { ProfileState, type ProfileTabType } from '../../../types';
import animateScroll from '../../../util/animateScroll'; import animateScroll from '../../../util/animateScroll';
import { throttle } from '../../../util/schedulers'; import { throttle } from '../../../util/schedulers';
@ -17,7 +17,7 @@ let isScrollingProgrammatically = false;
export default function useProfileState( export default function useProfileState(
containerRef: { current: HTMLDivElement | null }, containerRef: { current: HTMLDivElement | null },
tabType: string, tabType: ProfileTabType,
profileState: ProfileState, profileState: ProfileState,
onProfileStateChange: (state: ProfileState) => void, onProfileStateChange: (state: ProfileState) => void,
) { ) {
@ -27,11 +27,7 @@ export default function useProfileState(
const container = containerRef.current!; const container = containerRef.current!;
const tabsEl = container.querySelector<HTMLDivElement>('.TabList')!; const tabsEl = container.querySelector<HTMLDivElement>('.TabList')!;
if (container.scrollTop < tabsEl.offsetTop) { if (container.scrollTop < tabsEl.offsetTop) {
onProfileStateChange( onProfileStateChange(getStateFromTabType(tabType));
tabType === 'members'
? ProfileState.MemberList
: (tabType === 'stories' ? ProfileState.StoryList : ProfileState.SharedMedia),
);
isScrollingProgrammatically = true; isScrollingProgrammatically = true;
animateScroll(container, tabsEl, 'start', undefined, undefined, undefined, TRANSITION_DURATION); animateScroll(container, tabsEl, 'start', undefined, undefined, undefined, TRANSITION_DURATION);
setTimeout(() => { setTimeout(() => {
@ -69,9 +65,7 @@ export default function useProfileState(
setTimeout(() => { setTimeout(() => {
isScrollingProgrammatically = false; isScrollingProgrammatically = false;
}, PROGRAMMATIC_SCROLL_TIMEOUT_MS); }, PROGRAMMATIC_SCROLL_TIMEOUT_MS);
}, [profileState, containerRef]);
onProfileStateChange(profileState);
}, [profileState, containerRef, onProfileStateChange]);
const determineProfileState = useLastCallback(() => { const determineProfileState = useLastCallback(() => {
const container = containerRef.current; const container = containerRef.current;
@ -86,9 +80,7 @@ export default function useProfileState(
let state: ProfileState = ProfileState.Profile; let state: ProfileState = ProfileState.Profile;
if (container.scrollTop >= tabListEl.offsetTop) { if (container.scrollTop >= tabListEl.offsetTop) {
state = tabType === 'members' state = getStateFromTabType(tabType);
? ProfileState.MemberList
: (tabType === 'stories' ? ProfileState.StoryList : ProfileState.SharedMedia);
} }
onProfileStateChange(state); onProfileStateChange(state);
@ -114,3 +106,16 @@ export default function useProfileState(
return { handleScroll }; return { handleScroll };
} }
function getStateFromTabType(tabType: ProfileTabType) {
switch (tabType) {
case 'members':
return ProfileState.MemberList;
case 'stories':
return ProfileState.StoryList;
case 'dialogs':
return ProfileState.SavedDialogs;
default:
return ProfileState.SharedMedia;
}
}

View File

@ -3,10 +3,11 @@ import { useMemo, useRef } from '../../../lib/teact/teact';
import type { import type {
ApiChat, ApiChatMember, ApiMessage, ApiUser, ApiUserStatus, ApiChat, ApiChatMember, ApiMessage, ApiUser, ApiUserStatus,
} from '../../../api/types'; } from '../../../api/types';
import type { ProfileTabType, SharedMediaType } from '../../../types'; import type { ProfileTabType, SharedMediaType, ThreadId } from '../../../types';
import { MEMBERS_SLICE, MESSAGE_SEARCH_SLICE, SHARED_MEDIA_SLICE } from '../../../config'; import { MEMBERS_SLICE, MESSAGE_SEARCH_SLICE, SHARED_MEDIA_SLICE } from '../../../config';
import { getMessageContentIds, sortChatIds, sortUserIds } from '../../../global/helpers'; import { getMessageContentIds, sortUserIds } from '../../../global/helpers';
import sortChatIds from '../../common/helpers/sortChatIds';
import useInfiniteScroll from '../../../hooks/useInfiniteScroll'; import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
import useSyncEffect from '../../../hooks/useSyncEffect'; import useSyncEffect from '../../../hooks/useSyncEffect';
@ -26,7 +27,7 @@ export default function useProfileViewportIds(
chatsById?: Record<string, ApiChat>, chatsById?: Record<string, ApiChat>,
chatMessages?: Record<number, ApiMessage>, chatMessages?: Record<number, ApiMessage>,
foundIds?: number[], foundIds?: number[],
topicId?: number, threadId?: ThreadId,
storyIds?: number[], storyIds?: number[],
archiveStoryIds?: number[], archiveStoryIds?: number[],
similarChannels?: string[], similarChannels?: string[],
@ -50,7 +51,7 @@ export default function useProfileViewportIds(
return undefined; return undefined;
} }
return sortChatIds(commonChatIds, chatsById, true); return sortChatIds(commonChatIds, true);
}, [chatsById, commonChatIds]); }, [chatsById, commonChatIds]);
const [memberViewportIds, getMoreMembers, noProfileInfoForMembers] = useInfiniteScrollForLoadableItems( const [memberViewportIds, getMoreMembers, noProfileInfoForMembers] = useInfiniteScrollForLoadableItems(
@ -58,23 +59,23 @@ export default function useProfileViewportIds(
); );
const [mediaViewportIds, getMoreMedia, noProfileInfoForMedia] = useInfiniteScrollForSharedMedia( const [mediaViewportIds, getMoreMedia, noProfileInfoForMedia] = useInfiniteScrollForSharedMedia(
'media', resultType, searchMessages, chatMessages, foundIds, topicId, 'media', resultType, searchMessages, chatMessages, foundIds, threadId,
); );
const [documentViewportIds, getMoreDocuments, noProfileInfoForDocuments] = useInfiniteScrollForSharedMedia( const [documentViewportIds, getMoreDocuments, noProfileInfoForDocuments] = useInfiniteScrollForSharedMedia(
'documents', resultType, searchMessages, chatMessages, foundIds, topicId, 'documents', resultType, searchMessages, chatMessages, foundIds, threadId,
); );
const [linkViewportIds, getMoreLinks, noProfileInfoForLinks] = useInfiniteScrollForSharedMedia( const [linkViewportIds, getMoreLinks, noProfileInfoForLinks] = useInfiniteScrollForSharedMedia(
'links', resultType, searchMessages, chatMessages, foundIds, topicId, 'links', resultType, searchMessages, chatMessages, foundIds, threadId,
); );
const [audioViewportIds, getMoreAudio, noProfileInfoForAudio] = useInfiniteScrollForSharedMedia( const [audioViewportIds, getMoreAudio, noProfileInfoForAudio] = useInfiniteScrollForSharedMedia(
'audio', resultType, searchMessages, chatMessages, foundIds, topicId, 'audio', resultType, searchMessages, chatMessages, foundIds, threadId,
); );
const [voiceViewportIds, getMoreVoices, noProfileInfoForVoices] = useInfiniteScrollForSharedMedia( const [voiceViewportIds, getMoreVoices, noProfileInfoForVoices] = useInfiniteScrollForSharedMedia(
'voice', resultType, searchMessages, chatMessages, foundIds, topicId, 'voice', resultType, searchMessages, chatMessages, foundIds, threadId,
); );
const [commonChatViewportIds, getMoreCommonChats, noProfileInfoForCommonChats] = useInfiniteScrollForLoadableItems( const [commonChatViewportIds, getMoreCommonChats, noProfileInfoForCommonChats] = useInfiniteScrollForLoadableItems(
@ -146,6 +147,9 @@ export default function useProfileViewportIds(
case 'similarChannels': case 'similarChannels':
viewportIds = similarChannels; viewportIds = similarChannels;
break; break;
case 'dialogs':
noProfileInfo = true;
break;
} }
return [resultType, viewportIds, getMore, noProfileInfo] as const; return [resultType, viewportIds, getMore, noProfileInfo] as const;
@ -173,13 +177,13 @@ function useInfiniteScrollForSharedMedia(
handleLoadMore?: AnyToVoidFunction, handleLoadMore?: AnyToVoidFunction,
chatMessages?: Record<number, ApiMessage>, chatMessages?: Record<number, ApiMessage>,
foundIds?: number[], foundIds?: number[],
topicId?: number, threadId?: ThreadId,
) { ) {
const messageIdsRef = useRef<number[]>(); const messageIdsRef = useRef<number[]>();
useSyncEffect(() => { useSyncEffect(() => {
messageIdsRef.current = undefined; messageIdsRef.current = undefined;
}, [topicId]); }, [threadId]);
useSyncEffect(() => { useSyncEffect(() => {
if (currentResultType === forSharedMediaType && chatMessages && foundIds) { if (currentResultType === forSharedMediaType && chatMessages && foundIds) {

View File

@ -9,10 +9,11 @@ import { ManagementScreens } from '../../../types';
import { import {
filterUsersByName, getHasAdminRight, isChatBasicGroup, filterUsersByName, getHasAdminRight, isChatBasicGroup,
isChatChannel, isUserBot, sortChatIds, sortUserIds, isChatChannel, isUserBot, sortUserIds,
} from '../../../global/helpers'; } from '../../../global/helpers';
import { selectChat, selectChatFullInfo, selectTabState } from '../../../global/selectors'; import { selectChat, selectChatFullInfo, selectTabState } from '../../../global/selectors';
import { unique } from '../../../util/iteratees'; import { unique } from '../../../util/iteratees';
import sortChatIds from '../../common/helpers/sortChatIds';
import usePeerStoriesPolling from '../../../hooks/polling/usePeerStoriesPolling'; import usePeerStoriesPolling from '../../../hooks/polling/usePeerStoriesPolling';
import useHistoryBack from '../../../hooks/useHistoryBack'; import useHistoryBack from '../../../hooks/useHistoryBack';
@ -111,7 +112,6 @@ const ManageGroupMembers: FC<OwnProps & StateProps> = ({
const displayedIds = useMemo(() => { const displayedIds = useMemo(() => {
// No need for expensive global updates on users, so we avoid them // No need for expensive global updates on users, so we avoid them
const usersById = getGlobal().users.byId; const usersById = getGlobal().users.byId;
const chatsById = getGlobal().chats.byId;
const shouldUseSearchResults = Boolean(searchQuery); const shouldUseSearchResults = Boolean(searchQuery);
const listedIds = !shouldUseSearchResults const listedIds = !shouldUseSearchResults
? memberIds ? memberIds
@ -131,7 +131,6 @@ const ManageGroupMembers: FC<OwnProps & StateProps> = ({
return (isChannel || user.canBeInvitedToGroup || !isUserBot(user)) return (isChannel || user.canBeInvitedToGroup || !isUserBot(user))
&& (!noAdmins || !adminIds.includes(contactId)); && (!noAdmins || !adminIds.includes(contactId));
}), }),
chatsById,
true, true,
); );
}, [memberIds, localContactIds, searchQuery, localUserIds, globalUserIds, isChannel, noAdmins, adminIds]); }, [memberIds, localContactIds, searchQuery, localUserIds, globalUserIds, isChannel, noAdmins, adminIds]);

View File

@ -40,7 +40,6 @@ export const GLOBAL_STATE_CACHE_DISABLED = false;
export const GLOBAL_STATE_CACHE_KEY = 'tt-global-state'; export const GLOBAL_STATE_CACHE_KEY = 'tt-global-state';
export const GLOBAL_STATE_CACHE_USER_LIST_LIMIT = 500; export const GLOBAL_STATE_CACHE_USER_LIST_LIMIT = 500;
export const GLOBAL_STATE_CACHE_CHAT_LIST_LIMIT = 200; export const GLOBAL_STATE_CACHE_CHAT_LIST_LIMIT = 200;
export const GLOBAL_STATE_CACHE_CHATS_WITH_MESSAGES_LIMIT = 30;
export const GLOBAL_STATE_CACHE_CUSTOM_EMOJI_LIMIT = 150; export const GLOBAL_STATE_CACHE_CUSTOM_EMOJI_LIMIT = 150;
export const MEDIA_CACHE_DISABLED = false; export const MEDIA_CACHE_DISABLED = false;
@ -290,11 +289,13 @@ export const HEART_REACTION: ApiReactionEmoji = {
// MTProto constants // MTProto constants
export const SERVICE_NOTIFICATIONS_USER_ID = '777000'; export const SERVICE_NOTIFICATIONS_USER_ID = '777000';
export const REPLIES_USER_ID = '1271266957'; // TODO For Test connection ID must be equal to 708513 export const REPLIES_USER_ID = '1271266957'; // TODO For Test connection ID must be equal to 708513
export const ANONYMOUS_USER_ID = '2666000';
export const RESTRICTED_EMOJI_SET_ID = '7173162320003080'; export const RESTRICTED_EMOJI_SET_ID = '7173162320003080';
export const CHANNEL_ID_LENGTH = 14; // 14 symbols, including -100 prefix export const CHANNEL_ID_LENGTH = 14; // 14 symbols, including -100 prefix
export const DEFAULT_GIF_SEARCH_BOT_USERNAME = 'gif'; export const DEFAULT_GIF_SEARCH_BOT_USERNAME = 'gif';
export const ALL_FOLDER_ID = 0; export const ALL_FOLDER_ID = 0;
export const ARCHIVED_FOLDER_ID = 1; export const ARCHIVED_FOLDER_ID = 1;
export const SAVED_FOLDER_ID = -1;
export const DELETED_COMMENTS_CHANNEL_ID = '-100777'; export const DELETED_COMMENTS_CHANNEL_ID = '-100777';
export const MAX_MEDIA_FILES_FOR_ALBUM = 10; export const MAX_MEDIA_FILES_FOR_ALBUM = 10;
export const MAX_ACTIVE_PINNED_CHATS = 5; export const MAX_ACTIVE_PINNED_CHATS = 5;
@ -339,4 +340,5 @@ export const DEFAULT_LIMITS: Record<ApiLimitType, readonly [number, number]> = {
chatlistInvites: [3, 100], chatlistInvites: [3, 100],
chatlistJoined: [2, 20], chatlistJoined: [2, 20],
recommendedChannels: [10, 100], recommendedChannels: [10, 100],
savedDialogsPinned: [5, 100],
}; };

View File

@ -26,8 +26,19 @@ import {
import { replaceInlineBotSettings, replaceInlineBotsIsLoading } from '../../reducers/bots'; import { replaceInlineBotSettings, replaceInlineBotsIsLoading } from '../../reducers/bots';
import { updateTabState } from '../../reducers/tabs'; import { updateTabState } from '../../reducers/tabs';
import { import {
selectBot, selectChat, selectChatMessage, selectCurrentChat, selectCurrentMessageList, selectDraft, selectBot,
selectIsTrustedBot, selectMessageReplyInfo, selectSendAs, selectTabState, selectUser, selectUserFullInfo, selectChat,
selectChatLastMessageId,
selectChatMessage,
selectCurrentChat,
selectCurrentMessageList,
selectDraft,
selectIsTrustedBot,
selectMessageReplyInfo,
selectSendAs,
selectTabState,
selectUser,
selectUserFullInfo,
} from '../../selectors'; } from '../../selectors';
import { fetchChatByUsername } from './chats'; import { fetchChatByUsername } from './chats';
@ -193,8 +204,10 @@ addActionHandler('sendBotCommand', (global, actions, payload): ActionReturnType
actions.resetDraftReplyInfo({ tabId }); actions.resetDraftReplyInfo({ tabId });
actions.clearWebPagePreview({ tabId }); actions.clearWebPagePreview({ tabId });
const lastMessageId = selectChatLastMessageId(global, chat.id);
void sendBotCommand( void sendBotCommand(
chat, command, selectDraft(global, chat.id, threadId)?.replyInfo, selectSendAs(global, chat.id), chat, command, selectDraft(global, chat.id, threadId)?.replyInfo, selectSendAs(global, chat.id), lastMessageId,
); );
}); });
@ -207,6 +220,8 @@ addActionHandler('restartBot', async (global, actions, payload): Promise<void> =
return; return;
} }
const lastMessageId = selectChatLastMessageId(global, chat.id);
const result = await callApi('unblockUser', { user: bot }); const result = await callApi('unblockUser', { user: bot });
if (!result) { if (!result) {
return; return;
@ -215,7 +230,7 @@ addActionHandler('restartBot', async (global, actions, payload): Promise<void> =
global = getGlobal(); global = getGlobal();
global = removeBlockedUser(global, bot.id); global = removeBlockedUser(global, bot.id);
setGlobal(global); setGlobal(global);
void sendBotCommand(chat, '/start', undefined, selectSendAs(global, chatId)); void sendBotCommand(chat, '/start', undefined, selectSendAs(global, chatId), lastMessageId);
}); });
addActionHandler('loadTopInlineBots', async (global): Promise<void> => { addActionHandler('loadTopInlineBots', async (global): Promise<void> => {
@ -442,6 +457,7 @@ addActionHandler('sharePhoneWithBot', async (global, actions, payload): Promise<
const currentUser = selectUser(global, global.currentUserId!)!; const currentUser = selectUser(global, global.currentUserId!)!;
if (!chat) return; if (!chat) return;
const lastMessageId = selectChatLastMessageId(global, chat.id);
await callApi('sendMessage', { await callApi('sendMessage', {
chat, chat,
@ -451,6 +467,7 @@ addActionHandler('sharePhoneWithBot', async (global, actions, payload): Promise<
phoneNumber: currentUser.phoneNumber || '', phoneNumber: currentUser.phoneNumber || '',
userId: currentUser.id, userId: currentUser.id,
}, },
lastMessageId,
}); });
}); });
@ -1069,13 +1086,14 @@ async function searchInlineBot<T extends GlobalState>(global: T, {
} }
async function sendBotCommand( async function sendBotCommand(
chat: ApiChat, command: string, replyInfo?: ApiInputMessageReplyInfo, sendAs?: ApiPeer, chat: ApiChat, command: string, replyInfo?: ApiInputMessageReplyInfo, sendAs?: ApiPeer, lastMessageId?: number,
) { ) {
await callApi('sendMessage', { await callApi('sendMessage', {
chat, chat,
replyInfo, replyInfo,
text: command, text: command,
sendAs, sendAs,
lastMessageId,
}); });
} }

View File

@ -4,7 +4,7 @@ import type {
} from '../../../api/types'; } from '../../../api/types';
import type { RequiredGlobalActions } from '../../index'; import type { RequiredGlobalActions } from '../../index';
import type { import type {
ActionReturnType, GlobalState, TabArgs, ActionReturnType, ChatListType, GlobalState, TabArgs,
} from '../../types'; } from '../../types';
import { MAIN_THREAD_ID } from '../../../api/types'; import { MAIN_THREAD_ID } from '../../../api/types';
import { import {
@ -12,6 +12,7 @@ import {
ManagementProgress, ManagementProgress,
NewChatMembersProgress, NewChatMembersProgress,
SettingsScreens, SettingsScreens,
type ThreadId,
} from '../../../types'; } from '../../../types';
import { import {
@ -20,6 +21,7 @@ import {
CHAT_LIST_LOAD_SLICE, CHAT_LIST_LOAD_SLICE,
DEBUG, DEBUG,
RE_TG_LINK, RE_TG_LINK,
SAVED_FOLDER_ID,
SERVICE_NOTIFICATIONS_USER_ID, SERVICE_NOTIFICATIONS_USER_ID,
TME_WEB_DOMAINS, TME_WEB_DOMAINS,
TMP_CHAT_ID, TMP_CHAT_ID,
@ -37,10 +39,10 @@ import { debounce, pause, throttle } from '../../../util/schedulers';
import { extractCurrentThemeParams } from '../../../util/themeStyle'; import { extractCurrentThemeParams } from '../../../util/themeStyle';
import { callApi } from '../../../api/gramjs'; import { callApi } from '../../../api/gramjs';
import { import {
getIsSavedDialog,
isChatArchived, isChatArchived,
isChatBasicGroup, isChatBasicGroup,
isChatChannel, isChatChannel,
isChatSummaryOnly,
isChatSuperGroup, isChatSuperGroup,
isUserBot, isUserBot,
toChannelId, toChannelId,
@ -58,6 +60,7 @@ import {
addUsersToRestrictedInviteList, addUsersToRestrictedInviteList,
deleteTopic, deleteTopic,
leaveChat, leaveChat,
removeChatFromChatLists,
replaceChatFullInfo, replaceChatFullInfo,
replaceChatListIds, replaceChatListIds,
replaceChats, replaceChats,
@ -66,9 +69,11 @@ import {
replaceUserStatuses, replaceUserStatuses,
updateChat, updateChat,
updateChatFullInfo, updateChatFullInfo,
updateChatLastMessageId,
updateChatListIds, updateChatListIds,
updateChatListSecondaryInfo, updateChatListSecondaryInfo,
updateChats, updateChats,
updateChatsLastMessageId,
updateListedTopicIds, updateListedTopicIds,
updateManagementProgress, updateManagementProgress,
updatePeerFullInfo, updatePeerFullInfo,
@ -85,11 +90,14 @@ import {
selectChatByUsername, selectChatByUsername,
selectChatFolder, selectChatFolder,
selectChatFullInfo, selectChatFullInfo,
selectChatLastMessage,
selectChatLastMessageId,
selectChatListType, selectChatListType,
selectCurrentChat, selectCurrentChat,
selectCurrentMessageList, selectCurrentMessageList,
selectDraft, selectDraft,
selectIsChatPinned, selectIsChatPinned,
selectIsChatWithSelf,
selectLastServiceNotification, selectLastServiceNotification,
selectStickerSet, selectStickerSet,
selectSupportChat, selectSupportChat,
@ -146,12 +154,12 @@ addActionHandler('preloadTopChatMessages', async (global, actions): Promise<void
} }
}); });
function abortChatRequests(chatId: string, threadId?: number) { function abortChatRequests(chatId: string, threadId?: ThreadId) {
callApi('abortChatRequests', { chatId, threadId }); callApi('abortChatRequests', { chatId, threadId });
} }
function abortChatRequestsForCurrentChat<T extends GlobalState>( function abortChatRequestsForCurrentChat<T extends GlobalState>(
global: T, newChatId?: string, newThreadId?: number, global: T, newChatId?: string, newThreadId?: ThreadId,
...[tabId = getCurrentTabId()]: TabArgs<T> ...[tabId = getCurrentTabId()]: TabArgs<T>
) { ) {
const currentMessageList = selectCurrentMessageList(global, tabId); const currentMessageList = selectCurrentMessageList(global, tabId);
@ -202,15 +210,16 @@ addActionHandler('openChat', (global, actions, payload): ActionReturnType => {
return; return;
} }
const { currentUserId } = global;
const chat = selectChat(global, id); const chat = selectChat(global, id);
if (chat?.hasUnreadMark) { if (chat?.hasUnreadMark) {
actions.toggleChatUnread({ id }); actions.toggleChatUnread({ id });
} }
const isChatOnlySummary = !selectChatLastMessageId(global, id);
if (!chat) { if (!chat) {
if (id === currentUserId) { if (selectIsChatWithSelf(global, id)) {
void callApi('fetchChat', { type: 'self' }); void callApi('fetchChat', { type: 'self' });
} else { } else {
const user = selectUser(global, id); const user = selectUser(global, id);
@ -218,12 +227,23 @@ addActionHandler('openChat', (global, actions, payload): ActionReturnType => {
void callApi('fetchChat', { type: 'user', user }); void callApi('fetchChat', { type: 'user', user });
} }
} }
} else if (isChatSummaryOnly(chat) && !chat.isMin) { } else if (isChatOnlySummary && !chat.isMin) {
actions.requestChatUpdate({ chatId: id }); actions.requestChatUpdate({ chatId: id });
} }
actions.closeStoryViewer({ tabId }); actions.closeStoryViewer({ tabId });
}); });
addActionHandler('openSavedDialog', (global, actions, payload): ActionReturnType => {
const { chatId, tabId = getCurrentTabId(), ...otherParams } = payload;
actions.openThread({
chatId: global.currentUserId!,
threadId: chatId,
tabId,
...otherParams,
});
});
addActionHandler('openThread', async (global, actions, payload): Promise<void> => { addActionHandler('openThread', async (global, actions, payload): Promise<void> => {
const { const {
type, isComments, noForumTopicPanel, shouldReplaceHistory, shouldReplaceLast, type, isComments, noForumTopicPanel, shouldReplaceHistory, shouldReplaceLast,
@ -231,9 +251,9 @@ addActionHandler('openThread', async (global, actions, payload): Promise<void> =
tabId = getCurrentTabId(), tabId = getCurrentTabId(),
} = payload; } = payload;
let { chatId } = payload; let { chatId } = payload;
let threadId: number | undefined; let threadId: ThreadId | undefined;
let loadingChatId: string; let loadingChatId: string;
let loadingThreadId: number; let loadingThreadId: ThreadId;
if (!isComments) { if (!isComments) {
loadingChatId = payload.chatId; loadingChatId = payload.chatId;
@ -251,7 +271,7 @@ addActionHandler('openThread', async (global, actions, payload): Promise<void> =
tabId, tabId,
}); });
return; return;
} else if (originalChat?.isForum) { } else if (originalChat?.isForum || (chatId && getIsSavedDialog(chatId, threadId, global.currentUserId))) {
actions.processOpenChatOrThread({ actions.processOpenChatOrThread({
chatId, chatId,
type, type,
@ -280,7 +300,7 @@ addActionHandler('openThread', async (global, actions, payload): Promise<void> =
if (chatId if (chatId
&& threadInfo?.threadId && threadInfo?.threadId
&& (isComments || (thread?.listedIds?.length && thread.listedIds.includes(threadInfo.threadId)))) { && (isComments || (thread?.listedIds?.length && thread.listedIds.includes(Number(threadInfo.threadId))))) {
global = updateTabState(global, { global = updateTabState(global, {
loadingThread: undefined, loadingThread: undefined,
}, tabId); }, tabId);
@ -306,7 +326,7 @@ addActionHandler('openThread', async (global, actions, payload): Promise<void> =
global = updateTabState(global, { global = updateTabState(global, {
loadingThread: { loadingThread: {
loadingChatId, loadingChatId,
loadingMessageId: loadingThreadId, loadingMessageId: Number(loadingThreadId),
}, },
}, tabId); }, tabId);
setGlobal(global); setGlobal(global);
@ -337,7 +357,7 @@ addActionHandler('openThread', async (global, actions, payload): Promise<void> =
const result = await callApi('fetchDiscussionMessage', { const result = await callApi('fetchDiscussionMessage', {
chat: selectChat(global, loadingChatId)!, chat: selectChat(global, loadingChatId)!,
messageId: loadingThreadId, messageId: Number(loadingThreadId),
}); });
global = getGlobal(); global = getGlobal();
@ -476,13 +496,13 @@ addActionHandler('openSupportChat', async (global, actions, payload): Promise<vo
}); });
addActionHandler('loadAllChats', async (global, actions, payload): Promise<void> => { addActionHandler('loadAllChats', async (global, actions, payload): Promise<void> => {
const listType = payload.listType as 'active' | 'archived'; const listType = payload.listType;
const { onReplace } = payload; const { onReplace } = payload;
let { shouldReplace } = payload; let { shouldReplace } = payload;
let i = 0; let i = 0;
const getOrderDate = (chat: ApiChat) => { const getOrderDate = (chat: ApiChat) => {
return chat.lastMessage?.date || chat.creationDate; return selectChatLastMessage(global, chat.id)?.date || chat.creationDate;
}; };
while (shouldReplace || !global.chats.isFullyLoaded[listType]) { while (shouldReplace || !global.chats.isFullyLoaded[listType]) {
@ -559,11 +579,12 @@ addActionHandler('loadTopChats', (): ActionReturnType => {
runThrottledForLoadTopChats(() => { runThrottledForLoadTopChats(() => {
loadChats('active'); loadChats('active');
loadChats('archived'); loadChats('archived');
loadChats('saved');
}); });
}); });
addActionHandler('requestChatUpdate', (global, actions, payload): ActionReturnType => { addActionHandler('requestChatUpdate', (global, actions, payload): ActionReturnType => {
const { chatId } = payload!; const { chatId } = payload;
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
if (!chat) { if (!chat) {
return; return;
@ -577,6 +598,49 @@ addActionHandler('requestChatUpdate', (global, actions, payload): ActionReturnTy
}); });
}); });
addActionHandler('requestSavedDialogUpdate', async (global, actions, payload): Promise<void> => {
const { chatId } = payload;
const chat = selectChat(global, chatId);
if (!chat) {
return;
}
const result = await callApi('fetchMessages', {
chat,
isSavedDialog: true,
limit: 1,
});
if (!result) return;
global = getGlobal();
global = addMessages(global, result.messages);
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
global = addChats(global, buildCollectionByKey(result.chats, 'id'));
if (result.messages.length) {
global = updateChatLastMessageId(global, chatId, result.messages[0].id, 'saved');
global = updateChatListIds(global, 'saved', [chatId]);
setGlobal(global);
} else {
global = removeChatFromChatLists(global, chatId, 'saved');
setGlobal(global);
Object.values(global.byTabId).forEach(({ id: tabId }) => {
const currentMessageList = selectCurrentMessageList(global, tabId);
if (!currentMessageList) return;
const { chatId: tabChatId, threadId } = currentMessageList;
if (selectIsChatWithSelf(global, tabChatId) && threadId === chatId) {
actions.openChat({ id: undefined, tabId });
}
});
}
});
addActionHandler('updateChatMutedState', (global, actions, payload): ActionReturnType => { addActionHandler('updateChatMutedState', (global, actions, payload): ActionReturnType => {
const { chatId, muteUntil = 0 } = payload; const { chatId, muteUntil = 0 } = payload;
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
@ -906,6 +970,28 @@ addActionHandler('toggleChatArchived', (global, actions, payload): ActionReturnT
} }
}); });
addActionHandler('toggleSavedDialogPinned', (global, actions, payload): ActionReturnType => {
const { id, tabId = getCurrentTabId() } = payload!;
const chat = selectChat(global, id);
if (!chat) {
return;
}
const limit = selectCurrentLimit(global, 'savedDialogsPinned');
const isPinned = selectIsChatPinned(global, id, SAVED_FOLDER_ID);
const ids = global.chats.orderedPinnedIds.saved;
if ((ids?.length || 0) >= limit && !isPinned) {
actions.openLimitReachedModal({
limit: 'savedDialogsPinned',
tabId,
});
return;
}
void callApi('toggleSavedDialogPinned', { chat, shouldBePinned: !isPinned });
});
addActionHandler('loadChatFolders', async (global): Promise<void> => { addActionHandler('loadChatFolders', async (global): Promise<void> => {
const chatFolders = await callApi('fetchChatFolders'); const chatFolders = await callApi('fetchChatFolders');
@ -1889,7 +1975,7 @@ addActionHandler('fetchChat', (global, actions, payload): ActionReturnType => {
return; return;
} }
if (chatId === global.currentUserId) { if (selectIsChatWithSelf(global, chatId)) {
void callApi('fetchChat', { type: 'self' }); void callApi('fetchChat', { type: 'self' });
} else { } else {
const user = selectUser(global, chatId); const user = selectUser(global, chatId);
@ -2545,7 +2631,7 @@ addActionHandler('fetchChannelRecommendations', async (global, actions, payload)
}); });
async function loadChats( async function loadChats(
listType: 'active' | 'archived', listType: ChatListType,
offsetId?: string, offsetId?: string,
offsetDate?: number, offsetDate?: number,
shouldReplace = false, shouldReplace = false,
@ -2553,13 +2639,17 @@ async function loadChats(
) { ) {
// eslint-disable-next-line eslint-multitab-tt/no-immediate-global // eslint-disable-next-line eslint-multitab-tt/no-immediate-global
let global = getGlobal(); let global = getGlobal();
let lastLocalServiceMessage = selectLastServiceNotification(global)?.message; let lastLocalServiceMessageId = selectLastServiceNotification(global)?.id;
const result = await callApi('fetchChats', { const result = listType === 'saved' ? await callApi('fetchSavedChats', {
limit: CHAT_LIST_LOAD_SLICE,
offsetDate,
withPinned: shouldReplace,
}) : await callApi('fetchChats', {
limit: CHAT_LIST_LOAD_SLICE, limit: CHAT_LIST_LOAD_SLICE,
offsetDate, offsetDate,
archived: listType === 'archived', archived: listType === 'archived',
withPinned: shouldReplace, withPinned: shouldReplace,
lastLocalServiceMessage, lastLocalServiceMessageId,
}); });
if (!result) { if (!result) {
@ -2573,63 +2663,56 @@ async function loadChats(
} }
global = getGlobal(); global = getGlobal();
lastLocalServiceMessageId = selectLastServiceNotification(global)?.id;
lastLocalServiceMessage = selectLastServiceNotification(global)?.message; if (shouldReplace) {
if (listType === 'active') {
// Always include service notifications chat
if (!chatIds.includes(SERVICE_NOTIFICATIONS_USER_ID)) {
const result2 = await callApi('fetchChat', {
type: 'user',
user: SERVICE_NOTIFICATIONS_USER_MOCK,
});
if (shouldReplace && listType === 'active') { global = getGlobal();
// Always include service notifications chat
if (!chatIds.includes(SERVICE_NOTIFICATIONS_USER_ID)) {
const result2 = await callApi('fetchChat', {
type: 'user',
user: SERVICE_NOTIFICATIONS_USER_MOCK,
});
global = getGlobal(); const notificationsChat = result2 && selectChat(global, result2.chatId);
if (notificationsChat) {
const notificationsChat = result2 && selectChat(global, result2.chatId); chatIds.unshift(notificationsChat.id);
if (notificationsChat) { result.chats.unshift(notificationsChat);
chatIds.unshift(notificationsChat.id); if (lastLocalServiceMessageId) {
result.chats.unshift(notificationsChat); result.lastMessageByChatId[notificationsChat.id] = lastLocalServiceMessageId;
if (lastLocalServiceMessage) { }
notificationsChat.lastMessage = lastLocalServiceMessage;
} }
} }
const tabStates = Object.values(global.byTabId);
const visibleChats = tabStates.flatMap(({ id: tabId }) => {
const currentChat = selectCurrentChat(global, tabId);
return currentChat ? [currentChat] : [];
});
const visibleUsers = tabStates.flatMap(({ id: tabId }) => {
return selectVisibleUsers(global, tabId) || [];
});
if (global.currentUserId && global.users.byId[global.currentUserId]) {
visibleUsers.push(global.users.byId[global.currentUserId]);
}
global = replaceUsers(global, buildCollectionByKey(visibleUsers.concat(result.users), 'id'));
global = replaceUserStatuses(global, result.userStatusesById);
global = replaceChats(global, buildCollectionByKey(visibleChats.concat(result.chats), 'id'));
global = replaceChatListIds(global, listType, chatIds);
} else {
// Archived and Saved
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
global = addUserStatuses(global, result.userStatusesById);
global = updateChats(global, buildCollectionByKey(result.chats, 'id'));
global = replaceChatListIds(global, listType, chatIds);
} }
const tabStates = Object.values(global.byTabId);
const visibleChats = tabStates.flatMap(({ id: tabId }) => {
const currentChat = selectCurrentChat(global, tabId);
return currentChat ? [currentChat] : [];
});
const visibleUsers = tabStates.flatMap(({ id: tabId }) => {
return selectVisibleUsers(global, tabId) || [];
});
if (global.currentUserId && global.users.byId[global.currentUserId]) {
visibleUsers.push(global.users.byId[global.currentUserId]);
}
global = replaceUsers(global, buildCollectionByKey(visibleUsers.concat(result.users), 'id'));
global = replaceUserStatuses(global, result.userStatusesById);
global = replaceChats(global, buildCollectionByKey(visibleChats.concat(result.chats), 'id'));
global = replaceChatListIds(global, listType, chatIds);
} else if (shouldReplace && listType === 'archived') {
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
global = addUserStatuses(global, result.userStatusesById);
global = updateChats(global, buildCollectionByKey(result.chats, 'id'));
global = replaceChatListIds(global, listType, chatIds);
} else { } else {
const newChats = buildCollectionByKey(result.chats, 'id'); const newChats = buildCollectionByKey(result.chats, 'id');
if (chatIds.includes(SERVICE_NOTIFICATIONS_USER_ID)) {
const notificationsChat = newChats[SERVICE_NOTIFICATIONS_USER_ID];
if (notificationsChat && lastLocalServiceMessage) {
newChats[SERVICE_NOTIFICATIONS_USER_ID] = {
...notificationsChat,
lastMessage: lastLocalServiceMessage,
};
}
}
global = addUsers(global, buildCollectionByKey(result.users, 'id')); global = addUsers(global, buildCollectionByKey(result.users, 'id'));
global = addUserStatuses(global, result.userStatusesById); global = addUserStatuses(global, result.userStatusesById);
@ -2638,6 +2721,8 @@ async function loadChats(
} }
global = updateChatListSecondaryInfo(global, listType, result); global = updateChatListSecondaryInfo(global, listType, result);
global = addMessages(global, result.messages);
global = updateChatsLastMessageId(global, result.lastMessageByChatId, listType);
const idsToUpdateDraft = isFullDraftSync ? result.chatIds : Object.keys(result.draftsById); const idsToUpdateDraft = isFullDraftSync ? result.chatIds : Object.keys(result.draftsById);
idsToUpdateDraft.forEach((chatId) => { idsToUpdateDraft.forEach((chatId) => {
@ -2653,7 +2738,7 @@ async function loadChats(
} }
}); });
if (chatIds.length === 0 && !global.chats.isFullyLoaded[listType]) { if ((chatIds.length === 0 || chatIds.length === result.totalChatCount) && !global.chats.isFullyLoaded[listType]) {
global = { global = {
...global, ...global,
chats: { chats: {
@ -2831,7 +2916,7 @@ async function openChatByUsername<T extends GlobalState>(
global: T, global: T,
actions: RequiredGlobalActions, actions: RequiredGlobalActions,
username: string, username: string,
threadId?: number, threadId?: ThreadId,
channelPostId?: number, channelPostId?: number,
startParam?: string, startParam?: string,
startAttach?: string, startAttach?: string,

View File

@ -1,11 +1,12 @@
import type { ApiChat } from '../../../api/types'; import type { ApiChat } from '../../../api/types';
import type { SharedMediaType } from '../../../types'; import type { SharedMediaType, ThreadId } from '../../../types';
import type { ActionReturnType, GlobalState, TabArgs } from '../../types'; import type { ActionReturnType, GlobalState, TabArgs } from '../../types';
import { MESSAGE_SEARCH_SLICE, SHARED_MEDIA_SLICE } from '../../../config'; import { MESSAGE_SEARCH_SLICE, SHARED_MEDIA_SLICE } from '../../../config';
import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { buildCollectionByKey } from '../../../util/iteratees'; import { buildCollectionByKey } from '../../../util/iteratees';
import { callApi } from '../../../api/gramjs'; import { callApi } from '../../../api/gramjs';
import { getIsSavedDialog } from '../../helpers';
import { import {
addActionHandler, getGlobal, setGlobal, addActionHandler, getGlobal, setGlobal,
} from '../../index'; } from '../../index';
@ -26,7 +27,14 @@ import {
addActionHandler('searchTextMessagesLocal', async (global, actions, payload): Promise<void> => { addActionHandler('searchTextMessagesLocal', async (global, actions, payload): Promise<void> => {
const { tabId = getCurrentTabId() } = payload || {}; const { tabId = getCurrentTabId() } = payload || {};
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {}; const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
const chat = chatId ? selectChat(global, chatId) : undefined;
if (!chatId) return;
const currentUserId = global.currentUserId!;
const isSavedDialog = getIsSavedDialog(chatId, threadId, currentUserId);
const realChatId = isSavedDialog ? String(threadId) : chatId;
const chat = realChatId ? selectChat(global, realChatId) : undefined;
let currentSearch = selectCurrentTextSearch(global, tabId); let currentSearch = selectCurrentTextSearch(global, tabId);
if (!chat || !currentSearch || !threadId) { if (!chat || !currentSearch || !threadId) {
return; return;
@ -46,6 +54,7 @@ addActionHandler('searchTextMessagesLocal', async (global, actions, payload): Pr
threadId, threadId,
limit: MESSAGE_SEARCH_SLICE, limit: MESSAGE_SEARCH_SLICE,
offsetId, offsetId,
isSavedDialog,
}); });
if (!result) { if (!result) {
@ -66,10 +75,12 @@ addActionHandler('searchTextMessagesLocal', async (global, actions, payload): Pr
return; return;
} }
const resultChatId = isSavedDialog ? currentUserId : chat.id;
global = addChats(global, buildCollectionByKey(chats, 'id')); global = addChats(global, buildCollectionByKey(chats, 'id'));
global = addUsers(global, buildCollectionByKey(users, 'id')); global = addUsers(global, buildCollectionByKey(users, 'id'));
global = addChatMessagesById(global, chat.id, byId); global = addChatMessagesById(global, resultChatId, byId);
global = updateLocalTextSearchResults(global, chat.id, threadId, newFoundIds, totalCount, nextOffsetId, tabId); global = updateLocalTextSearchResults(global, resultChatId, threadId, newFoundIds, totalCount, nextOffsetId, tabId);
setGlobal(global); setGlobal(global);
}); });
@ -80,7 +91,10 @@ addActionHandler('searchMediaMessagesLocal', (global, actions, payload): ActionR
return; return;
} }
const chat = selectChat(global, chatId); const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
const realChatId = isSavedDialog ? String(threadId) : chatId;
const chat = selectChat(global, realChatId);
const currentSearch = selectCurrentMediaSearch(global, tabId); const currentSearch = selectCurrentMediaSearch(global, tabId);
if (!chat || !currentSearch) { if (!chat || !currentSearch) {
@ -95,7 +109,7 @@ addActionHandler('searchMediaMessagesLocal', (global, actions, payload): ActionR
return; return;
} }
void searchSharedMedia(global, chat, threadId, type, offsetId, undefined, tabId); void searchSharedMedia(global, chat, threadId, type, offsetId, undefined, isSavedDialog, tabId);
}); });
addActionHandler('searchMessagesByDate', async (global, actions, payload): Promise<void> => { addActionHandler('searchMessagesByDate', async (global, actions, payload): Promise<void> => {
@ -130,18 +144,22 @@ addActionHandler('searchMessagesByDate', async (global, actions, payload): Promi
async function searchSharedMedia<T extends GlobalState>( async function searchSharedMedia<T extends GlobalState>(
global: T, global: T,
chat: ApiChat, chat: ApiChat,
threadId: number, threadId: ThreadId,
type: SharedMediaType, type: SharedMediaType,
offsetId?: number, offsetId?: number,
isBudgetPreload = false, isBudgetPreload = false,
isSavedDialog?: boolean,
...[tabId = getCurrentTabId()]: TabArgs<T> ...[tabId = getCurrentTabId()]: TabArgs<T>
) { ) {
const resultChatId = isSavedDialog ? global.currentUserId! : chat.id;
const result = await callApi('searchMessagesLocal', { const result = await callApi('searchMessagesLocal', {
chat, chat,
type, type,
limit: SHARED_MEDIA_SLICE * 2, limit: SHARED_MEDIA_SLICE * 2,
threadId, threadId,
offsetId, offsetId,
isSavedDialog,
}); });
if (!result) { if (!result) {
@ -164,11 +182,13 @@ async function searchSharedMedia<T extends GlobalState>(
global = addChats(global, buildCollectionByKey(chats, 'id')); global = addChats(global, buildCollectionByKey(chats, 'id'));
global = addUsers(global, buildCollectionByKey(users, 'id')); global = addUsers(global, buildCollectionByKey(users, 'id'));
global = addChatMessagesById(global, chat.id, byId); global = addChatMessagesById(global, resultChatId, byId);
global = updateLocalMediaSearchResults(global, chat.id, threadId, type, newFoundIds, totalCount, nextOffsetId, tabId); global = updateLocalMediaSearchResults(
global, resultChatId, threadId, type, newFoundIds, totalCount, nextOffsetId, tabId,
);
setGlobal(global); setGlobal(global);
if (!isBudgetPreload) { if (!isBudgetPreload) {
void searchSharedMedia(global, chat, threadId, type, nextOffsetId, true, tabId); void searchSharedMedia(global, chat, threadId, type, nextOffsetId, true, isSavedDialog, tabId);
} }
} }

View File

@ -19,7 +19,7 @@ import type {
ActionReturnType, ApiDraft, GlobalState, TabArgs, ActionReturnType, ApiDraft, GlobalState, TabArgs,
} from '../../types'; } from '../../types';
import { MAIN_THREAD_ID, MESSAGE_DELETED } from '../../../api/types'; import { MAIN_THREAD_ID, MESSAGE_DELETED } from '../../../api/types';
import { LoadMoreDirection } from '../../../types'; import { LoadMoreDirection, type ThreadId } from '../../../types';
import { import {
GIF_MIME_TYPE, GIF_MIME_TYPE,
@ -44,6 +44,7 @@ import {
import { IS_IOS } from '../../../util/windowEnvironment'; import { IS_IOS } from '../../../util/windowEnvironment';
import { callApi, cancelApiProgress } from '../../../api/gramjs'; import { callApi, cancelApiProgress } from '../../../api/gramjs';
import { import {
getIsSavedDialog,
getMessageOriginalId, getMessageOriginalId,
getUserFullName, getUserFullName,
isChatChannel, isChatChannel,
@ -83,6 +84,7 @@ import { updateTabState } from '../../reducers/tabs';
import { import {
selectChat, selectChat,
selectChatFullInfo, selectChatFullInfo,
selectChatLastMessageId,
selectChatMessage, selectChatMessage,
selectCurrentChat, selectCurrentChat,
selectCurrentMessageList, selectCurrentMessageList,
@ -96,6 +98,7 @@ import {
selectFocusedMessageId, selectFocusedMessageId,
selectForwardsCanBeSentToChat, selectForwardsCanBeSentToChat,
selectForwardsContainVoiceMessages, selectForwardsContainVoiceMessages,
selectIsChatWithSelf,
selectIsCurrentUserPremium, selectIsCurrentUserPremium,
selectLanguageCode, selectLanguageCode,
selectListedIds, selectListedIds,
@ -227,7 +230,7 @@ async function loadWithBudget<T extends GlobalState>(
global: T, global: T,
actions: RequiredGlobalActions, actions: RequiredGlobalActions,
areAllLocal: boolean, isOutlying: boolean, isBudgetPreload: boolean, areAllLocal: boolean, isOutlying: boolean, isBudgetPreload: boolean,
chat: ApiChat, threadId: number, direction: LoadMoreDirection, offsetId?: number, chat: ApiChat, threadId: ThreadId, direction: LoadMoreDirection, offsetId?: number,
onLoaded?: NoneToVoidFunction, onLoaded?: NoneToVoidFunction,
...[tabId = getCurrentTabId()]: TabArgs<T> ...[tabId = getCurrentTabId()]: TabArgs<T>
) { ) {
@ -308,6 +311,7 @@ addActionHandler('sendMessage', (global, actions, payload): ActionReturnType =>
const messageReplyInfo = selectMessageReplyInfo(global, chatId!, threadId!, draftReplyInfo); const messageReplyInfo = selectMessageReplyInfo(global, chatId!, threadId!, draftReplyInfo);
const replyInfo = storyReplyInfo || messageReplyInfo; const replyInfo = storyReplyInfo || messageReplyInfo;
const lastMessageId = selectChatLastMessageId(global, chatId!);
const params = { const params = {
...payload, ...payload,
@ -315,6 +319,7 @@ addActionHandler('sendMessage', (global, actions, payload): ActionReturnType =>
replyInfo, replyInfo,
noWebPage: selectNoWebPage(global, chatId!, threadId!), noWebPage: selectNoWebPage(global, chatId!, threadId!),
sendAs: selectSendAs(global, chatId!), sendAs: selectSendAs(global, chatId!),
lastMessageId,
}; };
if (!isStoryReply) { if (!isStoryReply) {
@ -545,7 +550,7 @@ addActionHandler('resetDraftReplyInfo', (global, actions, payload): ActionReturn
async function saveDraft<T extends GlobalState>({ async function saveDraft<T extends GlobalState>({
global, chatId, threadId, draft, isLocalOnly, noLocalTimeUpdate, global, chatId, threadId, draft, isLocalOnly, noLocalTimeUpdate,
} : { } : {
global: T; chatId: string; threadId: number; draft?: ApiDraft; isLocalOnly?: boolean; noLocalTimeUpdate?: boolean; global: T; chatId: string; threadId: ThreadId; draft?: ApiDraft; isLocalOnly?: boolean; noLocalTimeUpdate?: boolean;
}) { }) {
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
const user = selectUser(global, chatId); const user = selectUser(global, chatId);
@ -732,7 +737,7 @@ addActionHandler('reportMessages', async (global, actions, payload): Promise<voi
addActionHandler('sendMessageAction', async (global, actions, payload): Promise<void> => { addActionHandler('sendMessageAction', async (global, actions, payload): Promise<void> => {
const { action, chatId, threadId } = payload!; const { action, chatId, threadId } = payload!;
if (global.connectionState !== 'connectionStateReady') return; if (global.connectionState !== 'connectionStateReady') return;
if (chatId === global.currentUserId) return; // Message actions are disabled in Saved Messages if (selectIsChatWithSelf(global, chatId)) return;
const chat = selectChat(global, chatId)!; const chat = selectChat(global, chatId)!;
if (!chat) return; if (!chat) return;
@ -754,7 +759,7 @@ addActionHandler('markMessageListRead', (global, actions, payload): ActionReturn
const { chatId, threadId } = currentMessageList; const { chatId, threadId } = currentMessageList;
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
if (!chat) { if (!chat || getIsSavedDialog(chatId, threadId, global.currentUserId)) {
return undefined; return undefined;
} }
@ -803,7 +808,7 @@ addActionHandler('markMessageListRead', (global, actions, payload): ActionReturn
unreadCount: Math.max(0, chat.unreadCount - 1), unreadCount: Math.max(0, chat.unreadCount - 1),
}); });
} }
return updateTopic(global, chatId, threadId, { return updateTopic(global, chatId, Number(threadId), {
unreadCount: newTopicUnreadCount, unreadCount: newTopicUnreadCount,
}); });
} }
@ -951,6 +956,7 @@ addActionHandler('forwardMessages', (global, actions, payload): ActionReturnType
const sendAs = selectSendAs(global, toChatId!); const sendAs = selectSendAs(global, toChatId!);
const draft = selectDraft(global, toChatId!, toThreadId || MAIN_THREAD_ID); const draft = selectDraft(global, toChatId!, toThreadId || MAIN_THREAD_ID);
const lastMessageId = selectChatLastMessageId(global, toChat.id);
const [realMessages, serviceMessages] = partition(messages, (m) => !isServiceNotificationMessage(m)); const [realMessages, serviceMessages] = partition(messages, (m) => !isServiceNotificationMessage(m));
if (realMessages.length) { if (realMessages.length) {
@ -969,6 +975,7 @@ addActionHandler('forwardMessages', (global, actions, payload): ActionReturnType
noCaptions, noCaptions,
isCurrentUserPremium, isCurrentUserPremium,
wasDrafted: Boolean(draft), wasDrafted: Boolean(draft),
lastMessageId,
}); });
})(); })();
} }
@ -990,6 +997,7 @@ addActionHandler('forwardMessages', (global, actions, payload): ActionReturnType
isSilent, isSilent,
scheduledAt, scheduledAt,
sendAs, sendAs,
lastMessageId,
}); });
}); });
@ -1021,7 +1029,7 @@ addActionHandler('loadScheduledHistory', async (global, actions, payload): Promi
global = replaceScheduledMessages(global, chat.id, byId); global = replaceScheduledMessages(global, chat.id, byId);
global = replaceThreadParam(global, chat.id, MAIN_THREAD_ID, 'scheduledIds', ids); global = replaceThreadParam(global, chat.id, MAIN_THREAD_ID, 'scheduledIds', ids);
if (chat?.isForum) { if (chat?.isForum) {
const scheduledPerThread: Record<number, number[]> = {}; const scheduledPerThread: Record<ThreadId, number[]> = {};
messages.forEach((message) => { messages.forEach((message) => {
const threadId = selectThreadIdFromMessage(global, message); const threadId = selectThreadIdFromMessage(global, message);
const scheduledInThread = scheduledPerThread[threadId] || []; const scheduledInThread = scheduledPerThread[threadId] || [];
@ -1121,7 +1129,7 @@ addActionHandler('loadCustomEmojis', async (global, actions, payload): Promise<v
async function loadViewportMessages<T extends GlobalState>( async function loadViewportMessages<T extends GlobalState>(
global: T, global: T,
chat: ApiChat, chat: ApiChat,
threadId: number, threadId: ThreadId,
offsetId: number | undefined, offsetId: number | undefined,
direction: LoadMoreDirection, direction: LoadMoreDirection,
isOutlying = false, isOutlying = false,
@ -1154,12 +1162,18 @@ async function loadViewportMessages<T extends GlobalState>(
} }
global = getGlobal(); global = getGlobal();
const currentUserId = global.currentUserId!;
const isSavedDialog = getIsSavedDialog(chatId, threadId, currentUserId);
const realChatId = isSavedDialog ? String(threadId) : chatId;
const result = await callApi('fetchMessages', { const result = await callApi('fetchMessages', {
chat: selectChat(global, chatId)!, chat: selectChat(global, realChatId)!,
offsetId, offsetId,
addOffset, addOffset,
limit: sliceSize, limit: sliceSize,
threadId, threadId,
isSavedDialog,
}); });
if (!result) { if (!result) {
@ -1179,10 +1193,10 @@ async function loadViewportMessages<T extends GlobalState>(
const byId = buildCollectionByKey(allMessages, 'id'); const byId = buildCollectionByKey(allMessages, 'id');
const ids = Object.keys(byId).map(Number); const ids = Object.keys(byId).map(Number);
if (threadId !== MAIN_THREAD_ID) { if (threadId !== MAIN_THREAD_ID && !getIsSavedDialog(chatId, threadId, global.currentUserId)) {
const threadFirstMessageId = selectFirstMessageId(global, chatId, threadId); const threadFirstMessageId = selectFirstMessageId(global, chatId, threadId);
if ((!ids[0] || threadFirstMessageId === ids[0]) && threadFirstMessageId !== threadId) { if ((!ids[0] || threadFirstMessageId === ids[0]) && threadFirstMessageId !== threadId) {
ids.unshift(threadId); ids.unshift(Number(threadId));
} }
} }
@ -1314,6 +1328,7 @@ async function sendMessage<T extends GlobalState>(global: T, params: {
sendAs?: ApiPeer; sendAs?: ApiPeer;
groupedId?: string; groupedId?: string;
wasDrafted?: boolean; wasDrafted?: boolean;
lastMessageId?: number;
}) { }) {
let localId: number | undefined; let localId: number | undefined;
const progressCallback = params.attachment ? (progress: number, messageLocalId: number) => { const progressCallback = params.attachment ? (progress: number, messageLocalId: number) => {
@ -1351,7 +1366,7 @@ async function sendMessage<T extends GlobalState>(global: T, params: {
addActionHandler('loadPinnedMessages', async (global, actions, payload): Promise<void> => { addActionHandler('loadPinnedMessages', async (global, actions, payload): Promise<void> => {
const { chatId, threadId } = payload; const { chatId, threadId } = payload;
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
if (!chat) { if (!chat || getIsSavedDialog(chatId, threadId, global.currentUserId)) {
return; return;
} }
@ -1651,12 +1666,15 @@ addActionHandler('forwardStory', (global, actions, payload): ActionReturnType =>
return; return;
} }
const lastMessageId = selectChatLastMessageId(global, toChatId);
const { text, entities } = (story as ApiStory).content.text || {}; const { text, entities } = (story as ApiStory).content.text || {};
void sendMessage(global, { void sendMessage(global, {
chat: toChat, chat: toChat,
text, text,
entities, entities,
story, story,
lastMessageId,
}); });
global = getGlobal(); global = getGlobal();

View File

@ -25,6 +25,7 @@ import {
selectChatMessage, selectChatMessage,
selectCurrentChat, selectCurrentChat,
selectDefaultReaction, selectDefaultReaction,
selectIsChatWithSelf,
selectMaxUserReactions, selectMaxUserReactions,
selectMessageIdsByGroupId, selectMessageIdsByGroupId,
selectPerformanceSettingsValue, selectPerformanceSettingsValue,
@ -98,7 +99,7 @@ addActionHandler('sendEmojiInteraction', (global, actions, payload): ActionRetur
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
if (!chat || !emoji || chatId === global.currentUserId) { if (!chat || !emoji || selectIsChatWithSelf(global, chatId)) {
return; return;
} }
@ -314,7 +315,7 @@ addActionHandler('sendWatchingEmojiInteraction', (global, actions, payload): Act
const tabState = selectTabState(global, tabId); const tabState = selectTabState(global, tabId);
if (!chat || !tabState.activeEmojiInteractions?.some((interaction) => interaction.id === id) if (!chat || !tabState.activeEmojiInteractions?.some((interaction) => interaction.id === id)
|| chatId === global.currentUserId) { || selectIsChatWithSelf(global, chatId)) {
return undefined; return undefined;
} }

View File

@ -1,6 +1,6 @@
import { addCallback } from '../../../lib/teact/teactn'; import { addCallback } from '../../../lib/teact/teactn';
import type { ApiChat } from '../../../api/types'; import type { ThreadId } from '../../../types';
import type { RequiredGlobalActions } from '../../index'; import type { RequiredGlobalActions } from '../../index';
import type { ActionReturnType, GlobalState, Thread } from '../../types'; import type { ActionReturnType, GlobalState, Thread } from '../../types';
import { MAIN_THREAD_ID } from '../../../api/types'; import { MAIN_THREAD_ID } from '../../../api/types';
@ -11,11 +11,13 @@ import {
buildCollectionByKey, omitUndefined, pick, unique, buildCollectionByKey, omitUndefined, pick, unique,
} from '../../../util/iteratees'; } from '../../../util/iteratees';
import { callApi } from '../../../api/gramjs'; import { callApi } from '../../../api/gramjs';
import { getIsSavedDialog } from '../../helpers';
import { import {
addActionHandler, getActions, getGlobal, setGlobal, addActionHandler, getActions, getGlobal, setGlobal,
} from '../../index'; } from '../../index';
import { import {
addChatMessagesById, addChatMessagesById,
addMessages,
safeReplaceViewportIds, safeReplaceViewportIds,
updateChats, updateChats,
updateListedIds, updateListedIds,
@ -25,6 +27,7 @@ import {
} from '../../reducers'; } from '../../reducers';
import { updateTabState } from '../../reducers/tabs'; import { updateTabState } from '../../reducers/tabs';
import { import {
selectChat,
selectChatMessage, selectChatMessage,
selectChatMessages, selectChatMessages,
selectCurrentMessageList, selectCurrentMessageList,
@ -87,6 +90,7 @@ addActionHandler('sync', (global, actions): ActionReturnType => {
initFolderManager(); initFolderManager();
loadAllChats({ listType: 'archived', shouldReplace: true }); loadAllChats({ listType: 'archived', shouldReplace: true });
loadAllChats({ listType: 'saved', shouldReplace: true });
void callApi('fetchCurrentUser'); void callApi('fetchCurrentUser');
preloadTopChatMessages(); preloadTopChatMessages();
loadAllStories(); loadAllStories();
@ -121,6 +125,14 @@ async function loadAndReplaceMessages<T extends GlobalState>(global: T, actions:
}, {}); }, {});
/* eslint-enable @typescript-eslint/indent */ /* eslint-enable @typescript-eslint/indent */
// 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);
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) || {};
@ -131,14 +143,15 @@ async function loadAndReplaceMessages<T extends GlobalState>(global: T, actions:
if (currentChatId && currentChat) { if (currentChatId && currentChat) {
const [result, resultDiscussion] = await Promise.all([ const [result, resultDiscussion] = await Promise.all([
loadTopMessages( loadTopMessages(
currentChat, global,
currentChatId,
activeThreadId, activeThreadId,
activeThreadId !== MAIN_THREAD_ID ? activeThreadId : undefined,
), ),
activeThreadId !== MAIN_THREAD_ID ? callApi('fetchDiscussionMessage', { activeThreadId !== MAIN_THREAD_ID && !getIsSavedDialog(currentChat.id, activeThreadId, global.currentUserId)
chat: currentChat, ? callApi('fetchDiscussionMessage', {
messageId: activeThreadId, chat: currentChat,
}) : undefined, messageId: Number(activeThreadId),
}) : undefined,
]); ]);
global = getGlobal(); global = getGlobal();
const { chatId: newCurrentChatId } = selectCurrentMessageList(global, tabId) || {}; const { chatId: newCurrentChatId } = selectCurrentMessageList(global, tabId) || {};
@ -212,7 +225,7 @@ async function loadAndReplaceMessages<T extends GlobalState>(global: T, actions:
actions.loadTopics({ chatId: currentChatId!, force: true }); actions.loadTopics({ chatId: currentChatId!, force: true });
if (currentThreadId && currentThreadId !== MAIN_THREAD_ID) { if (currentThreadId && currentThreadId !== MAIN_THREAD_ID) {
actions.loadTopicById({ actions.loadTopicById({
chatId: currentChatId!, topicId: currentThreadId, shouldCloseChatOnError: true, chatId: currentChatId!, topicId: Number(currentThreadId), shouldCloseChatOnError: true,
}); });
} }
} }
@ -245,6 +258,10 @@ async function loadAndReplaceMessages<T extends GlobalState>(global: T, actions:
}); });
}); });
// 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 }) => {
@ -255,13 +272,20 @@ async function loadAndReplaceMessages<T extends GlobalState>(global: T, actions:
}); });
} }
function loadTopMessages(chat: ApiChat, threadId: number, offsetId?: number) { function loadTopMessages<T extends GlobalState>(global: T, chatId: string, threadId: ThreadId) {
const currentUserId = global.currentUserId!;
const isSavedDialog = getIsSavedDialog(chatId, threadId, currentUserId);
const realChatId = isSavedDialog ? String(threadId) : chatId;
const chat = selectChat(global, realChatId)!;
return callApi('fetchMessages', { return callApi('fetchMessages', {
chat, chat,
threadId, threadId,
offsetId: offsetId || chat.lastReadInboxMessageId, offsetId: !isSavedDialog ? chat.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,
}); });
} }

View File

@ -219,6 +219,21 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
}; };
} }
case 'updatePinnedSavedDialogIds': {
const { ids } = update;
return {
...global,
chats: {
...global.chats,
orderedPinnedIds: {
...global.chats.orderedPinnedIds,
saved: ids.length ? ids : undefined,
},
},
};
}
case 'updateChatPinned': { case 'updateChatPinned': {
const { id, isPinned } = update; const { id, isPinned } = update;
const listType = selectChatListType(global, id); const listType = selectChatListType(global, id);
@ -256,6 +271,30 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
}; };
} }
case 'updateSavedDialogPinned': {
const { id, isPinned } = update;
const { saved: orderedPinnedIds } = global.chats.orderedPinnedIds;
let newOrderedPinnedIds = orderedPinnedIds || [];
if (!isPinned) {
newOrderedPinnedIds = newOrderedPinnedIds.filter((pinnedId) => pinnedId !== id);
} else if (!newOrderedPinnedIds.includes(id)) {
newOrderedPinnedIds = [id, ...newOrderedPinnedIds];
}
return {
...global,
chats: {
...global.chats,
orderedPinnedIds: {
...global.chats.orderedPinnedIds,
saved: newOrderedPinnedIds.length ? newOrderedPinnedIds : undefined,
},
},
};
}
case 'updateChatListType': { case 'updateChatListType': {
const { id, folderId } = update; const { id, folderId } = update;

View File

@ -1,6 +1,7 @@
import type { import type {
ApiChat, ApiMessage, ApiPollResult, ApiReactions, ApiChat, ApiMessage, ApiPollResult, ApiReactions,
} from '../../../api/types'; } from '../../../api/types';
import type { ThreadId } from '../../../types';
import type { RequiredGlobalActions } from '../../index'; import type { RequiredGlobalActions } from '../../index';
import type { import type {
ActionReturnType, ActiveEmojiInteraction, GlobalState, RequiredGlobalState, ActionReturnType, ActiveEmojiInteraction, GlobalState, RequiredGlobalState,
@ -14,12 +15,13 @@ import { omit, pickTruthy, unique } from '../../../util/iteratees';
import { notifyAboutMessage } from '../../../util/notifications'; import { notifyAboutMessage } from '../../../util/notifications';
import { onTickEnd } from '../../../util/schedulers'; import { onTickEnd } from '../../../util/schedulers';
import { import {
checkIfHasUnreadReactions, getMessageContent, getMessageText, isActionMessage, checkIfHasUnreadReactions, getIsSavedDialog, getMessageContent, getMessageText, isActionMessage,
isMessageLocal, isUserId, isMessageLocal, isUserId,
} from '../../helpers'; } from '../../helpers';
import { getMessageReplyInfo, getStoryReplyInfo } from '../../helpers/replies'; import { getMessageReplyInfo, getStoryReplyInfo } from '../../helpers/replies';
import { addActionHandler, getGlobal, setGlobal } from '../../index'; import { addActionHandler, getGlobal, setGlobal } from '../../index';
import { import {
addMessages,
addViewportId, addViewportId,
clearMessageTranslation, clearMessageTranslation,
deleteChatMessages, deleteChatMessages,
@ -28,6 +30,7 @@ import {
removeChatFromChatLists, removeChatFromChatLists,
replaceThreadParam, replaceThreadParam,
updateChat, updateChat,
updateChatLastMessageId,
updateChatMessage, updateChatMessage,
updateListedIds, updateListedIds,
updateMessageTranslations, updateMessageTranslations,
@ -41,6 +44,7 @@ import { updateUnreadReactions } from '../../reducers/reactions';
import { updateTabState } from '../../reducers/tabs'; import { updateTabState } from '../../reducers/tabs';
import { import {
selectChat, selectChat,
selectChatLastMessageId,
selectChatMessage, selectChatMessage,
selectChatMessageByPollId, selectChatMessageByPollId,
selectChatMessages, selectChatMessages,
@ -49,11 +53,13 @@ import {
selectCurrentMessageList, selectCurrentMessageList,
selectFirstUnreadId, selectFirstUnreadId,
selectIsChatListed, selectIsChatListed,
selectIsChatWithSelf,
selectIsMessageInCurrentMessageList, selectIsMessageInCurrentMessageList,
selectIsServiceChatReady, selectIsServiceChatReady,
selectIsViewportNewest, selectIsViewportNewest,
selectListedIds, selectListedIds,
selectPinnedIds, selectPinnedIds,
selectSavedDialogIdFromMessage,
selectScheduledIds, selectScheduledIds,
selectScheduledMessage, selectScheduledMessage,
selectSendAs, selectSendAs,
@ -87,8 +93,9 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
actions.loadTopicById({ chatId, topicId: replyInfo.replyToMsgId }); actions.loadTopicById({ chatId, topicId: replyInfo.replyToMsgId });
} }
const isLocal = isMessageLocal(message as ApiMessage);
Object.values(global.byTabId).forEach(({ id: tabId }) => { Object.values(global.byTabId).forEach(({ id: tabId }) => {
const isLocal = isMessageLocal(message as ApiMessage);
// Force update for last message on drafted messages to prevent flickering // Force update for last message on drafted messages to prevent flickering
if (isLocal && wasDrafted) { if (isLocal && wasDrafted) {
global = updateChatLastMessage(global, chatId, newMessage); global = updateChatLastMessage(global, chatId, newMessage);
@ -138,6 +145,22 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
actions.loadTopChats(); actions.loadTopChats();
} }
if (selectIsChatWithSelf(global, chatId) && !isLocal) {
const savedDialogId = selectSavedDialogIdFromMessage(global, newMessage);
if (savedDialogId && !selectIsChatListed(global, savedDialogId, 'saved')) {
actions.requestSavedDialogUpdate({ chatId: savedDialogId });
}
}
break;
}
case 'updateChatLastMessage': {
const { id, lastMessage } = update;
global = updateChatLastMessage(global, id, lastMessage);
global = addMessages(global, [lastMessage]);
setGlobal(global);
break; break;
} }
@ -197,10 +220,6 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
const newMessage = selectChatMessage(global, chatId, id)!; const newMessage = selectChatMessage(global, chatId, id)!;
if (currentMessage) {
global = updateChatLastMessage(global, chatId, newMessage);
}
if (message.reactions && chat) { if (message.reactions && chat) {
global = updateReactions(global, chatId, id, message.reactions, chat, newMessage.isOutgoing, currentMessage); global = updateReactions(global, chatId, id, message.reactions, chat, newMessage.isOutgoing, currentMessage);
} }
@ -289,6 +308,13 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
lastReadInboxMessageId: message.id, lastReadInboxMessageId: message.id,
}); });
if (selectIsChatWithSelf(global, chatId)) {
const savedDialogId = selectSavedDialogIdFromMessage(global, newMessage);
if (savedDialogId && !selectIsChatListed(global, savedDialogId, 'saved')) {
actions.requestSavedDialogUpdate({ chatId: savedDialogId });
}
}
setGlobal(global); setGlobal(global);
break; break;
@ -322,7 +348,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
const { chatId, isPinned, messageIds } = update; const { chatId, isPinned, messageIds } = update;
const messages = pickTruthy(selectChatMessages(global, chatId), messageIds); const messages = pickTruthy(selectChatMessages(global, chatId), messageIds);
const updatePerThread: Record<number, number[]> = { const updatePerThread: Record<ThreadId, number[]> = {
[MAIN_THREAD_ID]: messageIds, [MAIN_THREAD_ID]: messageIds,
}; };
Object.values(messages).forEach((message) => { Object.values(messages).forEach((message) => {
@ -361,7 +387,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
const currentThreadInfo = selectThreadInfo(global, chatId, threadId); const currentThreadInfo = selectThreadInfo(global, chatId, threadId);
if (chat?.isForum && threadInfo.lastReadInboxMessageId !== currentThreadInfo?.lastReadInboxMessageId) { if (chat?.isForum && threadInfo.lastReadInboxMessageId !== currentThreadInfo?.lastReadInboxMessageId) {
actions.loadTopicById({ chatId, topicId: threadId }); actions.loadTopicById({ chatId, topicId: Number(threadId) });
} }
// Update reply thread last read message id if already read in main thread // Update reply thread last read message id if already read in main thread
@ -445,10 +471,6 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
const chatId = selectCommonBoxChatId(global, id); const chatId = selectCommonBoxChatId(global, id);
if (chatId) { if (chatId) {
global = updateChatMessage(global, chatId, id, messageUpdate); global = updateChatMessage(global, chatId, id, messageUpdate);
const message = selectChatMessage(global, chatId, id);
if (message) {
global = updateChatLastMessage(global, chatId, message);
}
} }
}); });
@ -785,15 +807,17 @@ function updateListedAndViewportIds<T extends GlobalState>(
) { ) {
const { id, chatId } = message; const { id, chatId } = message;
const savedDialogId = selectSavedDialogIdFromMessage(global, message);
const { threadInfo } = selectThreadByMessage(global, message) || {}; const { threadInfo } = selectThreadByMessage(global, message) || {};
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
const isUnreadChatNotLoaded = chat?.unreadCount && !selectListedIds(global, chatId, MAIN_THREAD_ID); const isUnreadChatNotLoaded = chat?.unreadCount && !selectListedIds(global, chatId, MAIN_THREAD_ID);
global = updateThreadUnread(global, actions, message); global = updateThreadUnread(global, actions, message);
const { threadId } = threadInfo ?? {}; const { threadId } = threadInfo ?? { threadId: savedDialogId };
if (threadInfo && threadId) { if (threadId) {
global = updateListedIds(global, chatId, threadId, [id]); global = updateListedIds(global, chatId, threadId, [id]);
Object.values(global.byTabId).forEach(({ id: tabId }) => { Object.values(global.byTabId).forEach(({ id: tabId }) => {
@ -809,15 +833,17 @@ function updateListedAndViewportIds<T extends GlobalState>(
} }
}); });
global = replaceThreadParam(global, chatId, threadId, 'threadInfo', { if (threadInfo) {
...threadInfo, global = replaceThreadParam(global, chatId, threadId, 'threadInfo', {
lastMessageId: message.id, ...threadInfo,
}); lastMessageId: message.id,
if (!isMessageLocal(message) && !isActionMessage(message)) {
global = updateThreadInfo(global, chatId, threadId, {
messagesCount: (threadInfo.messagesCount || 0) + 1,
}); });
if (!isMessageLocal(message) && !isActionMessage(message)) {
global = updateThreadInfo(global, chatId, threadId, {
messagesCount: (threadInfo.messagesCount || 0) + 1,
});
}
} }
} }
@ -851,7 +877,7 @@ function updateChatLastMessage<T extends GlobalState>(
) { ) {
const { chats } = global; const { chats } = global;
const chat = chats.byId[chatId]; const chat = chats.byId[chatId];
const currentLastMessage = chat?.lastMessage; const currentLastMessageId = selectChatLastMessageId(global, chatId);
const topic = chat?.isForum ? selectTopicFromMessage(global, message) : undefined; const topic = chat?.isForum ? selectTopicFromMessage(global, message) : undefined;
if (topic) { if (topic) {
@ -860,22 +886,27 @@ function updateChatLastMessage<T extends GlobalState>(
}); });
} }
if (currentLastMessage && !force) { const savedDialogId = selectSavedDialogIdFromMessage(global, message);
if (savedDialogId) {
global = updateChatLastMessageId(global, savedDialogId, message.id, 'saved');
}
if (currentLastMessageId && !force) {
const isSameOrNewer = ( const isSameOrNewer = (
currentLastMessage.id === message.id || currentLastMessage.id === message.previousLocalId currentLastMessageId === message.id || currentLastMessageId === message.previousLocalId
) || message.id > currentLastMessage.id; ) || message.id > currentLastMessageId;
if (!isSameOrNewer) { if (!isSameOrNewer) {
return global; return global;
} }
} }
global = updateChat(global, chatId, { lastMessage: message }); global = updateChatLastMessageId(global, chatId, message.id);
return global; return global;
} }
function findLastMessage<T extends GlobalState>(global: T, chatId: string, threadId = MAIN_THREAD_ID) { function findLastMessage<T extends GlobalState>(global: T, chatId: string, threadId: ThreadId = MAIN_THREAD_ID) {
const byId = selectChatMessages(global, chatId); const byId = selectChatMessages(global, chatId);
const listedIds = selectListedIds(global, chatId, threadId); const listedIds = selectListedIds(global, chatId, threadId);
@ -903,7 +934,7 @@ export function deleteMessages<T extends GlobalState>(
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
if (!chat) return; if (!chat) return;
const threadIdsToUpdate = new Set<number>(); const threadIdsToUpdate = new Set<ThreadId>();
threadIdsToUpdate.add(MAIN_THREAD_ID); threadIdsToUpdate.add(MAIN_THREAD_ID);
ids.forEach((id) => { ids.forEach((id) => {
@ -979,6 +1010,18 @@ export function deleteMessages<T extends GlobalState>(
global = updateChatLastMessage(global, commonBoxChatId, newLastMessage, true); global = updateChatLastMessage(global, commonBoxChatId, newLastMessage, true);
} }
const message = selectChatMessage(global, commonBoxChatId, id);
if (selectIsChatWithSelf(global, commonBoxChatId) && message) {
const threadId = selectThreadIdFromMessage(global, message);
if (getIsSavedDialog(commonBoxChatId, threadId, global.currentUserId)) {
const newLastSavedDialogMessage = findLastMessage(global, commonBoxChatId, threadId);
actions.requestSavedDialogUpdate({ chatId: String(threadId) });
if (newLastSavedDialogMessage) {
global = updateChatLastMessageId(global, commonBoxChatId, newLastSavedDialogMessage.id, 'saved');
}
}
}
setTimeout(() => { setTimeout(() => {
global = getGlobal(); global = getGlobal();
global = deleteChatMessages(global, commonBoxChatId, [id]); global = deleteChatMessages(global, commonBoxChatId, [id]);

View File

@ -6,7 +6,12 @@ import { addActionHandler, getGlobal, setGlobal } from '../../index';
import { import {
deleteContact, replaceUserStatuses, updatePeerStoriesHidden, updateUser, updateUserFullInfo, deleteContact, replaceUserStatuses, updatePeerStoriesHidden, updateUser, updateUserFullInfo,
} from '../../reducers'; } from '../../reducers';
import { selectIsCurrentUserPremium, selectUser, selectUserFullInfo } from '../../selectors'; import {
selectIsChatWithSelf,
selectIsCurrentUserPremium,
selectUser,
selectUserFullInfo,
} from '../../selectors';
const STATUS_UPDATE_THROTTLE = 3000; const STATUS_UPDATE_THROTTLE = 3000;
@ -40,7 +45,7 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
case 'updateUser': { case 'updateUser': {
Object.values(global.byTabId).forEach(({ id: tabId }) => { Object.values(global.byTabId).forEach(({ id: tabId }) => {
if (update.id === global.currentUserId && update.user.isPremium !== selectIsCurrentUserPremium(global)) { if (selectIsChatWithSelf(global, update.id) && update.user.isPremium !== selectIsCurrentUserPremium(global)) {
if (update.user.isPremium && global.byTabId[tabId].premiumModal) { if (update.user.isPremium && global.byTabId[tabId].premiumModal) {
actions.openPremiumModal({ isSuccess: true, tabId }); actions.openPremiumModal({ isSuccess: true, tabId });
} }

View File

@ -20,7 +20,9 @@ import parseHtmlAsFormattedText from '../../../util/parseHtmlAsFormattedText';
import { getServerTime } from '../../../util/serverTime'; import { getServerTime } from '../../../util/serverTime';
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment'; import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
import versionNotification from '../../../versionNotification.txt'; import versionNotification from '../../../versionNotification.txt';
import { getMessageSummaryText, getSenderTitle, isChatChannel } from '../../helpers'; import {
getIsSavedDialog, getMessageSummaryText, getSenderTitle, isChatChannel,
} from '../../helpers';
import { renderMessageSummaryHtml } from '../../helpers/renderMessageSummaryHtml'; import { renderMessageSummaryHtml } from '../../helpers/renderMessageSummaryHtml';
import { addActionHandler, getGlobal, setGlobal } from '../../index'; import { addActionHandler, getGlobal, setGlobal } from '../../index';
import { import {
@ -38,6 +40,7 @@ import { updateTabState } from '../../reducers/tabs';
import { import {
selectAllowedMessageActions, selectAllowedMessageActions,
selectChat, selectChat,
selectChatLastMessageId,
selectChatMessages, selectChatMessages,
selectChatScheduledMessages, selectChatScheduledMessages,
selectCurrentChat, selectCurrentChat,
@ -144,9 +147,7 @@ addActionHandler('replyToNextMessage', (global, actions, payload): ActionReturnT
if (!isLatest || !replyInfo?.replyToMsgId) { if (!isLatest || !replyInfo?.replyToMsgId) {
if (threadId === MAIN_THREAD_ID) { if (threadId === MAIN_THREAD_ID) {
const chat = selectChat(global, chatId); messageId = selectChatLastMessageId(global, chatId);
messageId = chat?.lastMessage?.id;
} else { } else {
const threadInfo = selectThreadInfo(global, chatId, threadId); const threadInfo = selectThreadInfo(global, chatId, threadId);
@ -316,6 +317,8 @@ addActionHandler('focusLastMessage', (global, actions, payload): ActionReturnTyp
const { chatId, threadId, type } = currentMessageList; const { chatId, threadId, type } = currentMessageList;
const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
let lastMessageId: number | undefined; let lastMessageId: number | undefined;
if (threadId === MAIN_THREAD_ID) { if (threadId === MAIN_THREAD_ID) {
if (type === 'pinned') { if (type === 'pinned') {
@ -326,10 +329,10 @@ addActionHandler('focusLastMessage', (global, actions, payload): ActionReturnTyp
lastMessageId = pinnedMessageIds[pinnedMessageIds.length - 1]; lastMessageId = pinnedMessageIds[pinnedMessageIds.length - 1];
} else { } else {
const chat = selectChat(global, chatId); lastMessageId = selectChatLastMessageId(global, chatId);
lastMessageId = chat?.lastMessage?.id;
} }
} else if (isSavedDialog) {
lastMessageId = selectChatLastMessageId(global, String(threadId), 'saved');
} else { } else {
const threadInfo = selectThreadInfo(global, chatId, threadId); const threadInfo = selectThreadInfo(global, chatId, threadId);
@ -703,10 +706,9 @@ addActionHandler('checkVersionNotification', (global, actions): ActionReturnType
addActionHandler('createServiceNotification', (global, actions, payload): ActionReturnType => { addActionHandler('createServiceNotification', (global, actions, payload): ActionReturnType => {
const { message, version } = payload; const { message, version } = payload;
const { serviceNotifications } = global; const { serviceNotifications } = global;
const serviceChat = selectChat(global, SERVICE_NOTIFICATIONS_USER_ID)!;
const maxId = Math.max( const maxId = Math.max(
serviceChat.lastMessage?.id || 0, selectChatLastMessageId(global, SERVICE_NOTIFICATIONS_USER_ID) || 0,
...serviceNotifications.map(({ id }) => id), ...serviceNotifications.map(({ id }) => id),
); );
const fractionalPart = (serviceNotifications.length + 1) / SERVICE_NOTIFICATIONS_MAX_AMOUNT; const fractionalPart = (serviceNotifications.length + 1) / SERVICE_NOTIFICATIONS_MAX_AMOUNT;

View File

@ -12,11 +12,11 @@ import {
DEBUG, DEBUG,
DEFAULT_LIMITS, DEFAULT_LIMITS,
GLOBAL_STATE_CACHE_CHAT_LIST_LIMIT, GLOBAL_STATE_CACHE_CHAT_LIST_LIMIT,
GLOBAL_STATE_CACHE_CHATS_WITH_MESSAGES_LIMIT,
GLOBAL_STATE_CACHE_CUSTOM_EMOJI_LIMIT, GLOBAL_STATE_CACHE_CUSTOM_EMOJI_LIMIT,
GLOBAL_STATE_CACHE_DISABLED, GLOBAL_STATE_CACHE_DISABLED,
GLOBAL_STATE_CACHE_KEY, GLOBAL_STATE_CACHE_KEY,
GLOBAL_STATE_CACHE_USER_LIST_LIMIT, GLOBAL_STATE_CACHE_USER_LIST_LIMIT,
SAVED_FOLDER_ID,
} from '../config'; } from '../config';
import { getOrderedIds } from '../util/folderManager'; import { getOrderedIds } from '../util/folderManager';
import { import {
@ -31,6 +31,7 @@ import { INITIAL_GLOBAL_STATE, INITIAL_PERFORMANCE_STATE_MID, INITIAL_PERFORMANC
import { clearGlobalForLockScreen } from './reducers'; import { clearGlobalForLockScreen } from './reducers';
import { import {
selectChat, selectChat,
selectChatLastMessageId,
selectChatMessages, selectChatMessages,
selectCurrentMessageList, selectCurrentMessageList,
selectViewportIds, selectViewportIds,
@ -209,6 +210,10 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
cached.chats.similarChannelsById = initialState.chats.similarChannelsById; cached.chats.similarChannelsById = initialState.chats.similarChannelsById;
} }
if (!cached.chats.lastMessageIds) {
cached.chats.lastMessageIds = initialState.chats.lastMessageIds;
}
// Clear old color storage to optimize cache size // Clear old color storage to optimize cache size
if (untypedCached?.appConfig?.peerColors) { if (untypedCached?.appConfig?.peerColors) {
untypedCached.appConfig.peerColors = undefined; untypedCached.appConfig.peerColors = undefined;
@ -370,6 +375,7 @@ function reduceChats<T extends GlobalState>(global: T): GlobalState['chats'] {
...currentUserId ? [currentUserId] : [], ...currentUserId ? [currentUserId] : [],
...currentChatIds, ...currentChatIds,
...messagesChatIds, ...messagesChatIds,
...getOrderedIds(SAVED_FOLDER_ID) || [],
...getOrderedIds(ALL_FOLDER_ID) || [], ...getOrderedIds(ALL_FOLDER_ID) || [],
...getOrderedIds(ARCHIVED_FOLDER_ID) || [], ...getOrderedIds(ARCHIVED_FOLDER_ID) || [],
...global.recentlyFoundChatIds || [], ...global.recentlyFoundChatIds || [],
@ -382,6 +388,10 @@ function reduceChats<T extends GlobalState>(global: T): GlobalState['chats'] {
isFullyLoaded: {}, isFullyLoaded: {},
byId: pick(global.chats.byId, idsToSave), byId: pick(global.chats.byId, idsToSave),
fullInfoById: pick(global.chats.fullInfoById, idsToSave), fullInfoById: pick(global.chats.fullInfoById, idsToSave),
lastMessageIds: {
all: pick(global.chats.lastMessageIds.all || {}, idsToSave),
saved: global.chats.lastMessageIds.saved,
},
}; };
} }
@ -400,7 +410,7 @@ function reduceMessages<T extends GlobalState>(global: T): GlobalState['messages
...currentChatIds, ...currentChatIds,
...currentUserId ? [currentUserId] : [], ...currentUserId ? [currentUserId] : [],
...forumPanelChatIds, ...forumPanelChatIds,
...getOrderedIds(ALL_FOLDER_ID)?.slice(0, GLOBAL_STATE_CACHE_CHATS_WITH_MESSAGES_LIMIT) || [], ...getOrderedIds(ALL_FOLDER_ID) || [],
]); ]);
chatIdsToSave.forEach((chatId) => { chatIdsToSave.forEach((chatId) => {
@ -410,6 +420,7 @@ function reduceMessages<T extends GlobalState>(global: T): GlobalState['messages
} }
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
const chatLastMessageId = selectChatLastMessageId(global, chatId);
const threadIds = unique(compact(Object.values(global.byTabId).map(({ id: tabId }) => { const threadIds = unique(compact(Object.values(global.byTabId).map(({ id: tabId }) => {
const { chatId: tabChatId, threadId } = selectCurrentMessageList(global, tabId) || {}; const { chatId: tabChatId, threadId } = selectCurrentMessageList(global, tabId) || {};
@ -423,15 +434,18 @@ function reduceMessages<T extends GlobalState>(global: T): GlobalState['messages
.map(({ threadInfo }) => (threadInfo?.isCommentsInfo ? threadInfo?.originMessageId : undefined)), .map(({ threadInfo }) => (threadInfo?.isCommentsInfo ? threadInfo?.originMessageId : undefined)),
))); )));
const threadIdsToSave = threadIds.length ? [MAIN_THREAD_ID, ...threadIds] : [MAIN_THREAD_ID]; const threadsToSave = pickTruthy(current.threadsById, [MAIN_THREAD_ID, ...threadIds]);
const threadsToSave = pickTruthy(current.threadsById, threadIdsToSave);
if (!Object.keys(threadsToSave).length) { if (!Object.keys(threadsToSave).length) {
return; return;
} }
const viewportIdsToSave = unique(Object.values(threadsToSave).flatMap((thread) => thread.lastViewportIds || [])); const viewportIdsToSave = unique(Object.values(threadsToSave).flatMap((thread) => thread.lastViewportIds || []));
const lastMessageIdsToSave = chat?.topics const topicLastMessageIds = chat?.topics ? Object.values(chat.topics).map(({ lastMessageId }) => lastMessageId)
? Object.values(chat.topics).map(({ lastMessageId }) => lastMessageId) : []; : [];
const savedLastMessageIds = chatId === currentUserId && global.chats.lastMessageIds.saved
? Object.values(global.chats.lastMessageIds.saved) : [];
const lastMessageIdsToSave = [chatLastMessageId].concat(topicLastMessageIds).concat(savedLastMessageIds)
.filter(Boolean);
const byId = pick(current.byId, viewportIdsToSave.concat(lastMessageIdsToSave)); const byId = pick(current.byId, viewportIdsToSave.concat(lastMessageIdsToSave));
const threadsById = Object.keys(threadsToSave).reduce((acc, key) => { const threadsById = Object.keys(threadsToSave).reduce((acc, key) => {
const thread = threadsToSave[Number(key)]; const thread = threadsToSave[Number(key)];

View File

@ -8,23 +8,20 @@ import type {
ApiUser, ApiUser,
} from '../../api/types'; } from '../../api/types';
import type { LangFn } from '../../hooks/useLang'; import type { LangFn } from '../../hooks/useLang';
import type { NotifyException, NotifySettings } from '../../types'; import type { NotifyException, NotifySettings, ThreadId } from '../../types';
import { MAIN_THREAD_ID } from '../../api/types'; import { MAIN_THREAD_ID } from '../../api/types';
import { import {
ANONYMOUS_USER_ID,
ARCHIVED_FOLDER_ID, CHANNEL_ID_LENGTH, GENERAL_TOPIC_ID, REPLIES_USER_ID, TME_LINK_PREFIX, ARCHIVED_FOLDER_ID, CHANNEL_ID_LENGTH, GENERAL_TOPIC_ID, REPLIES_USER_ID, TME_LINK_PREFIX,
} from '../../config'; } from '../../config';
import { formatDateToString, formatTime } from '../../util/dateFormat'; import { formatDateToString, formatTime } from '../../util/dateFormat';
import { orderBy } from '../../util/iteratees';
import { prepareSearchWordsForNeedle } from '../../util/searchWords'; import { prepareSearchWordsForNeedle } from '../../util/searchWords';
import { getGlobal } from '..'; import { getGlobal } from '..';
import { getMainUsername, getUserFirstOrLastName } from './users'; import { getMainUsername, getUserFirstOrLastName } from './users';
const FOREVER_BANNED_DATE = Date.now() / 1000 + 31622400; // 366 days const FOREVER_BANNED_DATE = Date.now() / 1000 + 31622400; // 366 days
const VERIFIED_PRIORITY_BASE = 3e9;
const PINNED_PRIORITY_BASE = 3e8;
export function isUserId(entityId: string) { export function isUserId(entityId: string) {
return !entityId.startsWith('-'); return !entityId.startsWith('-');
} }
@ -65,6 +62,10 @@ export function isChatWithRepliesBot(chatId: string) {
return chatId === REPLIES_USER_ID; return chatId === REPLIES_USER_ID;
} }
export function isAnonymousForwardsChat(chatId: string) {
return chatId === ANONYMOUS_USER_ID;
}
export function getChatTypeString(chat: ApiChat) { export function getChatTypeString(chat: ApiChat) {
switch (chat.type) { switch (chat.type) {
case 'chatTypePrivate': case 'chatTypePrivate':
@ -116,10 +117,6 @@ export function getChatAvatarHash(
} }
} }
export function isChatSummaryOnly(chat: ApiChat) {
return !chat.lastMessage;
}
export function isChatAdmin(chat: ApiChat) { export function isChatAdmin(chat: ApiChat) {
return Boolean(chat.adminRights); return Boolean(chat.adminRights);
} }
@ -140,7 +137,7 @@ export function isUserRightBanned(chat: ApiChat, key: keyof ApiChatBannedRights)
); );
} }
export function getCanPostInChat(chat: ApiChat, threadId: number, isMessageThread?: boolean) { export function getCanPostInChat(chat: ApiChat, threadId: ThreadId, isMessageThread?: boolean) {
if (threadId !== MAIN_THREAD_ID) { if (threadId !== MAIN_THREAD_ID) {
if (chat.isForum) { if (chat.isForum) {
if (chat.isNotJoined) { if (chat.isNotJoined) {
@ -155,7 +152,7 @@ export function getCanPostInChat(chat: ApiChat, threadId: number, isMessageThrea
} }
if (chat.isRestricted || chat.isForbidden || chat.migratedTo if (chat.isRestricted || chat.isForbidden || chat.migratedTo
|| (!isMessageThread && chat.isNotJoined) || isChatWithRepliesBot(chat.id)) { || (!isMessageThread && chat.isNotJoined) || isChatWithRepliesBot(chat.id) || isAnonymousForwardsChat(chat.id)) {
return false; return false;
} }
@ -257,7 +254,7 @@ export function getMessageSendingRestrictionReason(
} }
export function getForumComposerPlaceholder( export function getForumComposerPlaceholder(
lang: LangFn, chat?: ApiChat, threadId = MAIN_THREAD_ID, isReplying?: boolean, lang: LangFn, chat?: ApiChat, threadId: ThreadId = MAIN_THREAD_ID, isReplying?: boolean,
) { ) {
if (!chat?.isForum) { if (!chat?.isForum) {
return undefined; return undefined;
@ -377,36 +374,6 @@ export function getMessageSenderName(lang: LangFn, chatId: string, sender?: ApiP
return getUserFirstOrLastName(sender); return getUserFirstOrLastName(sender);
} }
export function sortChatIds(
chatIds: string[],
chatsById: Record<string, ApiChat>,
shouldPrioritizeVerified = false,
priorityIds?: string[],
) {
return orderBy(chatIds, (id) => {
const chat = chatsById[id];
if (!chat) {
return 0;
}
let priority = 0;
if (chat.lastMessage) {
priority += chat.lastMessage.date;
}
if (shouldPrioritizeVerified && chat.isVerified) {
priority += VERIFIED_PRIORITY_BASE; // ~100 years in seconds
}
if (priorityIds && priorityIds.includes(id)) {
priority = Date.now() + PINNED_PRIORITY_BASE + (priorityIds.length - priorityIds.indexOf(id));
}
return priority;
}, 'desc');
}
export function filterChatsByName( export function filterChatsByName(
lang: LangFn, lang: LangFn,
chatIds: string[], chatIds: string[],
@ -480,3 +447,7 @@ export function getPeerColorCount(peer: ApiPeer) {
// eslint-disable-next-line eslint-multitab-tt/no-immediate-global // eslint-disable-next-line eslint-multitab-tt/no-immediate-global
return getGlobal().peerColors?.general[key].colors?.length || 1; return getGlobal().peerColors?.general[key].colors?.length || 1;
} }
export function getIsSavedDialog(chatId: string, threadId: ThreadId | undefined, currentUserId: string | undefined) {
return chatId === currentUserId && threadId !== MAIN_THREAD_ID;
}

View File

@ -1,3 +1,5 @@
export function buildChatThreadKey(chatId: string, threadId: number) { import type { ThreadId } from '../../types';
export function buildChatThreadKey(chatId: string, threadId: ThreadId) {
return `${chatId}_${threadId}`; return `${chatId}_${threadId}`;
} }

View File

@ -1,7 +1,7 @@
import type { ApiPeer, ApiUser, ApiUserStatus } from '../../api/types'; import type { ApiPeer, ApiUser, ApiUserStatus } from '../../api/types';
import type { LangFn } from '../../hooks/useLang'; import type { LangFn } from '../../hooks/useLang';
import { SERVICE_NOTIFICATIONS_USER_ID } from '../../config'; import { ANONYMOUS_USER_ID, SERVICE_NOTIFICATIONS_USER_ID } from '../../config';
import { formatFullDate, formatTime } from '../../util/dateFormat'; import { formatFullDate, formatTime } from '../../util/dateFormat';
import { orderBy } from '../../util/iteratees'; import { orderBy } from '../../util/iteratees';
import { formatPhoneNumber } from '../../util/phoneNumber'; import { formatPhoneNumber } from '../../util/phoneNumber';
@ -191,7 +191,7 @@ export function isUserBot(user: ApiUser) {
} }
export function getCanAddContact(user: ApiUser) { export function getCanAddContact(user: ApiUser) {
return !user.isSelf && !user.isContact && !isUserBot(user); return !user.isSelf && !user.isContact && !isUserBot(user) && user.id !== ANONYMOUS_USER_ID;
} }
export function sortUserIds( export function sortUserIds(

View File

@ -117,7 +117,7 @@ addActionHandler('init', (global, actions, payload): ActionReturnType => {
}; };
}); });
const parsedMessageList = parseLocationHash(); const parsedMessageList = parseLocationHash(global.currentUserId);
if (global.authState !== 'authorizationStateReady' if (global.authState !== 'authorizationStateReady'
&& !global.passcode.hasPasscode && !global.passcode.isScreenLocked) { && !global.passcode.hasPasscode && !global.passcode.isScreenLocked) {

View File

@ -103,6 +103,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
isFullyLoaded: {}, isFullyLoaded: {},
orderedPinnedIds: {}, orderedPinnedIds: {},
totalCount: {}, totalCount: {},
lastMessageIds: {},
byId: {}, byId: {},
fullInfoById: {}, fullInfoById: {},
similarChannelsById: {}, similarChannelsById: {},

View File

@ -1,7 +1,7 @@
import type { import type {
ApiChat, ApiChatFullInfo, ApiChatMember, ApiPhoto, ApiTopic, ApiChat, ApiChatFullInfo, ApiChatMember, ApiPhoto, ApiTopic,
} from '../../api/types'; } from '../../api/types';
import type { GlobalState } from '../types'; import type { ChatListType, GlobalState } from '../types';
import { ARCHIVED_FOLDER_ID } from '../../config'; import { ARCHIVED_FOLDER_ID } from '../../config';
import { areDeepEqual } from '../../util/areDeepEqual'; import { areDeepEqual } from '../../util/areDeepEqual';
@ -11,9 +11,11 @@ import {
import { selectChat, selectChatFullInfo } from '../selectors'; import { selectChat, selectChatFullInfo } from '../selectors';
import { updateThread, updateThreadInfo } from './messages'; import { updateThread, updateThreadInfo } from './messages';
const DEFAULT_CHAT_LISTS: ChatListType[] = ['active', 'archived'];
export function replaceChatListIds<T extends GlobalState>( export function replaceChatListIds<T extends GlobalState>(
global: T, global: T,
type: 'active' | 'archived', type: ChatListType,
newIds: string[] | undefined, newIds: string[] | undefined,
): T { ): T {
return { return {
@ -28,8 +30,46 @@ export function replaceChatListIds<T extends GlobalState>(
}; };
} }
export function updateChatLastMessageId<T extends GlobalState>(
global: T, chatId: string, lastMessageId: number, listType?: ChatListType,
): T {
const key = listType === 'saved' ? 'saved' : 'all';
return {
...global,
chats: {
...global.chats,
lastMessageIds: {
...global.chats.lastMessageIds,
[key]: {
...global.chats.lastMessageIds[key],
[chatId]: lastMessageId,
},
},
},
};
}
export function updateChatsLastMessageId<T extends GlobalState>(
global: T, messageIds: Record<string, number>, listType?: ChatListType,
): T {
const key = listType === 'saved' ? 'saved' : 'all';
return {
...global,
chats: {
...global.chats,
lastMessageIds: {
...global.chats.lastMessageIds,
[key]: {
...global.chats.lastMessageIds[key],
...messageIds,
},
},
},
};
}
export function updateChatListIds<T extends GlobalState>( export function updateChatListIds<T extends GlobalState>(
global: T, type: 'active' | 'archived', idsUpdate: string[], global: T, type: ChatListType, idsUpdate: string[],
): T { ): T {
const { [type]: listIds } = global.chats.listIds; const { [type]: listIds } = global.chats.listIds;
const newIds = listIds?.length const newIds = listIds?.length
@ -243,13 +283,13 @@ export function updateChatListType<T extends GlobalState>(
export function updateChatListSecondaryInfo<T extends GlobalState>( export function updateChatListSecondaryInfo<T extends GlobalState>(
global: T, global: T,
type: 'active' | 'archived', type: ChatListType,
info: { info: {
orderedPinnedIds?: string[]; orderedPinnedIds?: string[];
totalChatCount: number; totalChatCount: number;
}, },
): T { ): T {
const totalCountKey = type === 'active' ? 'all' : 'archived'; const totalCountKey = type === 'active' ? 'all' : type;
return { return {
...global, ...global,
@ -281,10 +321,12 @@ export function leaveChat<T extends GlobalState>(global: T, leftChatId: string):
return global; return global;
} }
export function removeChatFromChatLists<T extends GlobalState>(global: T, chatId: string): T { export function removeChatFromChatLists<T extends GlobalState>(
const lists = global.chats.listIds; global: T, chatId: string, type: 'all' | 'saved' = 'all',
Object.entries(lists).forEach(([listType, listIds]) => { ): T {
global = replaceChatListIds(global, listType as keyof typeof lists, listIds.filter((id) => id !== chatId)); const chatLists = type === 'all' ? DEFAULT_CHAT_LISTS : [type];
chatLists.forEach((listType) => {
global = replaceChatListIds(global, listType, global.chats.listIds[listType]?.filter((id) => id !== chatId));
}); });
return global; return global;
@ -393,8 +435,7 @@ export function deleteTopic<T extends GlobalState>(
global: T, chatId: string, topicId: number, global: T, chatId: string, topicId: number,
) { ) {
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
const topics = chat?.topics || []; const topics = chat?.topics || {};
global = updateChat(global, chatId, { global = updateChat(global, chatId, {
topics: omit(topics, [topicId]), topics: omit(topics, [topicId]),
}); });

View File

@ -1,5 +1,5 @@
import type { ApiMessageSearchType } from '../../api/types'; import type { ApiMessageSearchType } from '../../api/types';
import type { SharedMediaType } from '../../types'; import type { SharedMediaType, ThreadId } from '../../types';
import type { GlobalState, TabArgs } from '../types'; import type { GlobalState, TabArgs } from '../types';
import { getCurrentTabId } from '../../util/establishMultitabRole'; import { getCurrentTabId } from '../../util/establishMultitabRole';
@ -46,7 +46,7 @@ function replaceLocalTextSearch<T extends GlobalState>(
export function updateLocalTextSearch<T extends GlobalState>( export function updateLocalTextSearch<T extends GlobalState>(
global: T, global: T,
chatId: string, chatId: string,
threadId: number, threadId: ThreadId,
isActive: boolean, isActive: boolean,
query?: string, query?: string,
...[tabId = getCurrentTabId()]: TabArgs<T> ...[tabId = getCurrentTabId()]: TabArgs<T>
@ -63,7 +63,7 @@ export function updateLocalTextSearch<T extends GlobalState>(
export function replaceLocalTextSearchResults<T extends GlobalState>( export function replaceLocalTextSearchResults<T extends GlobalState>(
global: T, global: T,
chatId: string, chatId: string,
threadId: number, threadId: ThreadId,
foundIds?: number[], foundIds?: number[],
totalCount?: number, totalCount?: number,
nextOffsetId?: number, nextOffsetId?: number,
@ -84,7 +84,7 @@ export function replaceLocalTextSearchResults<T extends GlobalState>(
export function updateLocalTextSearchResults<T extends GlobalState>( export function updateLocalTextSearchResults<T extends GlobalState>(
global: T, global: T,
chatId: string, chatId: string,
threadId: number, threadId: ThreadId,
newFoundIds: number[], newFoundIds: number[],
totalCount?: number, totalCount?: number,
nextOffsetId?: number, nextOffsetId?: number,
@ -102,7 +102,7 @@ export function updateLocalTextSearchResults<T extends GlobalState>(
function replaceLocalMediaSearch<T extends GlobalState>( function replaceLocalMediaSearch<T extends GlobalState>(
global: T, global: T,
chatId: string, chatId: string,
threadId: number, threadId: ThreadId,
searchParams: MediaSearchParams, searchParams: MediaSearchParams,
...[tabId = getCurrentTabId()]: TabArgs<T> ...[tabId = getCurrentTabId()]: TabArgs<T>
): T { ): T {
@ -121,7 +121,7 @@ function replaceLocalMediaSearch<T extends GlobalState>(
export function updateLocalMediaSearchType<T extends GlobalState>( export function updateLocalMediaSearchType<T extends GlobalState>(
global: T, global: T,
chatId: string, chatId: string,
threadId: number, threadId: ThreadId,
currentType: SharedMediaType | undefined, currentType: SharedMediaType | undefined,
...[tabId = getCurrentTabId()]: TabArgs<T> ...[tabId = getCurrentTabId()]: TabArgs<T>
): T { ): T {
@ -136,7 +136,7 @@ export function updateLocalMediaSearchType<T extends GlobalState>(
export function replaceLocalMediaSearchResults<T extends GlobalState>( export function replaceLocalMediaSearchResults<T extends GlobalState>(
global: T, global: T,
chatId: string, chatId: string,
threadId: number, threadId: ThreadId,
type: ApiMessageSearchType, type: ApiMessageSearchType,
foundIds?: number[], foundIds?: number[],
totalCount?: number, totalCount?: number,
@ -161,7 +161,7 @@ export function replaceLocalMediaSearchResults<T extends GlobalState>(
export function updateLocalMediaSearchResults<T extends GlobalState>( export function updateLocalMediaSearchResults<T extends GlobalState>(
global: T, global: T,
chatId: string, chatId: string,
threadId: number, threadId: ThreadId,
type: SharedMediaType, type: SharedMediaType,
newFoundIds: number[], newFoundIds: number[],
totalCount?: number, totalCount?: number,

Some files were not shown because too many files have changed in this diff Show More