From c3c71cbc9e4ce255dc60887623ef42c2ea73fc64 Mon Sep 17 00:00:00 2001
From: Alexander Zinchuk
Date: Tue, 6 Feb 2024 16:48:54 +0100
Subject: [PATCH] Introduce Saved dialogs (#4177)
---
src/api/gramjs/ChatAbortController.ts | 10 +-
src/api/gramjs/apiBuilders/appConfig.ts | 4 +-
src/api/gramjs/apiBuilders/chats.ts | 14 +
src/api/gramjs/apiBuilders/messages.ts | 17 +-
src/api/gramjs/methods/chats.ts | 171 +++++++-
src/api/gramjs/methods/client.ts | 5 +-
src/api/gramjs/methods/index.ts | 2 +-
src/api/gramjs/methods/messages.ts | 50 ++-
src/api/gramjs/methods/users.ts | 2 +
src/api/gramjs/updater.ts | 20 +
src/api/types/chats.ts | 6 +-
src/api/types/messages.ts | 9 +-
src/api/types/updates.ts | 25 +-
src/assets/font-icons/author-hidden.svg | 1 +
src/assets/font-icons/my-notes.svg | 1 +
src/components/common/Avatar.tsx | 65 ++-
src/components/common/ChatExtra.tsx | 21 +-
src/components/common/ChatOrUserPicker.tsx | 3 +-
src/components/common/Composer.tsx | 6 +-
src/components/common/FullNameTitle.tsx | 28 +-
src/components/common/GroupChatInfo.tsx | 45 +-
src/components/common/Icon.tsx | 9 +-
src/components/common/PrivateChatInfo.tsx | 19 +
src/components/common/ProfileInfo.tsx | 25 +-
src/components/common/ProfilePhoto.tsx | 40 +-
src/components/common/RecipientPicker.tsx | 7 +-
src/components/common/helpers/sortChatIds.ts | 39 ++
src/components/left/main/Chat.tsx | 78 +++-
src/components/left/main/ChatBadge.tsx | 14 +-
src/components/left/main/ChatList.tsx | 46 +-
src/components/left/main/EmptyFolder.tsx | 2 +-
src/components/left/main/ForumPanel.tsx | 2 +-
.../left/main/hooks/useChatListEntry.tsx | 11 +-
src/components/left/newChat/NewChatStep1.tsx | 12 +-
src/components/left/search/ChatResults.tsx | 9 +-
src/components/main/DraftRecipientPicker.tsx | 3 +-
.../main/ForwardRecipientPicker.tsx | 6 +-
src/components/main/Main.tsx | 5 +-
.../main/premium/PremiumFeatureModal.tsx | 4 +-
.../common/PremiumLimitReachedModal.tsx | 4 +
src/components/mediaViewer/MediaViewer.tsx | 4 +-
.../mediaViewer/MediaViewerContent.tsx | 6 +-
.../mediaViewer/MediaViewerSlides.tsx | 4 +-
src/components/middle/ActionMessage.tsx | 4 +-
src/components/middle/ContactGreeting.tsx | 8 +-
src/components/middle/HeaderActions.tsx | 8 +-
src/components/middle/HeaderMenuContainer.tsx | 6 +-
src/components/middle/MessageList.scss | 3 +-
src/components/middle/MessageList.tsx | 41 +-
src/components/middle/MessageListContent.tsx | 12 +-
src/components/middle/MiddleColumn.scss | 10 +-
src/components/middle/MiddleColumn.tsx | 55 ++-
src/components/middle/MiddleHeader.scss | 11 +
src/components/middle/MiddleHeader.tsx | 42 +-
src/components/middle/MobileSearch.tsx | 3 +-
.../middle/composer/AttachBotItem.tsx | 4 +-
src/components/middle/composer/AttachMenu.tsx | 4 +-
.../middle/composer/AttachmentModal.tsx | 3 +-
.../middle/composer/MessageInput.tsx | 4 +-
.../middle/composer/StickerPicker.tsx | 4 +-
.../middle/composer/StickerTooltip.tsx | 3 +-
src/components/middle/composer/SymbolMenu.tsx | 3 +-
.../middle/composer/SymbolMenuButton.tsx | 4 +-
.../middle/composer/WebPagePreview.tsx | 4 +-
.../middle/composer/hooks/useDraft.ts | 5 +-
.../middle/composer/hooks/useEditing.ts | 3 +-
.../middle/helpers/groupMessages.ts | 2 +-
.../middle/hooks/usePinnedMessage.ts | 4 +-
src/components/middle/message/Message.tsx | 33 +-
src/components/middle/message/MessageMeta.tsx | 13 +-
.../middle/message/hooks/useInnerHandlers.ts | 4 +-
src/components/right/AddChatMembers.tsx | 14 +-
src/components/right/Profile.scss | 6 +-
src/components/right/Profile.tsx | 86 ++--
src/components/right/RightColumn.tsx | 45 +-
src/components/right/RightHeader.scss | 16 +-
src/components/right/RightHeader.tsx | 120 +++--
src/components/right/RightSearch.tsx | 30 +-
src/components/right/hooks/useProfileState.ts | 31 +-
.../right/hooks/useProfileViewportIds.ts | 26 +-
.../right/management/ManageGroupMembers.tsx | 5 +-
src/config.ts | 4 +-
src/global/actions/api/bots.ts | 28 +-
src/global/actions/api/chats.ts | 229 +++++++---
src/global/actions/api/localSearch.ts | 40 +-
src/global/actions/api/messages.ts | 42 +-
src/global/actions/api/reactions.ts | 5 +-
src/global/actions/api/sync.ts | 44 +-
src/global/actions/apiUpdaters/chats.ts | 39 ++
src/global/actions/apiUpdaters/messages.ts | 101 +++--
src/global/actions/apiUpdaters/users.ts | 9 +-
src/global/actions/ui/messages.ts | 20 +-
src/global/cache.ts | 26 +-
src/global/helpers/chats.ts | 55 +--
src/global/helpers/localSearch.ts | 4 +-
src/global/helpers/users.ts | 4 +-
src/global/init.ts | 2 +-
src/global/initialState.ts | 1 +
src/global/reducers/chats.ts | 63 ++-
src/global/reducers/localSearch.ts | 16 +-
src/global/reducers/messages.ts | 34 +-
src/global/reducers/stories.ts | 5 +-
src/global/selectors/chats.ts | 31 +-
src/global/selectors/management.ts | 2 +
src/global/selectors/messages.ts | 149 ++++---
src/global/types.ts | 78 ++--
src/hooks/scroll/useTopOverscroll.tsx | 3 +-
src/hooks/useChatContextActions.ts | 49 ++-
src/hooks/useSendMessageAction.ts | 3 +-
src/lib/gramjs/tl/apiTl.js | 4 +
src/lib/gramjs/tl/static/api.json | 4 +
src/styles/_common.scss | 1 +
src/styles/icons.scss | 414 +++++++++---------
src/styles/icons.woff | Bin 28060 -> 28476 bytes
src/styles/icons.woff2 | Bin 23560 -> 23852 bytes
src/types/icons/font.ts | 2 +
src/types/index.ts | 6 +-
src/util/deepLinkParser.ts | 6 +-
src/util/folderManager.ts | 145 ++++--
src/util/routing.ts | 11 +-
120 files changed, 2185 insertions(+), 1027 deletions(-)
create mode 100644 src/assets/font-icons/author-hidden.svg
create mode 100644 src/assets/font-icons/my-notes.svg
create mode 100644 src/components/common/helpers/sortChatIds.ts
diff --git a/src/api/gramjs/ChatAbortController.ts b/src/api/gramjs/ChatAbortController.ts
index 714d4ca6c..df088dce9 100644
--- a/src/api/gramjs/ChatAbortController.ts
+++ b/src/api/gramjs/ChatAbortController.ts
@@ -1,7 +1,9 @@
-export class ChatAbortController extends AbortController {
- private threads = new Map();
+import type { ThreadId } from '../../types';
- public getThreadSignal(threadId: number): AbortSignal {
+export class ChatAbortController extends AbortController {
+ private threads = new Map();
+
+ public getThreadSignal(threadId: ThreadId): AbortSignal {
let controller = this.threads.get(threadId);
if (!controller) {
controller = new AbortController();
@@ -10,7 +12,7 @@ export class ChatAbortController extends AbortController {
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.delete(threadId);
}
diff --git a/src/api/gramjs/apiBuilders/appConfig.ts b/src/api/gramjs/apiBuilders/appConfig.ts
index 716ae68c3..ffc39247e 100644
--- a/src/api/gramjs/apiBuilders/appConfig.ts
+++ b/src/api/gramjs/apiBuilders/appConfig.ts
@@ -29,7 +29,8 @@ type Limit =
| 'about_length_limit'
| 'chatlist_invites_limit'
| 'chatlist_joined_limit'
- | 'recommended_channels_limit';
+ | 'recommended_channels_limit'
+ | 'saved_dialogs_pinned_limit';
type LimitKey = `${Limit}_${LimitType}`;
type LimitsConfig = Record;
@@ -124,6 +125,7 @@ export function buildAppConfig(json: GramJs.TypeJSONValue, hash: number): ApiApp
chatlistInvites: getLimit(appConfig, 'chatlist_invites_limit', 'chatlistInvites'),
chatlistJoined: getLimit(appConfig, 'chatlist_joined_limit', 'chatlistJoined'),
recommendedChannels: getLimit(appConfig, 'recommended_channels_limit', 'recommendedChannels'),
+ savedDialogsPinned: getLimit(appConfig, 'saved_dialogs_pinned_limit', 'savedDialogsPinned'),
},
hash,
areStoriesHidden: appConfig.stories_all_hidden,
diff --git a/src/api/gramjs/apiBuilders/chats.ts b/src/api/gramjs/apiBuilders/chats.ts
index 27f7dcaa4..2928aad4a 100644
--- a/src/api/gramjs/apiBuilders/chats.ts
+++ b/src/api/gramjs/apiBuilders/chats.ts
@@ -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): {
adminRights?: ApiChatAdminRights;
currentUserBannedRights?: ApiChatBannedRights;
diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts
index 8f1fe5d88..c08988166 100644
--- a/src/api/gramjs/apiBuilders/messages.ts
+++ b/src/api/gramjs/apiBuilders/messages.ts
@@ -266,13 +266,15 @@ function buildApiMessageForwardInfo(fwdFrom: GramJs.MessageFwdHeader, isChatWith
return {
date: fwdFrom.date,
+ savedDate: fwdFrom.savedDate,
isImported: fwdFrom.imported,
isChannelPost: Boolean(fwdFrom.channelPost),
channelPostId: fwdFrom.channelPost,
isLinkedChannelPost: Boolean(fwdFrom.channelPost && savedFromPeerId && !isChatWithSelf),
+ savedFromPeerId,
+ fromId,
fromChatId: savedFromPeerId || fromId,
fromMessageId: fwdFrom.savedFromMsgId || fwdFrom.channelPost,
- senderUserId: fromId,
hiddenUserName: fwdFrom.fromName,
postAuthorTitle: fwdFrom.postAuthor,
};
@@ -747,6 +749,7 @@ function buildNewPoll(poll: ApiNewPoll, localId: number) {
export function buildLocalMessage(
chat: ApiChat,
+ lastMessageId?: number,
text?: string,
entities?: ApiMessageEntity[],
replyInfo?: ApiInputReplyInfo,
@@ -760,7 +763,7 @@ export function buildLocalMessage(
sendAs?: ApiPeer,
story?: ApiStory | ApiStorySkipped,
): ApiMessage {
- const localId = getNextLocalMessageId(chat.lastMessage?.id);
+ const localId = getNextLocalMessageId(lastMessageId);
const media = attachment && buildUploadingMedia(attachment);
const isChannel = chat.type === 'chatTypeChannel';
@@ -811,6 +814,7 @@ export function buildLocalForwardedMessage({
noAuthors,
noCaptions,
isCurrentUserPremium,
+ lastMessageId,
}: {
toChat: ApiChat;
toThreadId?: number;
@@ -819,8 +823,9 @@ export function buildLocalForwardedMessage({
noAuthors?: boolean;
noCaptions?: boolean;
isCurrentUserPremium?: boolean;
+ lastMessageId?: number;
}): ApiMessage {
- const localId = getNextLocalMessageId(toChat?.lastMessage?.id);
+ const localId = getNextLocalMessageId(lastMessageId);
const {
content,
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
...(message.chatId !== currentUserId && !isAudio && !noAuthors && {
forwardInfo: {
- date: message.date,
+ date: message.forwardInfo?.date || message.date,
+ savedDate: message.date,
isChannelPost: false,
fromChatId,
fromMessageId,
- senderUserId: senderId,
+ fromId: senderId,
+ savedFromPeerId: message.chatId,
},
}),
...(message.chatId === currentUserId && !noAuthors && { forwardInfo: message.forwardInfo }),
diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts
index 54c8925d9..e484f145c 100644
--- a/src/api/gramjs/methods/chats.ts
+++ b/src/api/gramjs/methods/chats.ts
@@ -37,6 +37,7 @@ import {
buildApiChatFolderFromSuggested,
buildApiChatFromDialog,
buildApiChatFromPreview,
+ buildApiChatFromSavedDialog,
buildApiChatlistExportedInvite,
buildApiChatlistInvite,
buildApiChatReactions,
@@ -84,6 +85,18 @@ type FullChatData = {
isForumAsMessages?: true;
};
+type ChatListData = {
+ chatIds: string[];
+ chats: ApiChat[];
+ users: ApiUser[];
+ userStatusesById: Record;
+ draftsById: Record;
+ orderedPinnedIds: string[] | undefined;
+ totalChatCount: number;
+ messages: ApiMessage[];
+ lastMessageByChatId: Record;
+};
+
let onUpdate: OnApiUpdate;
export function init(_onUpdate: OnApiUpdate) {
@@ -95,19 +108,18 @@ export async function fetchChats({
offsetDate,
archived,
withPinned,
- lastLocalServiceMessage,
+ lastLocalServiceMessageId,
}: {
limit: number;
offsetDate?: number;
archived?: boolean;
withPinned?: boolean;
- lastLocalServiceMessage?: ApiMessage;
-}) {
+ lastLocalServiceMessageId?: number;
+}): Promise {
const result = await invokeRequest(new GramJs.messages.GetDialogs({
offsetPeer: new GramJs.InputPeerEmpty(),
limit,
offsetDate,
- folderId: archived ? ARCHIVED_FOLDER_ID : undefined,
...(withPinned && { excludePinned: true }),
}));
const resultPinned = withPinned
@@ -125,12 +137,10 @@ export async function fetchChats({
}
updateLocalDb(result);
- const lastMessagesByChatId = buildCollectionByKey(
- (resultPinned ? resultPinned.messages : []).concat(result.messages)
- .map(buildApiMessage)
- .filter(Boolean),
- 'chatId',
- );
+ const messages = (resultPinned ? resultPinned.messages : [])
+ .concat(result.messages)
+ .map(buildApiMessage)
+ .filter(Boolean);
dispatchThreadInfoUpdates(result.messages);
@@ -145,6 +155,7 @@ export async function fetchChats({
const dialogs = (resultPinned ? resultPinned.dialogs : []).concat(result.dialogs);
const orderedPinnedIds: string[] = [];
+ const lastMessageByChatId: Record = {};
dialogs.forEach((dialog) => {
if (
@@ -158,6 +169,7 @@ export async function fetchChats({
const peerEntity = peersByKey[getPeerKey(dialog.peer)];
const chat = buildApiChatFromDialog(dialog, peerEntity);
+ lastMessageByChatId[chat.id] = dialog.topMessage;
if (dialog.pts) {
updateChannelState(chat.id, dialog.pts);
@@ -165,12 +177,10 @@ export async function fetchChats({
if (
chat.id === SERVICE_NOTIFICATIONS_USER_ID
- && lastLocalServiceMessage
- && (!lastMessagesByChatId[chat.id] || lastLocalServiceMessage.date > lastMessagesByChatId[chat.id].date)
+ && lastLocalServiceMessageId
+ && (lastLocalServiceMessageId > dialog.topMessage)
) {
- chat.lastMessage = lastLocalServiceMessage;
- } else {
- chat.lastMessage = lastMessagesByChatId[chat.id];
+ lastMessageByChatId[chat.id] = lastLocalServiceMessageId;
}
chat.isListed = true;
@@ -210,6 +220,96 @@ export async function fetchChats({
draftsById,
orderedPinnedIds: withPinned ? orderedPinnedIds : undefined,
totalChatCount,
+ lastMessageByChatId,
+ messages,
+ };
+}
+
+export async function fetchSavedChats({
+ limit,
+ offsetDate,
+ withPinned,
+}: {
+ limit: number;
+ offsetDate?: number;
+ withPinned?: boolean;
+}): Promise {
+ 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 = {};
+
+ 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
: lastRemoteMessage;
- const chatUpdate = {
- ...buildApiChatFromDialog(dialog, peerEntity),
- ...(!noLastMessage && { lastMessage }),
- };
+ const chatUpdate = buildApiChatFromDialog(dialog, peerEntity);
onUpdate({
'@type': 'updateChat',
@@ -359,6 +456,14 @@ export async function requestChatUpdate({
chat: chatUpdate,
});
+ if (!noLastMessage && lastMessage) {
+ onUpdate({
+ '@type': 'updateChatLastMessage',
+ id,
+ lastMessage,
+ });
+ }
+
applyState(result.state);
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({
chat, folderId,
}: {
@@ -1409,7 +1538,8 @@ export function toggleJoinRequest(chat: ApiChat, isEnabled: boolean) {
}
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,
) {
const store: Record = {};
@@ -1439,6 +1569,7 @@ function preparePeers(
function updateLocalDb(result: (
GramJs.messages.Dialogs | GramJs.messages.DialogsSlice | GramJs.messages.PeerDialogs |
+ GramJs.messages.SavedDialogs | GramJs.messages.SavedDialogsSlice |
GramJs.messages.ChatFull | GramJs.contacts.Found |
GramJs.contacts.ResolvedPeer | GramJs.channels.ChannelParticipants |
GramJs.messages.Chats | GramJs.messages.ChatsSlice | GramJs.TypeUpdates | GramJs.messages.ForumTopics
diff --git a/src/api/gramjs/methods/client.ts b/src/api/gramjs/methods/client.ts
index c8296c5af..e43e6fbc5 100644
--- a/src/api/gramjs/methods/client.ts
+++ b/src/api/gramjs/methods/client.ts
@@ -6,6 +6,7 @@ import type { TwoFaParams } from '../../../lib/gramjs/client/2fa';
import TelegramClient from '../../../lib/gramjs/client/TelegramClient';
import { Logger as GramJsLogger } from '../../../lib/gramjs/extensions/index';
+import type { ThreadId } from '../../../types';
import type {
ApiInitialArgs,
ApiMediaFormat,
@@ -222,7 +223,7 @@ type InvokeRequestParams = {
dcId?: number;
shouldIgnoreErrors?: boolean;
abortControllerChatId?: string;
- abortControllerThreadId?: number;
+ abortControllerThreadId?: ThreadId;
abortControllerGroup?: 'call';
shouldRetryOnTimeout?: boolean;
};
@@ -351,7 +352,7 @@ export function getTmpPassword(currentPassword: string, ttl?: number) {
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 controller = CHAT_ABORT_CONTROLLERS.get(chatId);
if (!threadId) {
diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts
index 7a33aeb3e..12b9498c8 100644
--- a/src/api/gramjs/methods/index.ts
+++ b/src/api/gramjs/methods/index.ts
@@ -24,7 +24,7 @@ export {
editTopic, toggleForum, fetchTopicById, createTopic, toggleParticipantsHidden, checkChatlistInvite,
joinChatlistInvite, createChalistInvite, editChatlistInvite, deleteChatlistInvite, fetchChatlistInvites,
fetchLeaveChatlistSuggestions, leaveChatlist, togglePeerTranslations, setViewForumAsMessages,
- fetchChannelRecommendations,
+ fetchChannelRecommendations, fetchSavedChats, toggleSavedDialogPinned,
} from './chats';
export {
diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts
index 0002e1b15..08c73e562 100644
--- a/src/api/gramjs/methods/messages.ts
+++ b/src/api/gramjs/methods/messages.ts
@@ -1,5 +1,6 @@
import { Api as GramJs } from '../../../lib/gramjs';
+import type { ThreadId } from '../../../types';
import type {
ApiAttachment,
ApiChat,
@@ -106,21 +107,25 @@ export async function fetchMessages({
chat,
threadId,
offsetId,
+ isSavedDialog,
...pagination
}: {
chat: ApiChat;
- threadId?: number;
+ threadId?: ThreadId;
offsetId?: number;
+ isSavedDialog?: boolean;
addOffset?: 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;
try {
result = await invokeRequest(new RequestClass({
peer: buildInputPeer(chat.id, chat.accessHash),
- ...(threadId !== MAIN_THREAD_ID && {
+ ...(threadId !== MAIN_THREAD_ID && !isSavedDialog && {
msgId: Number(threadId),
}),
...(offsetId && {
@@ -241,6 +246,7 @@ let mediaQueue = Promise.resolve();
export function sendMessage(
{
chat,
+ lastMessageId,
text,
entities,
replyInfo,
@@ -281,6 +287,7 @@ export function sendMessage(
) {
const localMessage = buildLocalMessage(
chat,
+ lastMessageId,
text,
entities,
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({
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({
peer, threadId, action,
}: {
- peer: ApiPeer; threadId?: number; action: ApiSendMessageAction;
+ peer: ApiPeer; threadId?: ThreadId; action: ApiSendMessageAction;
}) {
const gramAction = buildSendMessageAction(action);
if (!gramAction) {
@@ -820,7 +827,7 @@ export async function sendMessageAction({
try {
const result = await invokeRequest(new GramJs.messages.SetTyping({
peer: buildInputPeer(peer.id, peer.accessHash),
- topMsgId: threadId,
+ topMsgId: Number(threadId),
action: gramAction,
}), {
shouldThrow: true,
@@ -837,7 +844,7 @@ export async function sendMessageAction({
export async function markMessageListRead({
chat, threadId, maxId = 0,
}: {
- chat: ApiChat; threadId: number; maxId?: number;
+ chat: ApiChat; threadId: ThreadId; maxId?: number;
}) {
const isChannel = getEntityTypeById(chat.id) === 'channel';
@@ -851,7 +858,7 @@ export async function markMessageListRead({
} else if (isChannel) {
await invokeRequest(new GramJs.messages.ReadDiscussion({
peer: buildInputPeer(chat.id, chat.accessHash),
- msgId: threadId,
+ msgId: Number(threadId),
readMaxId: fixedMaxId,
}));
} else {
@@ -999,12 +1006,13 @@ export async function fetchDiscussionMessage({
}
export async function searchMessagesLocal({
- chat, type, query, threadId, minDate, maxDate, ...pagination
+ chat, isSavedDialog, type, query, threadId, minDate, maxDate, ...pagination
}: {
chat: ApiChat;
+ isSavedDialog?: boolean;
type?: ApiMessageSearchType | ApiGlobalMessageSearchType;
query?: string;
- threadId?: number;
+ threadId?: ThreadId;
offsetId?: number;
addOffset?: 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({
- peer: buildInputPeer(chat.id, chat.accessHash),
- topMsgId: threadId === MAIN_THREAD_ID ? undefined : threadId,
+ peer: isSavedDialog ? new GramJs.InputPeerSelf() : peer,
+ savedPeerId: isSavedDialog ? peer : undefined,
+ topMsgId: threadId !== MAIN_THREAD_ID && !isSavedDialog ? Number(threadId) : undefined,
filter,
q: query || '',
minDate,
@@ -1288,10 +1299,11 @@ export async function forwardMessages({
noCaptions,
isCurrentUserPremium,
wasDrafted,
+ lastMessageId,
}: {
fromChat: ApiChat;
toChat: ApiChat;
- toThreadId?: number;
+ toThreadId?: ThreadId;
messages: ApiMessage[];
isSilent?: boolean;
scheduledAt?: number;
@@ -1301,6 +1313,7 @@ export async function forwardMessages({
noCaptions?: boolean;
isCurrentUserPremium?: boolean;
wasDrafted?: boolean;
+ lastMessageId?: number;
}) {
const messageIds = messages.map(({ id }) => id);
const randomIds = messages.map(generateRandomBigInt);
@@ -1309,12 +1322,13 @@ export async function forwardMessages({
messages.forEach((message, index) => {
const localMessage = buildLocalForwardedMessage({
toChat,
- toThreadId,
+ toThreadId: Number(toThreadId),
message,
scheduledAt,
noAuthors,
noCaptions,
isCurrentUserPremium,
+ lastMessageId,
});
localMessages[randomIds[index].toString()] = localMessage;
@@ -1337,7 +1351,7 @@ export async function forwardMessages({
silent: isSilent || undefined,
dropAuthor: noAuthors || undefined,
dropMediaCaptions: noCaptions || undefined,
- ...(toThreadId && { topMsgId: toThreadId }),
+ ...(toThreadId && { topMsgId: Number(toThreadId) }),
...(scheduledAt && { scheduleDate: scheduledAt }),
...(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(
{
peer: buildInputPeer(chat.id, chat.accessHash),
filter: new GramJs.InputMessagesFilterPinned(),
q: '',
limit: PINNED_MESSAGES_LIMIT,
- topMsgId: threadId,
+ topMsgId: Number(threadId),
},
), {
abortControllerChatId: chat.id,
diff --git a/src/api/gramjs/methods/users.ts b/src/api/gramjs/methods/users.ts
index bfeb4ff8a..ad6da1a4c 100644
--- a/src/api/gramjs/methods/users.ts
+++ b/src/api/gramjs/methods/users.ts
@@ -267,6 +267,8 @@ export async function fetchProfilePhotos(user?: ApiUser, chat?: ApiChat) {
};
}
+ if (chat?.isRestricted) return undefined;
+
const result = await searchMessagesLocal({
chat: chat!,
type: 'profilePhoto',
diff --git a/src/api/gramjs/updater.ts b/src/api/gramjs/updater.ts
index f558c6d86..c1b7b7751 100644
--- a/src/api/gramjs/updater.ts
+++ b/src/api/gramjs/updater.ts
@@ -597,6 +597,26 @@ export function updater(update: Update) {
ids,
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) {
update.folderPeers.forEach((folderPeer) => {
const { folderId, peer } = folderPeer;
diff --git a/src/api/types/chats.ts b/src/api/types/chats.ts
index 4dddfc705..5575da6c5 100644
--- a/src/api/types/chats.ts
+++ b/src/api/types/chats.ts
@@ -1,6 +1,7 @@
+import type { ThreadId } from '../../types';
import type { ApiBotCommand } from './bots';
import type {
- ApiChatReactions, ApiMessage, ApiPhoto, ApiStickerSet,
+ ApiChatReactions, ApiPhoto, ApiStickerSet,
} from './messages';
import type { ApiChatInviteImporter } from './misc';
import type {
@@ -21,7 +22,6 @@ export interface ApiChat {
type: ApiChatType;
title: string;
hasUnreadMark?: boolean;
- lastMessage?: ApiMessage;
lastReadOutboxMessageId?: number;
lastReadInboxMessageId?: number;
unreadCount?: number;
@@ -48,7 +48,7 @@ export interface ApiChat {
emojiStatus?: ApiEmojiStatus;
isForum?: boolean;
isForumAsMessages?: true;
- topics?: Record;
+ topics?: Record;
listedTopicIds?: number[];
topicsCount?: number;
orderedPinnedTopicIds?: number[];
diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts
index c02f0bcb5..6ae8059b9 100644
--- a/src/api/types/messages.ts
+++ b/src/api/types/messages.ts
@@ -1,3 +1,4 @@
+import type { ThreadId } from '../../types';
import type { ApiWebDocument } from './bots';
import type { ApiGroupCall, PhoneCallAction } from './calls';
import type { ApiChat } from './chats';
@@ -370,12 +371,14 @@ export type ApiInputReplyInfo = ApiInputMessageReplyInfo | ApiInputStoryReplyInf
export interface ApiMessageForwardInfo {
date: number;
+ savedDate?: number;
isImported?: boolean;
isChannelPost: boolean;
channelPostId?: number;
isLinkedChannelPost?: boolean;
fromChatId?: string;
- senderUserId?: string;
+ fromId?: string;
+ savedFromPeerId?: string;
fromMessageId?: number;
hiddenUserName?: string;
postAuthorTitle?: string;
@@ -595,14 +598,14 @@ interface ApiBaseThreadInfo {
export interface ApiCommentsInfo extends ApiBaseThreadInfo {
isCommentsInfo: true;
- threadId?: number;
+ threadId?: ThreadId;
originChannelId: string;
originMessageId: number;
}
export interface ApiMessageThreadInfo extends ApiBaseThreadInfo {
isCommentsInfo: false;
- threadId: number;
+ threadId: ThreadId;
// For linked messages in discussion
fromChannelId?: string;
fromMessageId?: number;
diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts
index ce7354b4e..0934e901d 100644
--- a/src/api/types/updates.ts
+++ b/src/api/types/updates.ts
@@ -6,7 +6,7 @@ import type {
VideoRotation,
VideoState,
} from '../../lib/secret-sauce';
-import type { ApiPrivacyKey, PrivacyVisibility } from '../../types';
+import type { ApiPrivacyKey, PrivacyVisibility, ThreadId } from '../../types';
import type { ApiBotMenuButton } from './bots';
import type {
ApiGroupCall, ApiPhoneCall,
@@ -102,6 +102,12 @@ export type ApiUpdateChat = {
noTopChatsRequest?: boolean;
};
+export type ApiUpdateChatLastMessage = {
+ '@type': 'updateChatLastMessage';
+ id: string;
+ lastMessage: ApiMessage;
+};
+
export type ApiUpdateChatJoin = {
'@type': 'updateChatJoin';
id: string;
@@ -126,7 +132,7 @@ export type ApiUpdateChatInbox = {
export type ApiUpdateChatTypingStatus = {
'@type': 'updateChatTypingStatus';
id: string;
- threadId?: number;
+ threadId?: ThreadId;
typingStatus: ApiTypingStatus | undefined;
};
@@ -170,6 +176,17 @@ export type ApiUpdateChatPinned = {
isPinned: boolean;
};
+export type ApiUpdatePinnedSavedDialogIds = {
+ '@type': 'updatePinnedSavedDialogIds';
+ ids: string[];
+};
+
+export type ApiUpdateSavedDialogPinned = {
+ '@type': 'updateSavedDialogPinned';
+ id: string;
+ isPinned: boolean;
+};
+
export type ApiUpdateChatFolder = {
'@type': 'updateChatFolder';
id: number;
@@ -312,7 +329,7 @@ export type ApiUpdateResetMessages = {
export type ApiUpdateDraftMessage = {
'@type': 'draftMessage';
chatId: string;
- threadId?: number;
+ threadId?: ThreadId;
draft?: ApiDraft;
};
@@ -713,7 +730,7 @@ export type ApiUpdate = (
ApiUpdateRecentReactions | ApiUpdateStory | ApiUpdateReadStories | ApiUpdateDeleteStory | ApiUpdateSentStoryReaction |
ApiRequestReconnectApi | ApiRequestSync | ApiUpdateFetchingDifference | ApiUpdateChannelMessages |
ApiUpdateStealthMode | ApiUpdateAttachMenuBots | ApiUpdateNewAuthorization | ApiUpdateGroupInvitePrivacyForbidden |
- ApiUpdateViewForumAsMessages
+ ApiUpdateViewForumAsMessages | ApiUpdateSavedDialogPinned | ApiUpdatePinnedSavedDialogIds | ApiUpdateChatLastMessage
);
export type OnApiUpdate = (update: ApiUpdate) => void;
diff --git a/src/assets/font-icons/author-hidden.svg b/src/assets/font-icons/author-hidden.svg
new file mode 100644
index 000000000..29611d53f
--- /dev/null
+++ b/src/assets/font-icons/author-hidden.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/assets/font-icons/my-notes.svg b/src/assets/font-icons/my-notes.svg
new file mode 100644
index 000000000..18e760fb9
--- /dev/null
+++ b/src/assets/font-icons/my-notes.svg
@@ -0,0 +1 @@
+
\ No newline at end of file
diff --git a/src/components/common/Avatar.tsx b/src/components/common/Avatar.tsx
index 3a9a8ed36..9ad840c29 100644
--- a/src/components/common/Avatar.tsx
+++ b/src/components/common/Avatar.tsx
@@ -1,6 +1,6 @@
import type { MouseEvent as ReactMouseEvent } from 'react';
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 type {
@@ -16,6 +16,7 @@ import {
getChatTitle,
getPeerStoryHtmlId,
getUserFullName,
+ isAnonymousForwardsChat,
isChatWithRepliesBot,
isDeletedUser,
isUserId,
@@ -33,6 +34,7 @@ import useMediaTransition from '../../hooks/useMediaTransition';
import OptimizedVideo from '../ui/OptimizedVideo';
import AvatarStoryCircle from './AvatarStoryCircle';
+import Icon from './Icon';
import './Avatar.scss';
@@ -51,6 +53,7 @@ type OwnProps = {
photo?: ApiPhoto;
text?: string;
isSavedMessages?: boolean;
+ isSavedDialog?: boolean;
withVideo?: boolean;
withStory?: boolean;
forPremiumPromo?: boolean;
@@ -72,6 +75,7 @@ const Avatar: FC = ({
photo,
text,
isSavedMessages,
+ isSavedDialog,
withVideo,
withStory,
forPremiumPromo,
@@ -94,6 +98,7 @@ const Avatar: FC = ({
const chat = peer && isPeerChat ? peer as ApiChat : undefined;
const isDeleted = user && isDeletedUser(user);
const isReplies = peer && isChatWithRepliesBot(peer.id);
+ const isAnonymousForwards = peer && isAnonymousForwardsChat(peer.id);
const isForum = chat?.isForum;
let imageHash: string | undefined;
let videoHash: string | undefined;
@@ -112,6 +117,26 @@ const Avatar: FC = ({
}
}
+ 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 videoBlobUrl = useMedia(videoHash, !shouldLoadVideo, ApiMediaFormat.BlobUrl);
const hasBlobUrl = Boolean(imgBlobUrl || videoBlobUrl);
@@ -137,40 +162,13 @@ const Avatar: FC = ({
let content: TeactNode | undefined;
const author = user ? getUserFullName(user) : (chat ? getChatTitle(lang, chat) : text);
- if (isSavedMessages) {
+ if (specialIcon) {
content = (
-
- );
- } else if (isDeleted) {
- content = (
-
- );
- } else if (isReplies) {
- content = (
-
);
} else if (hasBlobUrl) {
@@ -214,6 +212,7 @@ const Avatar: FC = ({
className,
getPeerColorClass(peer),
isSavedMessages && 'saved-messages',
+ isAnonymousForwards && 'anonymous-forwards',
isDeleted && 'deleted-account',
isReplies && 'replies-bot-account',
isForum && 'forum',
diff --git a/src/components/common/ChatExtra.tsx b/src/components/common/ChatExtra.tsx
index cbfc61f56..4d34a7963 100644
--- a/src/components/common/ChatExtra.tsx
+++ b/src/components/common/ChatExtra.tsx
@@ -43,6 +43,7 @@ import Switcher from '../ui/Switcher';
type OwnProps = {
chatOrUserId: string;
+ isSavedDialog?: boolean;
forceShowSelf?: boolean;
};
@@ -57,11 +58,13 @@ type StateProps =
description?: string;
chatInviteLink?: string;
topicLink?: string;
+ hasSavedMessages?: boolean;
};
const runDebounced = debounce((cb) => cb(), 500, false);
const ChatExtra: FC = ({
+ chatOrUserId,
user,
chat,
forceShowSelf,
@@ -72,6 +75,7 @@ const ChatExtra: FC = ({
description,
chatInviteLink,
topicLink,
+ hasSavedMessages,
}) => {
const {
loadFullUser,
@@ -79,6 +83,7 @@ const ChatExtra: FC = ({
updateChatMutedState,
updateTopicMutedState,
loadPeerStories,
+ openSavedDialog,
} = getActions();
const {
@@ -151,6 +156,10 @@ const ChatExtra: FC = ({
});
});
+ const handleOpenSavedDialog = useLastCallback(() => {
+ openSavedDialog({ chatId: chatOrUserId });
+ });
+
if (!chat || chat.isRestricted || (isSelf && !forceShowSelf)) {
return undefined;
}
@@ -264,12 +273,17 @@ const ChatExtra: FC = ({
/>
)}
+ {hasSavedMessages && (
+
+ {lang('SavedMessagesTab')}
+
+ )}
);
};
export default memo(withGlobal(
- (global, { chatOrUserId }): StateProps => {
+ (global, { chatOrUserId, isSavedDialog }): StateProps => {
const { countryList: { phoneCodes: phoneCodeList } } = global;
const chat = chatOrUserId ? selectChat(global, chatOrUserId) : undefined;
@@ -277,7 +291,7 @@ export default memo(withGlobal(
const isForum = chat?.isForum;
const isMuted = chat && selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(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;
let description = user ? selectUserFullInfo(global, user.id)?.bio : undefined;
if (!description && chat) {
@@ -291,6 +305,8 @@ export default memo(withGlobal(
const topicLink = topicId ? selectTopicLink(global, chatOrUserId, topicId) : undefined;
+ const hasSavedMessages = !isSavedDialog && global.chats.listIds.saved?.includes(chatOrUserId);
+
return {
phoneCodeList,
chat,
@@ -301,6 +317,7 @@ export default memo(withGlobal(
chatInviteLink,
description,
topicLink,
+ hasSavedMessages,
};
},
)(ChatExtra));
diff --git a/src/components/common/ChatOrUserPicker.tsx b/src/components/common/ChatOrUserPicker.tsx
index d715aa6ad..a1d6b04c8 100644
--- a/src/components/common/ChatOrUserPicker.tsx
+++ b/src/components/common/ChatOrUserPicker.tsx
@@ -5,6 +5,7 @@ import React, {
import { getActions } from '../../global';
import type { ApiChat, ApiTopic } from '../../api/types';
+import type { ThreadId } from '../../types';
import { CHAT_HEIGHT_PX } from '../../config';
import { getCanPostInChat, isUserId } from '../../global/helpers';
@@ -41,7 +42,7 @@ export type OwnProps = {
className?: string;
loadMore?: NoneToVoidFunction;
onSearchChange: (search: string) => void;
- onSelectChatOrUser: (chatOrUserId: string, threadId?: number) => void;
+ onSelectChatOrUser: (chatOrUserId: string, threadId?: ThreadId) => void;
onClose: NoneToVoidFunction;
onCloseAnimationEnd?: NoneToVoidFunction;
};
diff --git a/src/components/common/Composer.tsx b/src/components/common/Composer.tsx
index f4e1468d7..625baded2 100644
--- a/src/components/common/Composer.tsx
+++ b/src/components/common/Composer.tsx
@@ -30,7 +30,9 @@ import type {
ApiDraft, GlobalState, MessageList,
MessageListType, TabState,
} 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 {
@@ -164,7 +166,7 @@ type ComposerType = 'messageList' | 'story';
type OwnProps = {
type: ComposerType;
chatId: string;
- threadId: number;
+ threadId: ThreadId;
storyId?: number;
messageListType: MessageListType;
dropAreaState?: string;
diff --git a/src/components/common/FullNameTitle.tsx b/src/components/common/FullNameTitle.tsx
index d5d5a3106..2f6ecb414 100644
--- a/src/components/common/FullNameTitle.tsx
+++ b/src/components/common/FullNameTitle.tsx
@@ -1,12 +1,14 @@
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 type { ApiChat, ApiPeer, ApiUser } from '../../api/types';
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
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 { copyTextToClipboard } from '../../util/clipboard';
import stopEvent from '../../util/stopEvent';
@@ -30,6 +32,7 @@ type OwnProps = {
withEmojiStatus?: boolean;
emojiStatusSize?: number;
isSavedMessages?: boolean;
+ isSavedDialog?: boolean;
noLoopLimit?: boolean;
canCopyTitle?: boolean;
onEmojiStatusClick?: NoneToVoidFunction;
@@ -44,6 +47,7 @@ const FullNameTitle: FC = ({
withEmojiStatus,
emojiStatusSize,
isSavedMessages,
+ isSavedDialog,
noLoopLimit,
canCopyTitle,
onEmojiStatusClick,
@@ -65,10 +69,26 @@ const FullNameTitle: FC = ({
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 (
-
{lang('SavedMessages')}
+ {specialTitle}
);
}
diff --git a/src/components/common/GroupChatInfo.tsx b/src/components/common/GroupChatInfo.tsx
index 4a24ec0a2..dbb053cd6 100644
--- a/src/components/common/GroupChatInfo.tsx
+++ b/src/components/common/GroupChatInfo.tsx
@@ -3,11 +3,11 @@ import React, { memo, useEffect, useMemo } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global';
import type {
- ApiChat, ApiThreadInfo, ApiTopic, ApiTypingStatus,
+ ApiChat, ApiThreadInfo, ApiTopic, ApiTypingStatus, ApiUser,
} from '../../api/types';
import type { LangFn } from '../../hooks/useLang';
import type { IconName } from '../../types/icons';
-import { MediaViewerOrigin, type StoryViewerOrigin } from '../../types';
+import { MediaViewerOrigin, type StoryViewerOrigin, type ThreadId } from '../../types';
import {
getChatTypeString,
@@ -20,6 +20,7 @@ import {
selectChatOnlineCount,
selectThreadInfo,
selectThreadMessagesCount,
+ selectUser,
} from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
import { REM } from './helpers/mediaDimensions';
@@ -39,7 +40,7 @@ const TOPIC_ICON_SIZE = 2.5 * REM;
type OwnProps = {
chatId: string;
- threadId?: number;
+ threadId?: ThreadId;
className?: string;
statusIcon?: IconName;
typingStatus?: ApiTypingStatus;
@@ -58,6 +59,7 @@ type OwnProps = {
noStatusOrTyping?: boolean;
withStory?: boolean;
storyViewerOrigin?: StoryViewerOrigin;
+ isSavedDialog?: boolean;
onClick?: VoidFunction;
onEmojiStatusClick?: NoneToVoidFunction;
};
@@ -70,6 +72,7 @@ type StateProps =
onlineCount?: number;
areMessagesLoaded: boolean;
messagesCount?: number;
+ self?: ApiUser;
};
const GroupChatInfo: FC = ({
@@ -97,6 +100,8 @@ const GroupChatInfo: FC = ({
storyViewerOrigin,
noEmojiStatus,
emojiStatusSize,
+ isSavedDialog,
+ self,
onClick,
onEmojiStatusClick,
}) => {
@@ -199,15 +204,28 @@ const GroupChatInfo: FC = ({
onClick={onClick}
>
{!noAvatar && !isTopic && (
-
+ <>
+ {isSavedDialog && self && (
+
+ )}
+
+ >
)}
{isTopic && (
= ({
peer={chat}
emojiStatusSize={emojiStatusSize}
withEmojiStatus={!noEmojiStatus}
+ isSavedDialog={isSavedDialog}
onEmojiStatusClick={onEmojiStatusClick}
/>
)}
@@ -258,6 +277,7 @@ export default memo(withGlobal(
const areMessagesLoaded = Boolean(selectChatMessages(global, chatId));
const topic = threadId ? chat?.topics?.[threadId] : undefined;
const messagesCount = topic && selectThreadMessagesCount(global, chatId, threadId!);
+ const self = selectUser(global, global.currentUserId!);
return {
chat,
@@ -266,6 +286,7 @@ export default memo(withGlobal(
topic,
areMessagesLoaded,
messagesCount,
+ self,
};
},
)(GroupChatInfo));
diff --git a/src/components/common/Icon.tsx b/src/components/common/Icon.tsx
index e147e8cd2..1575f4f44 100644
--- a/src/components/common/Icon.tsx
+++ b/src/components/common/Icon.tsx
@@ -1,3 +1,4 @@
+import type { AriaRole } from 'react';
import React from '../../lib/teact/teact';
import type { IconName } from '../../types/icons';
@@ -8,18 +9,24 @@ type OwnProps = {
name: IconName;
className?: string;
style?: string;
+ role?: AriaRole;
+ ariaLabel?: string;
};
const Icon = ({
name,
className,
style,
+ role,
+ ariaLabel,
}: OwnProps) => {
return (
);
};
diff --git a/src/components/common/PrivateChatInfo.tsx b/src/components/common/PrivateChatInfo.tsx
index bb219275f..f24282dfc 100644
--- a/src/components/common/PrivateChatInfo.tsx
+++ b/src/components/common/PrivateChatInfo.tsx
@@ -44,6 +44,7 @@ type OwnProps = {
noStatusOrTyping?: boolean;
noRtl?: boolean;
adminMember?: ApiChatMember;
+ isSavedDialog?: boolean;
className?: string;
onEmojiStatusClick?: NoneToVoidFunction;
};
@@ -52,6 +53,7 @@ type StateProps =
{
user?: ApiUser;
userStatus?: ApiUserStatus;
+ self?: ApiUser;
isSavedMessages?: boolean;
areMessagesLoaded: boolean;
};
@@ -73,7 +75,9 @@ const PrivateChatInfo: FC = ({
noRtl,
user,
userStatus,
+ self,
isSavedMessages,
+ isSavedDialog,
areMessagesLoaded,
adminMember,
ripple,
@@ -166,6 +170,7 @@ const PrivateChatInfo: FC = ({
withEmojiStatus={!noEmojiStatus}
emojiStatusSize={emojiStatusSize}
isSavedMessages={isSavedMessages}
+ isSavedDialog={isSavedDialog}
onEmojiStatusClick={onEmojiStatusClick}
/>
{customTitle && {customTitle}}
@@ -179,6 +184,7 @@ const PrivateChatInfo: FC = ({
withEmojiStatus={!noEmojiStatus}
emojiStatusSize={emojiStatusSize}
isSavedMessages={isSavedMessages}
+ isSavedDialog={isSavedDialog}
onEmojiStatusClick={onEmojiStatusClick}
/>
);
@@ -186,11 +192,22 @@ const PrivateChatInfo: FC = ({
return (
+ {isSavedDialog && self && (
+
+ )}
(
const user = selectUser(global, userId);
const userStatus = selectUserStatus(global, userId);
const isSavedMessages = !forceShowSelf && user && user.isSelf;
+ const self = isSavedMessages ? user : selectUser(global, global.currentUserId!);
const areMessagesLoaded = Boolean(selectChatMessages(global, userId));
return {
@@ -217,6 +235,7 @@ export default memo(withGlobal(
userStatus,
isSavedMessages,
areMessagesLoaded,
+ self,
};
},
)(PrivateChatInfo));
diff --git a/src/components/common/ProfileInfo.tsx b/src/components/common/ProfileInfo.tsx
index bcc85e3a1..d7cffb4b9 100644
--- a/src/components/common/ProfileInfo.tsx
+++ b/src/components/common/ProfileInfo.tsx
@@ -9,7 +9,7 @@ import type { GlobalState } from '../../global/types';
import { MediaViewerOrigin } from '../../types';
import {
- getUserStatus, isChatChannel, isUserId, isUserOnline,
+ getUserStatus, isAnonymousForwardsChat, isChatChannel, isUserId, isUserOnline,
} from '../../global/helpers';
import {
selectChat,
@@ -53,7 +53,6 @@ type StateProps =
user?: ApiUser;
userStatus?: ApiUserStatus;
chat?: ApiChat;
- isSavedMessages?: boolean;
mediaId?: number;
avatarOwnerId?: string;
topic?: ApiTopic;
@@ -75,7 +74,6 @@ const ProfileInfo: FC = ({
user,
userStatus,
chat,
- isSavedMessages,
connectionState,
mediaId,
avatarOwnerId,
@@ -107,8 +105,8 @@ const ProfileInfo: FC = ({
const slideAnimation = hasSlideAnimation ? (lang.isRtl ? 'slideRtl' : 'slide') : 'none';
const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0);
- const isFirst = isSavedMessages || photos.length <= 1 || currentPhotoIndex === 0;
- const isLast = isSavedMessages || photos.length <= 1 || currentPhotoIndex === photos.length - 1;
+ const isFirst = photos.length <= 1 || currentPhotoIndex === 0;
+ 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
useEffect(() => {
@@ -227,7 +225,7 @@ const ProfileInfo: FC = ({
}
function renderPhotoTabs() {
- if (isSavedMessages || !photos || photos.length <= 1) {
+ if (!photos || photos.length <= 1) {
return undefined;
}
@@ -241,7 +239,7 @@ const ProfileInfo: FC = ({
}
function renderPhoto(isActive?: boolean) {
- const photo = !isSavedMessages && photos.length > 0
+ const photo = photos.length > 0
? photos[currentPhotoIndex]
: undefined;
const profilePhoto = photo || userPersonalPhoto || userProfilePhoto || chatProfilePhoto || userFallbackPhoto;
@@ -252,7 +250,6 @@ const ProfileInfo: FC = ({
user={user}
chat={chat}
photo={profilePhoto}
- isSavedMessages={isSavedMessages}
canPlayVideo={Boolean(isActive && canPlayVideo)}
onClick={handleProfilePhotoClick}
/>
@@ -260,6 +257,11 @@ const ProfileInfo: FC = ({
}
function renderStatus() {
+ const peerId = (chatId || userId)!;
+
+ const isAnonymousForwards = isAnonymousForwardsChat(peerId);
+ if (isAnonymousForwards) return undefined;
+
if (user) {
return (
@@ -348,26 +350,24 @@ const ProfileInfo: FC = ({
peer={(user || chat)!}
withEmojiStatus
emojiStatusSize={EMOJI_STATUS_SIZE}
- isSavedMessages={isSavedMessages}
onEmojiStatusClick={handleStatusClick}
noLoopLimit
canCopyTitle
/>
)}
- {!isSavedMessages && renderStatus()}
+ {renderStatus()}
);
};
export default memo(withGlobal(
- (global, { userId, forceShowSelf }): StateProps => {
+ (global, { userId }): StateProps => {
const { connectionState } = global;
const user = selectUser(global, userId);
const isPrivate = isUserId(userId);
const userStatus = selectUserStatus(global, userId);
const chat = selectChat(global, userId);
- const isSavedMessages = !forceShowSelf && user && user.isSelf;
const { mediaId, avatarOwnerId } = selectTabState(global).mediaViewer;
const isForum = chat?.isForum;
const { threadId: currentTopicId } = selectCurrentMessageList(global) || {};
@@ -387,7 +387,6 @@ export default memo(withGlobal(
userProfilePhoto: userFullInfo?.profilePhoto,
userFallbackPhoto: userFullInfo?.fallbackPhoto,
chatProfilePhoto: chatFullInfo?.profilePhoto,
- isSavedMessages,
mediaId,
avatarOwnerId,
emojiStatusSticker,
diff --git a/src/components/common/ProfilePhoto.tsx b/src/components/common/ProfilePhoto.tsx
index f2f236a80..9fdcbc35a 100644
--- a/src/components/common/ProfilePhoto.tsx
+++ b/src/components/common/ProfilePhoto.tsx
@@ -1,5 +1,7 @@
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';
@@ -8,6 +10,7 @@ import {
getChatTitle,
getUserFullName,
getVideoAvatarMediaHash,
+ isAnonymousForwardsChat,
isChatWithRepliesBot,
isDeletedUser,
isUserId,
@@ -27,6 +30,7 @@ import useMediaTransition from '../../hooks/useMediaTransition';
import OptimizedVideo from '../ui/OptimizedVideo';
import Spinner from '../ui/Spinner';
+import Icon from './Icon';
import './ProfilePhoto.scss';
@@ -34,6 +38,7 @@ type OwnProps = {
chat?: ApiChat;
user?: ApiUser;
isSavedMessages?: boolean;
+ isSavedDialog?: boolean;
photo?: ApiPhoto;
canPlayVideo: boolean;
onClick: NoneToVoidFunction;
@@ -44,6 +49,7 @@ const ProfilePhoto: FC = ({
user,
photo,
isSavedMessages,
+ isSavedDialog,
canPlayVideo,
onClick,
}) => {
@@ -55,8 +61,9 @@ const ProfilePhoto: FC = ({
const isDeleted = user && isDeletedUser(user);
const isRepliesChat = chat && isChatWithRepliesBot(chat.id);
+ const isAnonymousForwards = chat && isAnonymousForwardsChat(chat.id);
const peer = user || chat;
- const canHaveMedia = peer && !isSavedMessages && !isDeleted && !isRepliesChat;
+ const canHaveMedia = peer && !isSavedMessages && !isDeleted && !isRepliesChat && !isAnonymousForwards;
const { isVideo } = photo || {};
const avatarHash = canHaveMedia && getChatAvatarHash(peer, 'normal');
@@ -84,14 +91,30 @@ const ProfilePhoto: FC = ({
}
}, [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;
- if (isSavedMessages) {
- content = ;
- } else if (isDeleted) {
- content = ;
- } else if (isRepliesChat) {
- content = ;
+ if (specialIcon) {
+ content = ;
} else if (hasMedia) {
content = (
<>
@@ -141,6 +164,7 @@ const ProfilePhoto: FC = ({
'ProfilePhoto',
getPeerColorClass(peer),
isSavedMessages && 'saved-messages',
+ isAnonymousForwards && 'anonymous-forwards',
isDeleted && 'deleted-account',
isRepliesChat && 'replies-bot-account',
(!isSavedMessages && !hasMedia) && 'no-photo',
diff --git a/src/components/common/RecipientPicker.tsx b/src/components/common/RecipientPicker.tsx
index 84f742c37..38fd95a97 100644
--- a/src/components/common/RecipientPicker.tsx
+++ b/src/components/common/RecipientPicker.tsx
@@ -3,6 +3,7 @@ import React, { memo, useMemo, useState } from '../../lib/teact/teact';
import { getGlobal, withGlobal } from '../../global';
import type { ApiChat, ApiChatType } from '../../api/types';
+import type { ThreadId } from '../../types';
import { MAIN_THREAD_ID } from '../../api/types';
import { API_CHAT_TYPES } from '../../config';
@@ -11,10 +12,10 @@ import {
filterUsersByName,
getCanPostInChat,
isDeletedUser,
- sortChatIds,
} from '../../global/helpers';
import { filterChatIdsByType } from '../../global/selectors';
import { unique } from '../../util/iteratees';
+import sortChatIds from './helpers/sortChatIds';
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
import useLang from '../../hooks/useLang';
@@ -27,7 +28,7 @@ export type OwnProps = {
className?: string;
filter?: ApiChatType[];
loadMore?: NoneToVoidFunction;
- onSelectRecipient: (peerId: string, threadId?: number) => void;
+ onSelectRecipient: (peerId: string, threadId?: ThreadId) => void;
onClose: NoneToVoidFunction;
onCloseAnimationEnd?: NoneToVoidFunction;
};
@@ -85,7 +86,7 @@ const RecipientPicker: FC = ({
const sorted = sortChatIds(unique([
...filterChatsByName(lang, chatIds, chatsById, search, currentUserId),
...(contactIds && filter.includes('users') ? filterUsersByName(contactIds, usersById, search) : []),
- ]), chatsById, undefined, priorityIds);
+ ]), undefined, priorityIds);
return filterChatIdsByType(global, sorted, filter);
}, [pinnedIds, currentUserId, activeListIds, search, archivedListIds, lang, chatsById, contactIds, filter, isOpen]);
diff --git a/src/components/common/helpers/sortChatIds.ts b/src/components/common/helpers/sortChatIds.ts
new file mode 100644
index 000000000..cd17bb8bd
--- /dev/null
+++ b/src/components/common/helpers/sortChatIds.ts
@@ -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');
+}
diff --git a/src/components/left/main/Chat.tsx b/src/components/left/main/Chat.tsx
index 51bf9674b..859c66481 100644
--- a/src/components/left/main/Chat.tsx
+++ b/src/components/left/main/Chat.tsx
@@ -1,5 +1,5 @@
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 type {
@@ -29,6 +29,8 @@ import { getMessageReplyInfo } from '../../../global/helpers/replies';
import {
selectCanAnimateInterface,
selectChat,
+ selectChatLastMessage,
+ selectChatLastMessageId,
selectChatMessage,
selectCurrentMessageList,
selectDraft,
@@ -37,6 +39,7 @@ import {
selectNotifyExceptions,
selectNotifySettings,
selectOutgoingStatus,
+ selectPeer,
selectTabState,
selectThreadParam,
selectTopicFromMessage,
@@ -49,6 +52,7 @@ import { IS_OPEN_IN_NEW_TAB_SUPPORTED } from '../../../util/windowEnvironment';
import useAppLayout from '../../../hooks/useAppLayout';
import useChatContextActions from '../../../hooks/useChatContextActions';
+import useEnsureMessage from '../../../hooks/useEnsureMessage';
import useFlag from '../../../hooks/useFlag';
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
import useLastCallback from '../../../hooks/useLastCallback';
@@ -76,6 +80,7 @@ type OwnProps = {
animationType: ChatAnimationTypes;
isPinned?: boolean;
offsetTop: number;
+ isSavedDialog?: boolean;
observeIntersection?: ObserveFn;
onDragEnter?: (chatId: string) => void;
};
@@ -99,6 +104,9 @@ type StateProps = {
lastMessageTopic?: ApiTopic;
typingStatus?: ApiTypingStatus;
withInterfaceAnimations?: boolean;
+ lastMessageId?: number;
+ lastMessage?: ApiMessage;
+ currentUserId: string;
};
const Chat: FC = ({
@@ -127,10 +135,16 @@ const Chat: FC = ({
canChangeFolder,
lastMessageTopic,
typingStatus,
+ lastMessageId,
+ lastMessage,
+ isSavedDialog,
+ currentUserId,
onDragEnter,
}) => {
const {
openChat,
+ openSavedDialog,
+ toggleChatInfo,
focusLastMessage,
loadTopics,
openForumPanel,
@@ -147,7 +161,9 @@ const Chat: FC = ({
const [shouldRenderChatFolderModal, markRenderChatFolderModal, unmarkRenderChatFolderModal] = 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({
chat,
@@ -164,6 +180,7 @@ const Chat: FC = ({
animationType,
withInterfaceAnimations,
orderDiff,
+ isSavedDialog,
});
const getIsForumPanelClosed = useSelectorSignal(selectIsForumPanelClosed);
@@ -171,6 +188,15 @@ const Chat: FC = ({
const handleClick = useLastCallback(() => {
const noForumTopicPanel = isMobile && isForumAsMessages;
+ if (isSavedDialog) {
+ openSavedDialog({ chatId, noForumTopicPanel: true }, { forceOnHeavyAnimation: true });
+
+ if (isMobile) {
+ toggleChatInfo({ force: false });
+ }
+ return;
+ }
+
if (isForum) {
if (isForumPanelOpen) {
closeForumPanel(undefined, { forceOnHeavyAnimation: true });
@@ -228,6 +254,8 @@ const Chat: FC = ({
isPinned,
isMuted,
canChangeFolder,
+ isSavedDialog,
+ currentUserId,
});
const isIntersecting = useIsIntersecting(ref, chat ? observeIntersection : undefined);
@@ -242,6 +270,16 @@ const Chat: FC = ({
const isOnline = user && userStatus && isUserOnline(user, userStatus);
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) {
return undefined;
}
@@ -260,7 +298,7 @@ const Chat: FC = ({
= ({
= ({
peer={peer}
withEmojiStatus
isSavedMessages={chatId === user?.id && user?.isSelf}
+ isSavedDialog={isSavedDialog}
observeIntersection={observeIntersection}
/>
- {isMuted && }
+ {isMuted && !isSavedDialog && }
- {chat.lastMessage && (
+ {lastMessage && (
)}
{renderSubtitle()}
-
+
{shouldRenderDeleteModal && (
@@ -346,29 +386,34 @@ const Chat: FC = ({
};
export default memo(withGlobal(
- (global, { chatId }): StateProps => {
+ (global, { chatId, isSavedDialog }): StateProps => {
const chat = selectChat(global, chatId);
if (!chat) {
- return {};
+ return {
+ currentUserId: global.currentUserId!,
+ };
}
- const { lastMessage } = chat;
- const { senderId, isOutgoing } = lastMessage || {};
+ const lastMessageId = selectChatLastMessageId(global, chatId, isSavedDialog ? 'saved' : 'all');
+ 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 lastMessageSender = senderId
- ? (selectUser(global, senderId) || selectChat(global, senderId)) : undefined;
+ const lastMessageSender = actualSenderId ? selectPeer(global, actualSenderId) : undefined;
const lastMessageAction = lastMessage ? getMessageAction(lastMessage) : undefined;
const actionTargetMessage = lastMessageAction && replyToMessageId
? selectChatMessage(global, chat.id, replyToMessageId)
: undefined;
const { targetUserIds: actionTargetUserIds, targetChatId: actionTargetChatId } = lastMessageAction || {};
const privateChatUserId = getPrivateChatUserId(chat);
+
const {
chatId: currentChatId,
threadId: currentThreadId,
type: messageListType,
} = 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)
|| chatId === selectTabState(global).forumPanelChatId;
@@ -399,6 +444,9 @@ export default memo(withGlobal(
lastMessageTopic,
typingStatus,
withInterfaceAnimations: selectCanAnimateInterface(global),
+ lastMessage,
+ lastMessageId,
+ currentUserId: global.currentUserId!,
};
},
)(Chat));
diff --git a/src/components/left/main/ChatBadge.tsx b/src/components/left/main/ChatBadge.tsx
index 6106476bf..e39b9c291 100644
--- a/src/components/left/main/ChatBadge.tsx
+++ b/src/components/left/main/ChatBadge.tsx
@@ -21,12 +21,13 @@ type OwnProps = {
wasTopicOpened?: boolean;
isPinned?: boolean;
isMuted?: boolean;
+ isSavedDialog?: boolean;
shouldShowOnlyMostImportant?: boolean;
forceHidden?: boolean | Signal;
};
const ChatBadge: FC = ({
- topic, chat, isPinned, isMuted, shouldShowOnlyMostImportant, wasTopicOpened, forceHidden,
+ topic, chat, isPinned, isMuted, shouldShowOnlyMostImportant, wasTopicOpened, forceHidden, isSavedDialog,
}) => {
const {
unreadMentionsCount = 0, unreadReactionsCount = 0,
@@ -64,7 +65,7 @@ const ChatBadge: FC = ({
|| isTopicUnopened,
);
- const isUnread = Boolean(unreadCount || hasUnreadMark);
+ const isUnread = Boolean((unreadCount || hasUnreadMark) && !isSavedDialog);
const className = buildClassName(
'ChatBadge',
shouldBeMuted && 'muted',
@@ -95,16 +96,21 @@ const ChatBadge: FC = ({
) : undefined;
- const pinnedElement = isPinned && !unreadCountElement && !unreadMentionsElement && !unreadReactionsElement && (
+ const pinnedElement = isPinned && (
);
+ const visiblePinnedElement = !unreadCountElement && !unreadMentionsElement && !unreadReactionsElement
+ && pinnedElement;
+
const elements = [
- unopenedTopicElement, unreadReactionsElement, unreadMentionsElement, unreadCountElement, pinnedElement,
+ unopenedTopicElement, unreadReactionsElement, unreadMentionsElement, unreadCountElement, visiblePinnedElement,
].filter(Boolean);
+ if (isSavedDialog) return pinnedElement;
+
if (elements.length === 0) return undefined;
if (elements.length === 1) return elements[0];
diff --git a/src/components/left/main/ChatList.tsx b/src/components/left/main/ChatList.tsx
index c599c9457..8a35018e9 100644
--- a/src/components/left/main/ChatList.tsx
+++ b/src/components/left/main/ChatList.tsx
@@ -17,6 +17,7 @@ import {
CHAT_HEIGHT_PX,
CHAT_LIST_SLICE,
FRESH_AUTH_PERIOD,
+ SAVED_FOLDER_ID,
} from '../../../config';
import buildClassName from '../../../util/buildClassName';
import { getOrderKey, getPinnedChatsCount } from '../../../util/folderManager';
@@ -41,16 +42,17 @@ import EmptyFolder from './EmptyFolder';
import UnconfirmedSession from './UnconfirmedSession';
type OwnProps = {
- folderType: 'all' | 'archived' | 'folder';
+ className?: string;
+ folderType: 'all' | 'archived' | 'saved' | 'folder';
folderId?: number;
isActive: boolean;
canDisplayArchive?: boolean;
- archiveSettings: GlobalState['archiveSettings'];
+ archiveSettings?: GlobalState['archiveSettings'];
isForumPanelOpen?: boolean;
sessions?: Record;
- foldersDispatch: FolderEditDispatch;
- onSettingsScreenSelect: (screen: SettingsScreens) => void;
- onLeftColumnContentChange: (content: LeftColumnContent) => void;
+ foldersDispatch?: FolderEditDispatch;
+ onSettingsScreenSelect?: (screen: SettingsScreens) => void;
+ onLeftColumnContentChange?: (content: LeftColumnContent) => void;
};
const INTERSECTION_THROTTLE = 200;
@@ -58,6 +60,7 @@ const DRAG_ENTER_DEBOUNCE = 500;
const RESERVED_HOTKEYS = new Set(['9', '0']);
const ChatList: FC = ({
+ className,
folderType,
folderId,
isActive,
@@ -82,18 +85,19 @@ const ChatList: FC = ({
const isArchived = folderType === 'archived';
const isAllFolder = folderType === 'all';
+ const isSaved = folderType === 'saved';
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);
usePeerStoriesPolling(orderedIds);
const chatsHeight = (orderedIds?.length || 0) * CHAT_HEIGHT_PX;
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);
@@ -125,7 +129,7 @@ const ChatList: FC = ({
// Support + to navigate between chats
useEffect(() => {
- if (!isActive || !orderedIds || !IS_APP) {
+ if (!isActive || isSaved || !orderedIds || !IS_APP) {
return undefined;
}
@@ -134,13 +138,13 @@ const ChatList: FC = ({
const [, digit] = e.code.match(/Digit(\d)/) || [];
if (!digit || RESERVED_HOTKEYS.has(digit)) return;
- const isArchiveInList = shouldDisplayArchive && !archiveSettings.isMinimized;
+ const isArchiveInList = shouldDisplayArchive && archiveSettings && !archiveSettings.isMinimized;
const shift = isArchiveInList ? -1 : 0;
const position = Number(digit) + shift - 1;
if (isArchiveInList && position === -1) {
- onLeftColumnContentChange(LeftColumnContent.Archived);
+ onLeftColumnContentChange?.(LeftColumnContent.Archived);
return;
}
@@ -155,7 +159,10 @@ const ChatList: FC = ({
return () => {
document.removeEventListener('keydown', handleKeyDown);
};
- }, [archiveSettings, isActive, onLeftColumnContentChange, openChat, openNextChat, orderedIds, shouldDisplayArchive]);
+ }, [
+ archiveSettings, isSaved, isActive, onLeftColumnContentChange, openChat, openNextChat, orderedIds,
+ shouldDisplayArchive,
+ ]);
const { observe } = useIntersectionObserver({
rootRef: containerRef,
@@ -163,7 +170,7 @@ const ChatList: FC = ({
});
const handleArchivedClick = useLastCallback(() => {
- onLeftColumnContentChange(LeftColumnContent.Archived);
+ onLeftColumnContentChange!(LeftColumnContent.Archived);
closeForumPanel();
});
@@ -199,7 +206,7 @@ const ChatList: FC = ({
toggleStoryRibbon({ isShown: false, isArchived });
});
- const renderedOverflowTrigger = useTopOverscroll(containerRef, handleShowStoryRibbon, handleHideStoryRibbon);
+ const renderedOverflowTrigger = useTopOverscroll(containerRef, handleShowStoryRibbon, handleHideStoryRibbon, isSaved);
function renderChats() {
const viewportOffset = orderedIds!.indexOf(viewportIds![0]);
@@ -213,10 +220,11 @@ const ChatList: FC = ({
return (
= ({
return (
= ({
)}
{viewportIds?.length ? (
renderChats()
- ) : viewportIds && !viewportIds.length ? (
+ ) : viewportIds && !viewportIds.length && !isSaved ? (
(
)
) : (
diff --git a/src/components/left/main/EmptyFolder.tsx b/src/components/left/main/EmptyFolder.tsx
index 6f4bcc8b2..8b20a22b1 100644
--- a/src/components/left/main/EmptyFolder.tsx
+++ b/src/components/left/main/EmptyFolder.tsx
@@ -18,7 +18,7 @@ import styles from './EmptyFolder.module.scss';
type OwnProps = {
folderId?: number;
- folderType: 'all' | 'archived' | 'folder';
+ folderType: 'all' | 'archived' | 'saved' | 'folder';
foldersDispatch: FolderEditDispatch;
onSettingsScreenSelect: (screen: SettingsScreens) => void;
};
diff --git a/src/components/left/main/ForumPanel.tsx b/src/components/left/main/ForumPanel.tsx
index 441f8f8c1..cfb132d54 100644
--- a/src/components/left/main/ForumPanel.tsx
+++ b/src/components/left/main/ForumPanel.tsx
@@ -288,7 +288,7 @@ export default memo(withGlobal(
return {
chat,
- currentTopicId: chatId === currentChatId ? currentThreadId : undefined,
+ currentTopicId: chatId === currentChatId ? Number(currentThreadId) : undefined,
withInterfaceAnimations: selectCanAnimateInterface(global),
};
},
diff --git a/src/components/left/main/hooks/useChatListEntry.tsx b/src/components/left/main/hooks/useChatListEntry.tsx
index 03e9be663..d7cbe91f3 100644
--- a/src/components/left/main/hooks/useChatListEntry.tsx
+++ b/src/components/left/main/hooks/useChatListEntry.tsx
@@ -58,6 +58,7 @@ export default function useChatListEntry({
orderDiff,
withInterfaceAnimations,
isTopic,
+ isSavedDialog,
}: {
chat?: ApiChat;
lastMessage?: ApiMessage;
@@ -71,6 +72,7 @@ export default function useChatListEntry({
actionTargetChatId?: string;
observeIntersection?: ObserveFn;
isTopic?: boolean;
+ isSavedDialog?: boolean;
animationType: ChatAnimationTypes;
orderDiff: number;
@@ -102,14 +104,15 @@ export default function useChatListEntry({
}, [actionTargetUserIds]);
const renderLastMessageOrTyping = useCallback(() => {
- if (typingStatus && lastMessage && typingStatus.timestamp > lastMessage.date * 1000) {
+ if (!isSavedDialog && typingStatus && lastMessage && typingStatus.timestamp > lastMessage.date * 1000) {
return ;
}
const isDraftReplyToTopic = draft && draft.replyInfo?.replyToMsgId === lastMessageTopic?.id;
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) {
return (
@@ -169,7 +172,7 @@ export default function useChatListEntry({
:
>
)}
- {lastMessage.forwardInfo && ()}
+ {!isSavedDialog && lastMessage.forwardInfo && ()}
{lastMessage.replyInfo?.type === 'story' && ()}
{renderSummary(lang, lastMessage, observeIntersection, mediaBlobUrl || mediaThumbnail, isRoundVideo)}
@@ -177,7 +180,7 @@ export default function useChatListEntry({
}, [
actionTargetChatId, actionTargetMessage, actionTargetUsers, chat, chatId, draft, isAction,
isRoundVideo, isTopic, lang, lastMessage, lastMessageSender, lastMessageTopic, mediaBlobUrl, mediaThumbnail,
- observeIntersection, typingStatus,
+ observeIntersection, typingStatus, isSavedDialog,
]);
function renderSubtitle() {
diff --git a/src/components/left/newChat/NewChatStep1.tsx b/src/components/left/newChat/NewChatStep1.tsx
index ca8383d07..89a953f5f 100644
--- a/src/components/left/newChat/NewChatStep1.tsx
+++ b/src/components/left/newChat/NewChatStep1.tsx
@@ -2,11 +2,10 @@ import type { FC } from '../../../lib/teact/teact';
import React, { memo, useCallback, useMemo } from '../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../global';
-import type { ApiChat } from '../../../api/types';
-
-import { filterUsersByName, isUserBot, sortChatIds } from '../../../global/helpers';
+import { filterUsersByName, isUserBot } from '../../../global/helpers';
import { selectTabState } from '../../../global/selectors';
import { unique } from '../../../util/iteratees';
+import sortChatIds from '../../common/helpers/sortChatIds';
import useHistoryBack from '../../../hooks/useHistoryBack';
import useLang from '../../../hooks/useLang';
@@ -25,7 +24,6 @@ export type OwnProps = {
};
type StateProps = {
- chatsById: Record;
localContactIds?: string[];
searchQuery?: string;
isSearching?: boolean;
@@ -40,7 +38,6 @@ const NewChatStep1: FC = ({
onSelectedMemberIdsChange,
onNextStep,
onReset,
- chatsById,
localContactIds,
searchQuery,
isSearching,
@@ -80,11 +77,10 @@ const NewChatStep1: FC = ({
return !user.isSelf && (user.canBeInvitedToGroup || !isUserBot(user));
}),
- chatsById,
false,
selectedMemberIds,
);
- }, [localContactIds, chatsById, searchQuery, localUserIds, globalUserIds, selectedMemberIds]);
+ }, [localContactIds, searchQuery, localUserIds, globalUserIds, selectedMemberIds]);
const handleNextStep = useCallback(() => {
setGlobalSearchQuery({ query: '' });
@@ -133,7 +129,6 @@ const NewChatStep1: FC = ({
export default memo(withGlobal(
(global): StateProps => {
const { userIds: localContactIds } = global.contactList || {};
- const { byId: chatsById } = global.chats;
const {
query: searchQuery,
@@ -145,7 +140,6 @@ export default memo(withGlobal(
const { userIds: localUserIds } = localResults || {};
return {
- chatsById,
localContactIds,
searchQuery,
isSearching: fetchingStatus?.chats,
diff --git a/src/components/left/search/ChatResults.tsx b/src/components/left/search/ChatResults.tsx
index 04e229a7d..1b2ae4a3b 100644
--- a/src/components/left/search/ChatResults.tsx
+++ b/src/components/left/search/ChatResults.tsx
@@ -11,7 +11,6 @@ import { ALL_FOLDER_ID } from '../../../config';
import {
filterChatsByName,
filterUsersByName,
- sortChatIds,
} from '../../../global/helpers';
import { selectTabState } from '../../../global/selectors';
import { getOrderedIds } from '../../../util/folderManager';
@@ -19,6 +18,7 @@ import { unique } from '../../../util/iteratees';
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
import { throttle } from '../../../util/schedulers';
import { renderMessageSummary } from '../../common/helpers/renderMessageText';
+import sortChatIds from '../../common/helpers/sortChatIds';
import useAppLayout from '../../../hooks/useAppLayout';
import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
@@ -147,8 +147,8 @@ const ChatResults: FC = ({
].filter((accountPeerId) => !localPeerIds.includes(accountPeerId)));
return [
- ...sortChatIds(localPeerIds, chatsById, undefined, currentUserId ? [currentUserId] : undefined),
- ...sortChatIds(accountPeerIds, chatsById),
+ ...sortChatIds(localPeerIds, undefined, currentUserId ? [currentUserId] : undefined),
+ ...sortChatIds(accountPeerIds),
];
}, [searchQuery, currentUserId, contactIds, lang, accountChatIds, accountUserIds, chatsById]);
@@ -161,10 +161,9 @@ const ChatResults: FC = ({
return sortChatIds(
unique([...globalChatIds, ...globalUserIds]),
- chatsById,
true,
);
- }, [chatsById, globalChatIds, globalUserIds, searchQuery]);
+ }, [globalChatIds, globalUserIds, searchQuery]);
const foundMessages = useMemo(() => {
if ((!searchQuery && !searchDate) || !foundIds || foundIds.length === 0) {
diff --git a/src/components/main/DraftRecipientPicker.tsx b/src/components/main/DraftRecipientPicker.tsx
index 6579a2c8d..e059d4085 100644
--- a/src/components/main/DraftRecipientPicker.tsx
+++ b/src/components/main/DraftRecipientPicker.tsx
@@ -5,6 +5,7 @@ import React, {
import { getActions } from '../../global';
import type { TabState } from '../../global/types';
+import type { ThreadId } from '../../types';
import useFlag from '../../hooks/useFlag';
import useLang from '../../hooks/useLang';
@@ -33,7 +34,7 @@ const DraftRecipientPicker: FC = ({
}
}, [isOpen, markIsShown]);
- const handleSelectRecipient = useCallback((recipientId: string, threadId?: number) => {
+ const handleSelectRecipient = useCallback((recipientId: string, threadId?: ThreadId) => {
openChatWithDraft({
chatId: recipientId,
threadId,
diff --git a/src/components/main/ForwardRecipientPicker.tsx b/src/components/main/ForwardRecipientPicker.tsx
index 0e7598631..8ba2200dc 100644
--- a/src/components/main/ForwardRecipientPicker.tsx
+++ b/src/components/main/ForwardRecipientPicker.tsx
@@ -4,6 +4,8 @@ import React, {
} from '../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../global';
+import type { ThreadId } from '../../types';
+
import { getChatTitle, getUserFirstOrLastName, isUserId } from '../../global/helpers';
import { selectChat, selectTabState, selectUser } from '../../global/selectors';
@@ -47,7 +49,7 @@ const ForwardRecipientPicker: FC = ({
}
}, [isOpen, markIsShown]);
- const handleSelectRecipient = useCallback((recipientId: string, threadId?: number) => {
+ const handleSelectRecipient = useCallback((recipientId: string, threadId?: ThreadId) => {
const isSelf = recipientId === currentUserId;
if (isStory) {
forwardStory({ toChatId: recipientId });
@@ -82,7 +84,7 @@ const ForwardRecipientPicker: FC = ({
forwardToSavedMessages();
showNotification({ message });
} else {
- setForwardChatOrTopic({ chatId: recipientId, topicId: threadId });
+ setForwardChatOrTopic({ chatId: recipientId, topicId: Number(threadId) });
}
}, [currentUserId, isManyMessages, isStory, lang]);
diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx
index 9fb1ab657..2a0169966 100644
--- a/src/components/main/Main.tsx
+++ b/src/components/main/Main.tsx
@@ -227,6 +227,7 @@ const Main: FC = ({
isSynced,
inviteViaLinkModal,
oneTimeMediaModal,
+ currentUserId,
}) => {
const {
initMain,
@@ -420,7 +421,7 @@ const Main: FC = ({
}, []);
useEffect(() => {
- const parsedLocationHash = parseLocationHash();
+ const parsedLocationHash = parseLocationHash(currentUserId);
if (!parsedLocationHash) return;
openThread({
@@ -428,7 +429,7 @@ const Main: FC = ({
threadId: parsedLocationHash.threadId,
type: parsedLocationHash.type,
});
- }, []);
+ }, [currentUserId]);
// Restore Transition slide class after async rendering
useLayoutEffect(() => {
diff --git a/src/components/main/premium/PremiumFeatureModal.tsx b/src/components/main/premium/PremiumFeatureModal.tsx
index 34b75b14a..b1d43b3ca 100644
--- a/src/components/main/premium/PremiumFeatureModal.tsx
+++ b/src/components/main/premium/PremiumFeatureModal.tsx
@@ -86,7 +86,9 @@ const PREMIUM_BOTTOM_VIDEOS: string[] = [
'translations',
];
-type ApiLimitTypeWithoutUpload = Exclude;
+type ApiLimitTypeWithoutUpload = Exclude;
const LIMITS_ORDER: ApiLimitTypeWithoutUpload[] = [
'channels',
diff --git a/src/components/main/premium/common/PremiumLimitReachedModal.tsx b/src/components/main/premium/common/PremiumLimitReachedModal.tsx
index 3c705a9a3..f0d967143 100644
--- a/src/components/main/premium/common/PremiumLimitReachedModal.tsx
+++ b/src/components/main/premium/common/PremiumLimitReachedModal.tsx
@@ -31,6 +31,7 @@ const LIMIT_DESCRIPTION: Record = {
channels: 'LimitReachedCommunities',
chatlistInvites: 'LimitReachedFolderLinks',
chatlistJoined: 'LimitReachedSharedFolders',
+ savedDialogsPinned: 'LimitReachedPinSavedDialogs',
};
const LIMIT_DESCRIPTION_BLOCKED: Record = {
@@ -42,6 +43,7 @@ const LIMIT_DESCRIPTION_BLOCKED: Record = {
channels: 'LimitReachedCommunitiesLocked',
chatlistInvites: 'LimitReachedFolderLinksLocked',
chatlistJoined: 'LimitReachedSharedFoldersLocked',
+ savedDialogsPinned: 'LimitReachedPinSavedDialogsLocked',
};
const LIMIT_DESCRIPTION_PREMIUM: Record = {
@@ -53,6 +55,7 @@ const LIMIT_DESCRIPTION_PREMIUM: Record = {
channels: 'LimitReachedCommunitiesPremium',
chatlistInvites: 'LimitReachedFolderLinksPremium',
chatlistJoined: 'LimitReachedSharedFoldersPremium',
+ savedDialogsPinned: 'LimitReachedPinSavedDialogsPremium',
};
const LIMIT_ICON: Record = {
@@ -64,6 +67,7 @@ const LIMIT_ICON: Record = {
channels: 'chats-badge',
chatlistInvites: 'link-badge',
chatlistJoined: 'folder-badge',
+ savedDialogsPinned: 'pin-badge',
};
const LIMIT_VALUE_FORMATTER: Partial string>> = {
diff --git a/src/components/mediaViewer/MediaViewer.tsx b/src/components/mediaViewer/MediaViewer.tsx
index 8902c7f09..5b3b2884e 100644
--- a/src/components/mediaViewer/MediaViewer.tsx
+++ b/src/components/mediaViewer/MediaViewer.tsx
@@ -7,7 +7,7 @@ import { getActions, withGlobal } from '../../global';
import type {
ApiMessage, ApiPeer, ApiPhoto, ApiUser,
} from '../../api/types';
-import { MediaViewerOrigin } from '../../types';
+import { MediaViewerOrigin, type ThreadId } from '../../types';
import { ANIMATION_END_DELAY } from '../../config';
import { getChatMediaMessageIds, isChatAdmin, isUserId } from '../../global/helpers';
@@ -57,7 +57,7 @@ import './MediaViewer.scss';
type StateProps = {
chatId?: string;
- threadId?: number;
+ threadId?: ThreadId;
mediaId?: number;
senderId?: string;
isChatWithSelf?: boolean;
diff --git a/src/components/mediaViewer/MediaViewerContent.tsx b/src/components/mediaViewer/MediaViewerContent.tsx
index fabf6ea5c..af92a5c75 100644
--- a/src/components/mediaViewer/MediaViewerContent.tsx
+++ b/src/components/mediaViewer/MediaViewerContent.tsx
@@ -5,7 +5,7 @@ import { withGlobal } from '../../global';
import type {
ApiDimensions, ApiMessage, ApiPeer,
} from '../../api/types';
-import { MediaViewerOrigin } from '../../types';
+import { MediaViewerOrigin, type ThreadId } from '../../types';
import {
selectChat, selectChatMessage, selectIsMessageProtected, selectScheduledMessage, selectTabState, selectUser,
@@ -31,7 +31,7 @@ import './MediaViewerContent.scss';
type OwnProps = {
mediaId?: number;
chatId?: string;
- threadId?: number;
+ threadId?: ThreadId;
avatarOwnerId?: string;
origin?: MediaViewerOrigin;
isActive?: boolean;
@@ -45,7 +45,7 @@ type StateProps = {
chatId?: string;
mediaId?: number;
senderId?: string;
- threadId?: number;
+ threadId?: ThreadId;
avatarOwner?: ApiPeer;
message?: ApiMessage;
origin?: MediaViewerOrigin;
diff --git a/src/components/mediaViewer/MediaViewerSlides.tsx b/src/components/mediaViewer/MediaViewerSlides.tsx
index 9432101fc..5778485ba 100644
--- a/src/components/mediaViewer/MediaViewerSlides.tsx
+++ b/src/components/mediaViewer/MediaViewerSlides.tsx
@@ -3,7 +3,7 @@ import React, {
memo, useEffect, useLayoutEffect, useRef, useState,
} from '../../lib/teact/teact';
-import type { MediaViewerOrigin } from '../../types';
+import type { MediaViewerOrigin, ThreadId } from '../../types';
import type { RealTouchEvent } from '../../util/captureEvents';
import { animateNumber, timingFunctions } from '../../util/animation';
@@ -46,7 +46,7 @@ type OwnProps = {
isOpen?: boolean;
selectMedia: (id?: number) => void;
chatId?: string;
- threadId?: number;
+ threadId?: ThreadId;
avatarOwnerId?: string;
origin?: MediaViewerOrigin;
withAnimation?: boolean;
diff --git a/src/components/middle/ActionMessage.tsx b/src/components/middle/ActionMessage.tsx
index 3428951fc..76c3faca8 100644
--- a/src/components/middle/ActionMessage.tsx
+++ b/src/components/middle/ActionMessage.tsx
@@ -9,7 +9,7 @@ import type {
} from '../../api/types';
import type { MessageListType } from '../../global/types';
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
-import type { FocusDirection } from '../../types';
+import type { FocusDirection, ThreadId } from '../../types';
import type { PinnedIntersectionChangedCallback } from './hooks/usePinnedMessage';
import {
@@ -45,7 +45,7 @@ import ContextMenuContainer from './message/ContextMenuContainer.async';
type OwnProps = {
message: ApiMessage;
- threadId?: number;
+ threadId?: ThreadId;
messageListType?: MessageListType;
observeIntersectionForReading?: ObserveFn;
observeIntersectionForLoading?: ObserveFn;
diff --git a/src/components/middle/ContactGreeting.tsx b/src/components/middle/ContactGreeting.tsx
index 17f7bad22..c73a77835 100644
--- a/src/components/middle/ContactGreeting.tsx
+++ b/src/components/middle/ContactGreeting.tsx
@@ -6,7 +6,7 @@ import type { ApiSticker, ApiUpdateConnectionStateType } from '../../api/types';
import type { MessageList } from '../../global/types';
import { getPeerIdDividend } from '../../global/helpers';
-import { selectChat, selectCurrentMessageList } from '../../global/selectors';
+import { selectChat, selectChatLastMessage, selectCurrentMessageList } from '../../global/selectors';
import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback';
@@ -101,10 +101,12 @@ export default memo(withGlobal(
return {};
}
+ const lastMessage = selectChatLastMessage(global, chat.id);
+
return {
sticker,
- lastUnreadMessageId: chat.lastMessage && chat.lastMessage.id !== chat.lastReadInboxMessageId
- ? chat.lastMessage.id
+ lastUnreadMessageId: lastMessage && lastMessage.id !== chat.lastReadInboxMessageId
+ ? lastMessage.id
: undefined,
connectionState: global.connectionState,
currentMessageList: selectCurrentMessageList(global),
diff --git a/src/components/middle/HeaderActions.tsx b/src/components/middle/HeaderActions.tsx
index f5c24669d..0891e3dd9 100644
--- a/src/components/middle/HeaderActions.tsx
+++ b/src/components/middle/HeaderActions.tsx
@@ -5,13 +5,14 @@ import React, {
import { getActions, withGlobal } from '../../global';
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 { ManagementScreens } from '../../types';
import { requestMeasure, requestNextMutation } from '../../lib/fasterdom/fasterdom';
import {
getHasAdminRight,
+ isAnonymousForwardsChat,
isChatBasicGroup, isChatChannel, isChatSuperGroup, isUserId,
} from '../../global/helpers';
import {
@@ -44,7 +45,7 @@ import HeaderMenuContainer from './HeaderMenuContainer.async';
interface OwnProps {
chatId: string;
- threadId: number;
+ threadId: ThreadId;
messageListType: MessageListType;
canExpandActions: boolean;
isForForum?: boolean;
@@ -485,7 +486,8 @@ export default memo(withGlobal(
(isMainThread || chat.isForum) && (isChannel || isChatSuperGroup(chat)) && chat.isNotJoined,
);
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 canLeave = isMainThread && !canSubscribe;
const canEnterVoiceChat = ARE_CALLS_SUPPORTED && isMainThread && chat.isCallActive;
diff --git a/src/components/middle/HeaderMenuContainer.tsx b/src/components/middle/HeaderMenuContainer.tsx
index 74fdd3e83..d2eb8b2e9 100644
--- a/src/components/middle/HeaderMenuContainer.tsx
+++ b/src/components/middle/HeaderMenuContainer.tsx
@@ -5,7 +5,7 @@ import React, {
import { getActions, withGlobal } from '../../global';
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 { MAIN_THREAD_ID } from '../../api/types';
@@ -71,7 +71,7 @@ const BOT_BUTTONS: Record = {
export type OwnProps = {
chatId: string;
- threadId: number;
+ threadId: ThreadId;
isOpen: boolean;
withExtraActions: boolean;
anchor: IAnchorPosition;
@@ -276,7 +276,7 @@ const HeaderMenuContainer: FC = ({
});
const handleEditTopicClick = useLastCallback(() => {
- openEditTopicPanel({ chatId, topicId: threadId });
+ openEditTopicPanel({ chatId, topicId: Number(threadId) });
setShouldCloseFast(!isRightColumnShown);
closeMenu();
});
diff --git a/src/components/middle/MessageList.scss b/src/components/middle/MessageList.scss
index 1a086e489..1aa680da9 100644
--- a/src/components/middle/MessageList.scss
+++ b/src/components/middle/MessageList.scss
@@ -77,7 +77,8 @@
}
&.select-mode-active,
- &.type-pinned {
+ &.type-pinned,
+ &.saved-dialog {
margin-bottom: 0;
.last-in-list {
diff --git a/src/components/middle/MessageList.tsx b/src/components/middle/MessageList.tsx
index a6cda0844..23ee1327a 100644
--- a/src/components/middle/MessageList.tsx
+++ b/src/components/middle/MessageList.tsx
@@ -15,16 +15,19 @@ import type { MessageListType } from '../../global/types';
import type { Signal } from '../../util/signals';
import type { PinnedIntersectionChangedCallback } from './hooks/usePinnedMessage';
import { MAIN_THREAD_ID } from '../../api/types';
-import { LoadMoreDirection } from '../../types';
+import { LoadMoreDirection, type ThreadId } from '../../types';
import {
ANIMATION_END_DELAY,
+ ANONYMOUS_USER_ID,
MESSAGE_LIST_SLICE,
SERVICE_NOTIFICATIONS_USER_ID,
} from '../../config';
import { forceMeasure, requestForcedReflow, requestMeasure } from '../../lib/fasterdom/fasterdom';
import {
+ getIsSavedDialog,
getMessageHtmlId,
+ isAnonymousForwardsChat,
isChatChannel,
isChatGroup,
isChatWithRepliesBot,
@@ -35,6 +38,7 @@ import {
selectBot,
selectChat,
selectChatFullInfo,
+ selectChatLastMessage,
selectChatMessages,
selectChatScheduledMessages,
selectCurrentMessageIds,
@@ -80,7 +84,7 @@ import './MessageList.scss';
type OwnProps = {
chatId: string;
- threadId: number;
+ threadId: ThreadId;
type: MessageListType;
isComments?: boolean;
canPost: boolean;
@@ -101,6 +105,7 @@ type StateProps = {
isGroupChat?: boolean;
isChatWithSelf?: boolean;
isRepliesChat?: boolean;
+ isAnonymousForwards?: boolean;
isCreator?: boolean;
isBot?: boolean;
isSynced?: boolean;
@@ -119,6 +124,7 @@ type StateProps = {
isServiceNotificationsChat?: boolean;
isEmptyThread?: boolean;
isForum?: boolean;
+ currentUserId: string;
};
const MESSAGE_REACTIONS_POLLING_INTERVAL = 20 * 1000;
@@ -152,6 +158,7 @@ const MessageList: FC = ({
isReady,
isChatWithSelf,
isRepliesChat,
+ isAnonymousForwards,
isCreator,
isBot,
messageIds,
@@ -171,8 +178,9 @@ const MessageList: FC = ({
topic,
noMessageSendingAnimation,
isServiceNotificationsChat,
- onPinnedIntersectionChange,
+ currentUserId,
getForceNextPinnedInHeader,
+ onPinnedIntersectionChange,
}) => {
const {
loadViewportMessages, setScrollOffset, loadSponsoredMessages, loadMessageReactions, copyMessagesByIds,
@@ -199,6 +207,9 @@ const MessageList: FC = ({
const isScrollTopJustUpdatedRef = useRef(false);
const shouldAnimateAppearanceRef = useRef(Boolean(lastMessage));
+ const isSavedDialog = getIsSavedDialog(chatId, threadId, currentUserId);
+ const hasOpenChatButton = isSavedDialog && threadId !== ANONYMOUS_USER_ID;
+
const areMessagesLoaded = Boolean(messageIds);
useSyncEffect(() => {
@@ -250,7 +261,7 @@ const MessageList: FC = ({
? groupMessages(
orderBy(listedMessages, orderRule),
memoUnreadDividerBeforeIdRef.current,
- !isForum ? threadId : undefined,
+ !isForum ? Number(threadId) : undefined,
isChatWithSelf,
)
: undefined;
@@ -547,9 +558,9 @@ const MessageList: FC = ({
}, [isSelectModeActive]);
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 shouldRenderGreeting = isUserId(chatId) && !isChatWithSelf && !isBot
+ const shouldRenderGreeting = isUserId(chatId) && !isChatWithSelf && !isBot && !isAnonymousForwards
&& (
(
!messageGroups && !lastMessage && messageIds
@@ -575,6 +586,7 @@ const MessageList: FC = ({
isSelectModeActive && 'select-mode-active',
isScrolled && 'scrolled',
!isReady && 'is-animating',
+ hasOpenChatButton && 'saved-dialog',
);
const hasMessages = (messageIds && messageGroups) || lastMessage;
@@ -610,6 +622,7 @@ const MessageList: FC = ({
chatId={chatId}
isComments={isComments}
isChannelChat={isChannelChat}
+ isSavedDialog={isSavedDialog}
messageIds={messageIds || [lastMessage!.id]}
messageGroups={messageGroups || groupMessages([lastMessage!])}
getContainerHeight={getContainerHeight}
@@ -642,9 +655,10 @@ const MessageList: FC = ({
export default memo(withGlobal(
(global, { chatId, threadId, type }): StateProps => {
+ const currentUserId = global.currentUserId!;
const chat = selectChat(global, chatId);
if (!chat) {
- return {};
+ return { currentUserId };
}
const messageIds = selectCurrentMessageIds(global, chatId, threadId, type);
@@ -652,14 +666,17 @@ export default memo(withGlobal(
? selectChatScheduledMessages(global, chatId)
: selectChatMessages(global, chatId);
+ const isSavedDialog = getIsSavedDialog(chatId, threadId, currentUserId);
+
if (
- threadId !== MAIN_THREAD_ID && !chat?.isForum
- && !(messagesById && threadId && messagesById[threadId])
+ threadId !== MAIN_THREAD_ID && !isSavedDialog && !chat?.isForum
+ && !(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 withLastMessageWhenPreloading = (
@@ -683,6 +700,7 @@ export default memo(withGlobal(
isCreator: chat.isCreator,
isChatWithSelf: selectIsChatWithSelf(global, chatId),
isRepliesChat: isChatWithRepliesBot(chatId),
+ isAnonymousForwards: isAnonymousForwardsChat(chatId),
isBot: Boolean(chatBot),
isSynced: global.isSynced,
messageIds,
@@ -697,6 +715,7 @@ export default memo(withGlobal(
isServiceNotificationsChat: chatId === SERVICE_NOTIFICATIONS_USER_ID,
isForum: chat.isForum,
isEmptyThread,
+ currentUserId,
...(withLastMessageWhenPreloading && { lastMessage }),
};
},
diff --git a/src/components/middle/MessageListContent.tsx b/src/components/middle/MessageListContent.tsx
index 027133787..2aaf8a5d0 100644
--- a/src/components/middle/MessageListContent.tsx
+++ b/src/components/middle/MessageListContent.tsx
@@ -4,6 +4,7 @@ import React, { memo } from '../../lib/teact/teact';
import { getActions } from '../../global';
import type { MessageListType } from '../../global/types';
+import type { ThreadId } from '../../types';
import type { Signal } from '../../util/signals';
import type { MessageDateGroup } from './helpers/groupMessages';
import type { PinnedIntersectionChangedCallback } from './hooks/usePinnedMessage';
@@ -33,7 +34,7 @@ import MessageListBotInfo from './MessageListBotInfo';
interface OwnProps {
isCurrentUserPremium?: boolean;
chatId: string;
- threadId: number;
+ threadId: ThreadId;
messageIds: number[];
messageGroups: MessageDateGroup[];
getContainerHeight: Signal;
@@ -54,6 +55,7 @@ interface OwnProps {
isSchedule: boolean;
shouldRenderBotInfo?: boolean;
noAppearanceAnimation: boolean;
+ isSavedDialog?: boolean;
onFabToggle: AnyToVoidFunction;
onNotchToggle: AnyToVoidFunction;
onPinnedIntersectionChange: PinnedIntersectionChangedCallback;
@@ -85,6 +87,7 @@ const MessageListContent: FC = ({
isSchedule,
shouldRenderBotInfo,
noAppearanceAnimation,
+ isSavedDialog,
onFabToggle,
onNotchToggle,
onPinnedIntersectionChange,
@@ -92,6 +95,7 @@ const MessageListContent: FC = ({
const { openHistoryCalendar } = getActions();
const getIsReady = useDerivedSignal(isReady);
+ const areDatesClickable = !isSavedDialog && !isSchedule;
const {
observeIntersectionForReading,
@@ -162,7 +166,7 @@ const MessageListContent: FC = ({
message={message}
threadId={threadId}
messageListType={type}
- isInsideTopic={Boolean(threadId && threadId !== MAIN_THREAD_ID)}
+ isInsideTopic={Boolean(threadId && threadId !== MAIN_THREAD_ID && !isSavedDialog)}
observeIntersectionForReading={observeIntersectionForReading}
observeIntersectionForLoading={observeIntersectionForLoading}
observeIntersectionForPlaying={observeIntersectionForPlaying}
@@ -261,10 +265,10 @@ const MessageListContent: FC = ({
teactFastList
>
openHistoryCalendar({ selectedAt: dateGroup.datetime }) : undefined}
+ onClick={areDatesClickable ? () => openHistoryCalendar({ selectedAt: dateGroup.datetime }) : undefined}
>
{isSchedule && dateGroup.originalDate === SCHEDULED_WHEN_ONLINE && (
diff --git a/src/components/middle/MiddleColumn.scss b/src/components/middle/MiddleColumn.scss
index d59612cf7..4fea37fa4 100644
--- a/src/components/middle/MiddleColumn.scss
+++ b/src/components/middle/MiddleColumn.scss
@@ -254,8 +254,7 @@
.Composer,
.MessageSelectToolbar,
-.unpin-all-button,
-.join-subscribe-button,
+.composer-button,
.messaging-disabled {
width: 100%;
display: flex;
@@ -264,8 +263,7 @@
}
.MessageSelectToolbar-inner,
-.unpin-all-button,
-.join-subscribe-button,
+.composer-button,
.messaging-disabled {
.mask-image-disabled & {
box-shadow: 0 0.25rem 0.5rem 0.125rem var(--color-default-shadow);
@@ -310,11 +308,11 @@
}
}
- .join-subscribe-button,
- .unpin-all-button {
+ .composer-button {
height: 3.5rem;
transform: scaleX(1);
transition: transform var(--select-transition), background-color 0.15s, color 0.15s;
+ color: var(--color-primary);
.select-mode-active + .middle-column-footer & {
box-shadow: none;
diff --git a/src/components/middle/MiddleColumn.tsx b/src/components/middle/MiddleColumn.tsx
index bddc14f5d..dbe53c357 100644
--- a/src/components/middle/MiddleColumn.tsx
+++ b/src/components/middle/MiddleColumn.tsx
@@ -9,11 +9,12 @@ import type {
ActiveEmojiInteraction,
MessageListType,
} from '../../global/types';
-import type { ThemeKey } from '../../types';
+import type { ThemeKey, ThreadId } from '../../types';
import { MAIN_THREAD_ID } from '../../api/types';
import {
ANIMATION_END_DELAY,
+ ANONYMOUS_USER_ID,
EDITABLE_INPUT_CSS_SELECTOR,
EDITABLE_INPUT_ID,
GENERAL_TOPIC_ID,
@@ -29,6 +30,7 @@ import {
getCanPostInChat,
getForumComposerPlaceholder,
getHasAdminRight,
+ getIsSavedDialog,
getMessageSendingRestrictionReason,
isChatChannel,
isChatGroup,
@@ -101,7 +103,7 @@ interface OwnProps {
type StateProps = {
chatId?: string;
- threadId?: number;
+ threadId?: ThreadId;
isComments?: boolean;
messageListType?: MessageListType;
chat?: ApiChat;
@@ -145,6 +147,8 @@ type StateProps = {
topMessageId?: number;
canUnpin?: boolean;
canUnblock?: boolean;
+ isSavedDialog?: boolean;
+ canShowOpenChatButton?: boolean;
};
function isImage(item: DataTransferItem) {
@@ -201,6 +205,8 @@ function MiddleColumn({
topMessageId,
canUnpin,
canUnblock,
+ isSavedDialog,
+ canShowOpenChatButton,
}: OwnProps & StateProps) {
const {
openChat,
@@ -378,6 +384,10 @@ function MiddleColumn({
setIsUnpinModalOpen(false);
});
+ const handleOpenChatFromSaved = useLastCallback(() => {
+ openChat({ id: String(threadId) });
+ });
+
const handleUnpinAllMessages = useLastCallback(() => {
unpinAllMessages({ chatId: chatId!, threadId: threadId! });
closeUnpinModal();
@@ -465,12 +475,12 @@ function MiddleColumn({
});
const isMessagingDisabled = Boolean(
- !isPinnedMessageList && !renderingCanPost && !renderingCanRestartBot && !renderingCanStartBot
+ !isPinnedMessageList && !isSavedDialog && !renderingCanPost && !renderingCanRestartBot && !renderingCanStartBot
&& !renderingCanSubscribe && composerRestrictionMessage,
);
const withMessageListBottomShift = Boolean(
renderingCanRestartBot || renderingCanSubscribe || renderingShouldSendJoinRequest || renderingCanStartBot
- || isPinnedMessageList || renderingCanUnblock,
+ || isPinnedMessageList || canShowOpenChatButton || renderingCanUnblock,
);
const withExtraShift = Boolean(isMessagingDisabled || isSelectModeActive || isPinnedMessageList);
@@ -563,7 +573,7 @@ function MiddleColumn({
size="tiny"
fluid
color="secondary"
- className="unpin-all-button"
+ className="composer-button unpin-all-button"
onClick={handleOpenUnpinModal}
>
@@ -571,6 +581,19 @@ function MiddleColumn({
)}
+ {canShowOpenChatButton && (
+
+
+
+ )}
{isMessagingDisabled && (
@@ -588,7 +611,7 @@ function MiddleColumn({
size="tiny"
fluid
ripple
- className="join-subscribe-button"
+ className="composer-button join-subscribe-button"
onClick={handleSubscribeClick}
>
{lang(renderingIsChannel ? 'ProfileJoinChannel' : 'ProfileJoinGroup')}
@@ -601,7 +624,7 @@ function MiddleColumn({
size="tiny"
fluid
ripple
- className="join-subscribe-button"
+ className="composer-button join-subscribe-button"
onClick={handleSubscribeClick}
>
{lang('ChannelJoinRequest')}
@@ -614,7 +637,7 @@ function MiddleColumn({
size="tiny"
fluid
ripple
- className="join-subscribe-button"
+ className="composer-button join-subscribe-button"
onClick={handleStartBot}
>
{lang('BotStart')}
@@ -627,7 +650,7 @@ function MiddleColumn({
size="tiny"
fluid
ripple
- className="join-subscribe-button"
+ className="composer-button join-subscribe-button"
onClick={handleRestartBot}
>
{lang('BotRestart')}
@@ -640,7 +663,7 @@ function MiddleColumn({
size="tiny"
fluid
ripple
- className="join-subscribe-button"
+ className="composer-button join-subscribe-button"
onClick={handleUnblock}
>
{lang('Unblock')}
@@ -763,8 +786,11 @@ export default memo(withGlobal
(
? selectChatMessage(global, audioChatId, audioMessageId)
: undefined;
- const isCommentThread = threadId !== MAIN_THREAD_ID && !chat?.isForum;
- const topMessageId = isCommentThread ? threadId : undefined;
+ const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
+ 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 && (
isPrivate || (
@@ -787,7 +813,8 @@ export default memo(withGlobal(
&& (!chat || canPost)
&& !isBotNotStarted
&& !(shouldJoinToSend && chat?.isNotJoined)
- && !shouldBlockSendInForum,
+ && !shouldBlockSendInForum
+ && !isSavedDialog,
isPinnedMessageList,
currentUserBannedRights: chat?.currentUserBannedRights,
defaultBannedRights: chat?.defaultBannedRights,
@@ -807,6 +834,8 @@ export default memo(withGlobal(
topMessageId,
canUnpin,
canUnblock,
+ isSavedDialog,
+ canShowOpenChatButton,
};
},
)(MiddleColumn));
diff --git a/src/components/middle/MiddleHeader.scss b/src/components/middle/MiddleHeader.scss
index 3d47864a2..0e1426ec0 100644
--- a/src/components/middle/MiddleHeader.scss
+++ b/src/components/middle/MiddleHeader.scss
@@ -243,6 +243,17 @@
}
}
+ .saved-dialog-avatar {
+ position: absolute;
+ }
+
+ .overlay-avatar {
+ margin-left: 2.125rem;
+ .inner {
+ outline: 2px solid var(--color-background);
+ }
+ }
+
.status,
.typing-status {
display: inline;
diff --git a/src/components/middle/MiddleHeader.tsx b/src/components/middle/MiddleHeader.tsx
index a2329d8b0..4f8fcd62d 100644
--- a/src/components/middle/MiddleHeader.tsx
+++ b/src/components/middle/MiddleHeader.tsx
@@ -10,7 +10,7 @@ import type {
import type { GlobalState, MessageListType } from '../../global/types';
import type { Signal } from '../../util/signals';
import { MAIN_THREAD_ID } from '../../api/types';
-import { StoryViewerOrigin } from '../../types';
+import { StoryViewerOrigin, type ThreadId } from '../../types';
import {
EDITABLE_INPUT_CSS_SELECTOR,
@@ -24,6 +24,7 @@ import {
import { requestMutation } from '../../lib/fasterdom/fasterdom';
import {
getChatTitle,
+ getIsSavedDialog,
getMessageKey,
getSenderTitle,
isChatChannel,
@@ -83,7 +84,7 @@ const EMOJI_STATUS_SIZE = 22;
type OwnProps = {
chatId: string;
- threadId: number;
+ threadId: ThreadId;
messageListType: MessageListType;
isComments?: boolean;
isReady?: boolean;
@@ -98,6 +99,7 @@ type StateProps = {
pinnedMessageIds?: number[] | number;
messagesById?: Record;
canUnpin?: boolean;
+ isSavedDialog?: boolean;
topMessageSender?: ApiPeer;
typingStatus?: ApiTypingStatus;
isSelectModeActive?: boolean;
@@ -145,6 +147,7 @@ const MiddleHeader: FC = ({
getCurrentPinnedIndexes,
getLoadingPinnedId,
emojiStatusSticker,
+ isSavedDialog,
onFocusPinnedMessage,
}) => {
const {
@@ -352,7 +355,7 @@ const MiddleHeader: FC = ({
function renderInfo() {
if (messageListType === 'thread') {
- if (threadId === MAIN_THREAD_ID || chat?.isForum) {
+ if (threadId === MAIN_THREAD_ID || isSavedDialog || chat?.isForum) {
return renderChatInfo();
}
}
@@ -377,25 +380,30 @@ const MiddleHeader: FC = ({
}
function renderChatInfo() {
+ // TODO Implement count
+ const savedMessagesStatus = isSavedDialog ? lang('SavedMessages') : undefined;
+
+ const realChatId = isSavedDialog ? String(threadId) : chatId;
return (
<>
- {(isLeftColumnHideable || currentTransitionKey > 0) && renderBackButton(shouldShowCloseButton, true)}
+ {(isLeftColumnHideable || currentTransitionKey > 0) && renderBackButton(shouldShowCloseButton, !isSavedDialog)}
- {isUserId(chatId) ? (
+ {isUserId(realChatId) ? (
= ({
/>
) : (
(
const emojiStatus = chat?.emojiStatus;
const emojiStatusSticker = emojiStatus && global.customEmojis.byId[emojiStatus.documentId];
+ const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
+
const state: StateProps = {
typingStatus,
isLeftColumnShown,
@@ -569,6 +580,7 @@ export default memo(withGlobal(
isFetchingDifference: global.isFetchingDifference,
emojiStatusSticker,
hasButtonInHeader: canStartBot || canRestartBot || canSubscribe || shouldSendJoinRequest,
+ isSavedDialog,
};
const messagesById = selectChatMessages(global, chatId);
@@ -576,8 +588,8 @@ export default memo(withGlobal(
return state;
}
- if (threadId !== MAIN_THREAD_ID && !chat?.isForum) {
- const pinnedMessageId = threadId;
+ if (threadId !== MAIN_THREAD_ID && !isSavedDialog && !chat?.isForum) {
+ const pinnedMessageId = Number(threadId);
const message = pinnedMessageId ? selectChatMessage(global, chatId, pinnedMessageId) : undefined;
const topMessageSender = message ? selectForwardedSender(global, message) : undefined;
@@ -590,7 +602,7 @@ export default memo(withGlobal(
};
}
- const pinnedMessageIds = selectPinnedIds(global, chatId, threadId);
+ const pinnedMessageIds = !isSavedDialog ? selectPinnedIds(global, chatId, threadId) : undefined;
if (pinnedMessageIds?.length) {
const firstPinnedMessage = messagesById[pinnedMessageIds[0]];
const {
diff --git a/src/components/middle/MobileSearch.tsx b/src/components/middle/MobileSearch.tsx
index 10e7f1bfb..c70fba170 100644
--- a/src/components/middle/MobileSearch.tsx
+++ b/src/components/middle/MobileSearch.tsx
@@ -6,6 +6,7 @@ import React, {
import { getActions, withGlobal } from '../../global';
import type { ApiChat } from '../../api/types';
+import type { ThreadId } from '../../types';
import { requestMutation } from '../../lib/fasterdom/fasterdom';
import {
@@ -32,7 +33,7 @@ export type OwnProps = {
type StateProps = {
isActive?: boolean;
chat?: ApiChat;
- threadId?: number;
+ threadId?: ThreadId;
query?: string;
totalCount?: number;
foundIds?: number[];
diff --git a/src/components/middle/composer/AttachBotItem.tsx b/src/components/middle/composer/AttachBotItem.tsx
index 139ea9d1b..943aa8a05 100644
--- a/src/components/middle/composer/AttachBotItem.tsx
+++ b/src/components/middle/composer/AttachBotItem.tsx
@@ -5,7 +5,7 @@ import React, {
import { getActions } from '../../../global';
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 useLang from '../../../hooks/useLang';
@@ -20,7 +20,7 @@ type OwnProps = {
theme: ISettings['theme'];
isInSideMenu?: true;
chatId?: string;
- threadId?: number;
+ threadId?: ThreadId;
canShowNew?: boolean;
onMenuOpened: VoidFunction;
onMenuClosed: VoidFunction;
diff --git a/src/components/middle/composer/AttachMenu.tsx b/src/components/middle/composer/AttachMenu.tsx
index d9f7bad6d..e77a06bc2 100644
--- a/src/components/middle/composer/AttachMenu.tsx
+++ b/src/components/middle/composer/AttachMenu.tsx
@@ -6,7 +6,7 @@ import React, {
import type { ApiAttachMenuPeerType } from '../../../api/types';
import type { GlobalState } from '../../../global/types';
-import type { ISettings } from '../../../types';
+import type { ISettings, ThreadId } from '../../../types';
import {
CONTENT_TYPES_WITH_PREVIEW, DEBUG_LOG_FILENAME, SUPPORTED_AUDIO_CONTENT_TYPES,
@@ -32,7 +32,7 @@ import './AttachMenu.scss';
export type OwnProps = {
chatId: string;
- threadId?: number;
+ threadId?: ThreadId;
isButtonVisible: boolean;
canAttachMedia: boolean;
canAttachPolls: boolean;
diff --git a/src/components/middle/composer/AttachmentModal.tsx b/src/components/middle/composer/AttachmentModal.tsx
index 1646f1b05..6f607c630 100644
--- a/src/components/middle/composer/AttachmentModal.tsx
+++ b/src/components/middle/composer/AttachmentModal.tsx
@@ -8,6 +8,7 @@ import type {
ApiAttachment, ApiChatMember, ApiSticker,
} from '../../../api/types';
import type { GlobalState } from '../../../global/types';
+import type { ThreadId } from '../../../types';
import type { Signal } from '../../../util/signals';
import {
@@ -59,7 +60,7 @@ import styles from './AttachmentModal.module.scss';
export type OwnProps = {
chatId: string;
- threadId: number;
+ threadId: ThreadId;
attachments: ApiAttachment[];
getHtml: Signal;
canShowCustomSendMenu?: boolean;
diff --git a/src/components/middle/composer/MessageInput.tsx b/src/components/middle/composer/MessageInput.tsx
index 6b8dd4fd8..a2f28a4a1 100644
--- a/src/components/middle/composer/MessageInput.tsx
+++ b/src/components/middle/composer/MessageInput.tsx
@@ -7,7 +7,7 @@ import React, {
import { getActions, withGlobal } from '../../../global';
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 { EDITABLE_INPUT_ID } from '../../../config';
@@ -48,7 +48,7 @@ type OwnProps = {
ref?: RefObject;
id: string;
chatId: string;
- threadId: number;
+ threadId: ThreadId;
isAttachmentModalInput?: boolean;
isStoryInput?: boolean;
customEmojiPrefix: string;
diff --git a/src/components/middle/composer/StickerPicker.tsx b/src/components/middle/composer/StickerPicker.tsx
index 3868ab9e9..a2b27d7d4 100644
--- a/src/components/middle/composer/StickerPicker.tsx
+++ b/src/components/middle/composer/StickerPicker.tsx
@@ -6,7 +6,7 @@ import React, {
import { getActions, withGlobal } from '../../../global';
import type { ApiChat, ApiSticker, ApiStickerSet } from '../../../api/types';
-import type { StickerSetOrReactionsSetOrRecent } from '../../../types';
+import type { StickerSetOrReactionsSetOrRecent, ThreadId } from '../../../types';
import {
CHAT_STICKER_SET_ID,
@@ -48,7 +48,7 @@ import styles from './StickerPicker.module.scss';
type OwnProps = {
chatId: string;
- threadId?: number;
+ threadId?: ThreadId;
className: string;
isHidden?: boolean;
isTranslucent?: boolean;
diff --git a/src/components/middle/composer/StickerTooltip.tsx b/src/components/middle/composer/StickerTooltip.tsx
index 4abb34d51..ad33e591e 100644
--- a/src/components/middle/composer/StickerTooltip.tsx
+++ b/src/components/middle/composer/StickerTooltip.tsx
@@ -3,6 +3,7 @@ import React, { memo, useEffect, useRef } from '../../../lib/teact/teact';
import { withGlobal } from '../../../global';
import type { ApiSticker } from '../../../api/types';
+import type { ThreadId } from '../../../types';
import { STICKER_SIZE_PICKER } from '../../../config';
import { selectIsChatWithSelf, selectIsCurrentUserPremium } from '../../../global/selectors';
@@ -21,7 +22,7 @@ import './StickerTooltip.scss';
export type OwnProps = {
chatId: string;
- threadId?: number;
+ threadId?: ThreadId;
isOpen: boolean;
onStickerSelect: (sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean) => void;
onClose: NoneToVoidFunction;
diff --git a/src/components/middle/composer/SymbolMenu.tsx b/src/components/middle/composer/SymbolMenu.tsx
index f27d60457..a5289a8dc 100644
--- a/src/components/middle/composer/SymbolMenu.tsx
+++ b/src/components/middle/composer/SymbolMenu.tsx
@@ -6,6 +6,7 @@ import { withGlobal } from '../../../global';
import type { ApiSticker, ApiVideo } from '../../../api/types';
import type { GlobalActions } from '../../../global';
+import type { ThreadId } from '../../../types';
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
import { selectIsContextMenuTranslucent, selectTabState } from '../../../global/selectors';
@@ -35,7 +36,7 @@ const STICKERS_TAB_INDEX = 2;
export type OwnProps = {
chatId: string;
- threadId?: number;
+ threadId?: ThreadId;
isOpen: boolean;
canSendStickers?: boolean;
canSendGifs?: boolean;
diff --git a/src/components/middle/composer/SymbolMenuButton.tsx b/src/components/middle/composer/SymbolMenuButton.tsx
index ea316a0a4..0ea7c4d4b 100644
--- a/src/components/middle/composer/SymbolMenuButton.tsx
+++ b/src/components/middle/composer/SymbolMenuButton.tsx
@@ -3,7 +3,7 @@ import React, { memo, useRef, useState } from '../../../lib/teact/teact';
import { getActions } from '../../../global';
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 buildClassName from '../../../util/buildClassName';
@@ -21,7 +21,7 @@ const MOBILE_KEYBOARD_HIDE_DELAY_MS = 100;
type OwnProps = {
chatId: string;
- threadId?: number;
+ threadId?: ThreadId;
isMobile?: boolean;
isReady?: boolean;
isSymbolMenuOpen?: boolean;
diff --git a/src/components/middle/composer/WebPagePreview.tsx b/src/components/middle/composer/WebPagePreview.tsx
index cb0733e2e..2992ae71f 100644
--- a/src/components/middle/composer/WebPagePreview.tsx
+++ b/src/components/middle/composer/WebPagePreview.tsx
@@ -5,7 +5,7 @@ import { getActions, withGlobal } from '../../../global';
import type {
ApiFormattedText, ApiMessage, ApiMessageEntityTextUrl, ApiWebPage,
} from '../../../api/types';
-import type { ISettings } from '../../../types';
+import type { ISettings, ThreadId } from '../../../types';
import type { Signal } from '../../../util/signals';
import { ApiMessageEntityTypes } from '../../../api/types';
@@ -29,7 +29,7 @@ import './WebPagePreview.scss';
type OwnProps = {
chatId: string;
- threadId: number;
+ threadId: ThreadId;
getHtml: Signal;
isDisabled?: boolean;
};
diff --git a/src/components/middle/composer/hooks/useDraft.ts b/src/components/middle/composer/hooks/useDraft.ts
index d314928c9..16cefa97d 100644
--- a/src/components/middle/composer/hooks/useDraft.ts
+++ b/src/components/middle/composer/hooks/useDraft.ts
@@ -3,6 +3,7 @@ import { getActions } from '../../../../global';
import type { ApiMessage } from '../../../../api/types';
import type { ApiDraft } from '../../../../global/types';
+import type { ThreadId } from '../../../../types';
import type { Signal } from '../../../../util/signals';
import { ApiMessageEntityTypes } from '../../../../api/types';
@@ -43,7 +44,7 @@ const useDraft = ({
} : {
draft?: ApiDraft;
chatId: string;
- threadId: number;
+ threadId: ThreadId;
getHtml: Signal;
setHtml: (html: string) => void;
editedMessage?: ApiMessage;
@@ -68,7 +69,7 @@ const useDraft = ({
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;
const html = getHtml();
diff --git a/src/components/middle/composer/hooks/useEditing.ts b/src/components/middle/composer/hooks/useEditing.ts
index 5f1635988..27559ae2c 100644
--- a/src/components/middle/composer/hooks/useEditing.ts
+++ b/src/components/middle/composer/hooks/useEditing.ts
@@ -3,6 +3,7 @@ import { getActions } from '../../../../global';
import type { ApiFormattedText, ApiMessage } from '../../../../api/types';
import type { ApiDraft, MessageListType } from '../../../../global/types';
+import type { ThreadId } from '../../../../types';
import type { Signal } from '../../../../util/signals';
import { ApiMessageEntityTypes } from '../../../../api/types';
@@ -30,7 +31,7 @@ const useEditing = (
resetComposer: (shouldPreserveInput?: boolean) => void,
openDeleteModal: () => void,
chatId: string,
- threadId: number,
+ threadId: ThreadId,
type: MessageListType,
draft?: ApiDraft,
editingDraft?: ApiFormattedText,
diff --git a/src/components/middle/helpers/groupMessages.ts b/src/components/middle/helpers/groupMessages.ts
index 489d28b5c..c017e591c 100644
--- a/src/components/middle/helpers/groupMessages.ts
+++ b/src/components/middle/helpers/groupMessages.ts
@@ -85,7 +85,7 @@ export function groupMessages(
|| (lastSenderGroupItem
&& 'mainMessage' in lastSenderGroupItem && lastSenderGroupItem.mainMessage?.id === topMessageId))
&& nextMessage.id !== topMessageId)
- || (isChatWithSelf && message.forwardInfo?.senderUserId !== nextMessage.forwardInfo?.senderUserId)
+ || (isChatWithSelf && message.forwardInfo?.fromId !== nextMessage.forwardInfo?.fromId)
) {
currentSenderGroup = [];
currentDateGroup.senderGroups.push(currentSenderGroup);
diff --git a/src/components/middle/hooks/usePinnedMessage.ts b/src/components/middle/hooks/usePinnedMessage.ts
index 292810016..4c2a9312b 100644
--- a/src/components/middle/hooks/usePinnedMessage.ts
+++ b/src/components/middle/hooks/usePinnedMessage.ts
@@ -1,6 +1,8 @@
import { useEffect, useRef } from '../../../lib/teact/teact';
import { getGlobal } from '../../../global';
+import type { ThreadId } from '../../../types';
+
import {
selectFocusedMessageId,
selectListedIds,
@@ -24,7 +26,7 @@ type PinnedIntersectionChangedParams = {
export type PinnedIntersectionChangedCallback = (params: PinnedIntersectionChangedParams) => void;
export default function usePinnedMessage(
- chatId?: string, threadId?: number, pinnedIds?: number[], topMessageId?: number,
+ chatId?: string, threadId?: ThreadId, pinnedIds?: number[], topMessageId?: number,
) {
const [getCurrentPinnedIndexes, setCurrentPinnedIndexes] = useSignal>({});
const [getForceNextPinnedInHeader, setForceNextPinnedInHeader] = useSignal();
diff --git a/src/components/middle/message/Message.tsx b/src/components/middle/message/Message.tsx
index 499dccebb..d38f94185 100644
--- a/src/components/middle/message/Message.tsx
+++ b/src/components/middle/message/Message.tsx
@@ -23,7 +23,9 @@ import type {
MessageListType,
} from '../../../global/types';
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 { PinnedIntersectionChangedCallback } from '../hooks/usePinnedMessage';
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 {
areReactionsEmpty,
+ getIsSavedDialog,
getMessageContent,
getMessageCustomShape,
getMessageHtmlId,
@@ -41,6 +44,7 @@ import {
getSenderTitle,
hasMessageText,
hasMessageTtl,
+ isAnonymousForwardsChat,
isAnonymousOwnMessage,
isChatChannel,
isChatGroup,
@@ -191,7 +195,7 @@ type OwnProps =
noAvatars?: boolean;
withAvatar?: boolean;
withSenderName?: boolean;
- threadId: number;
+ threadId: ThreadId;
messageListType: MessageListType;
noComments: boolean;
noReplies: boolean;
@@ -232,6 +236,7 @@ type StateProps = {
isForwarding?: boolean;
isChatWithSelf?: boolean;
isRepliesChat?: boolean;
+ isAnonymousForwards?: boolean;
isChannel?: boolean;
isGroup?: boolean;
canReply?: boolean;
@@ -243,7 +248,7 @@ type StateProps = {
isSelected?: boolean;
isGroupSelected?: boolean;
isDownloading?: boolean;
- threadId?: number;
+ threadId?: ThreadId;
isPinnedList?: boolean;
isPinned?: boolean;
canAutoLoadMedia?: boolean;
@@ -274,6 +279,7 @@ type StateProps = {
isConnected: boolean;
isLoadingComments?: boolean;
shouldWarnAboutSvg?: boolean;
+ isInSavedDialog?: boolean;
};
type MetaPosition =
@@ -347,6 +353,7 @@ const Message: FC = ({
isForwarding,
isChatWithSelf,
isRepliesChat,
+ isAnonymousForwards,
isChannel,
isGroup,
canReply,
@@ -387,6 +394,7 @@ const Message: FC = ({
isConnected,
getIsMessageListReady,
shouldWarnAboutSvg,
+ isInSavedDialog,
onPinnedIntersectionChange,
}) => {
const {
@@ -483,6 +491,7 @@ const Message: FC = ({
forwardInfo
&& (!isChatWithSelf || isScheduled)
&& !isRepliesChat
+ && !isAnonymousForwards
&& !forwardInfo.isLinkedChannelPost
&& !isCustomShape
) || Boolean(message.content.storyData && !message.content.storyData.isMention);
@@ -500,7 +509,7 @@ const Message: FC = ({
const canForward = isChannel && !isScheduled && message.isForwardingAllowed && !isChatProtected;
const canFocus = Boolean(isPinnedList
|| (forwardInfo
- && (forwardInfo.isChannelPost || (isChatWithSelf && !isOwn) || isRepliesChat)
+ && (forwardInfo.isChannelPost || (isChatWithSelf && !isOwn) || isRepliesChat || isAnonymousForwards)
&& forwardInfo.fromMessageId
));
@@ -520,7 +529,8 @@ const Message: FC = ({
const messageSender = canShowSender ? sender : undefined;
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 messageColorPeer = originSender || sender;
const senderPeer = (forwardInfo || message.content.storyData) ? originSender : messageSender;
@@ -914,6 +924,7 @@ const Message: FC = ({
(
const chat = selectChat(global, chatId);
const isChatWithSelf = selectIsChatWithSelf(global, chatId);
const isRepliesChat = isChatWithRepliesBot(chatId);
+ const isAnonymousForwards = isAnonymousForwardsChat(chatId);
const isChannel = chat && isChatChannel(chat);
const isGroup = chat && isChatGroup(chat);
const chatFullInfo = !isUserId(chatId) ? selectChatFullInfo(global, chatId) : undefined;
@@ -1487,11 +1499,12 @@ export default memo(withGlobal(
const shouldHideReply = replyToMsgId && replyToMsgId === threadId;
const replyMessage = replyToMsgId ? selectChatMessage(global, replyToPeerId || chatId, replyToMsgId) : undefined;
const forwardHeader = forwardInfo || replyFrom;
- const replyMessageSender = replyMessage ? selectReplySender(global, replyMessage) : forwardHeader && !isRepliesChat
- ? selectSenderFromHeader(global, forwardHeader) : undefined;
+ const replyMessageSender = replyMessage ? selectReplySender(global, replyMessage)
+ : forwardHeader && !isRepliesChat && !isAnonymousForwards
+ ? selectSenderFromHeader(global, forwardHeader) : undefined;
const replyMessageForwardSender = replyMessage && selectForwardedSender(global, replyMessage);
const replyMessageChat = replyToPeerId ? selectChat(global, replyToPeerId) : undefined;
- const isReplyPrivate = !isRepliesChat && replyMessageChat && !isChatPublic(replyMessageChat)
+ const isReplyPrivate = !isRepliesChat && !isAnonymousForwards && replyMessageChat && !isChatPublic(replyMessageChat)
&& (replyMessageChat.isNotJoined || replyMessageChat.isRestricted);
const isReplyToTopicStart = replyMessage?.content.action?.type === 'topicCreate';
const replyStory = storyReplyId && storyReplyUserId
@@ -1554,6 +1567,8 @@ export default memo(withGlobal(
const hasActiveReactions = Boolean(reactionMessage && activeReactions[getMessageKey(reactionMessage)]?.length);
+ const isInSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
+
return {
theme: selectTheme(global),
forceSenderName,
@@ -1578,6 +1593,7 @@ export default memo(withGlobal(
reactionMessage,
isChatWithSelf,
isRepliesChat,
+ isAnonymousForwards,
isChannel,
isGroup,
canReply,
@@ -1621,6 +1637,7 @@ export default memo(withGlobal(
withStickerEffects: selectPerformanceSettingsValue(global, 'stickerEffects'),
webPageStory,
isConnected,
+ isInSavedDialog,
isLoadingComments: repliesThreadInfo?.isCommentsInfo
&& loadingThread?.loadingChatId === repliesThreadInfo?.originChannelId
&& loadingThread?.loadingMessageId === repliesThreadInfo?.originMessageId,
diff --git a/src/components/middle/message/MessageMeta.tsx b/src/components/middle/message/MessageMeta.tsx
index 3ed490ca3..d45c06aa1 100644
--- a/src/components/middle/message/MessageMeta.tsx
+++ b/src/components/middle/message/MessageMeta.tsx
@@ -29,6 +29,7 @@ type OwnProps = {
repliesThreadInfo?: ApiThreadInfo;
isTranslated?: boolean;
isPinned?: boolean;
+ isInSavedDialog?: boolean;
onClick: (e: React.MouseEvent) => void;
onTranslationClick: (e: React.MouseEvent) => void;
renderQuickReactionButton?: () => TeactNode | undefined;
@@ -45,6 +46,7 @@ const MessageMeta: FC = ({
noReplies,
isTranslated,
isPinned,
+ isInSavedDialog,
onClick,
onTranslationClick,
onOpenThread,
@@ -72,7 +74,12 @@ const MessageMeta: FC = ({
const editDateTime = message.isEdited
&& formatDateTimeToString(message.editDate! * 1000, lang.code, undefined, lang.timeFormat);
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;
if (editDateTime) {
@@ -137,7 +144,9 @@ const MessageMeta: FC = ({
>
)}
{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)}
{outgoingStatus && (
diff --git a/src/components/middle/message/hooks/useInnerHandlers.ts b/src/components/middle/message/hooks/useInnerHandlers.ts
index 3cead036e..3440c25ca 100644
--- a/src/components/middle/message/hooks/useInnerHandlers.ts
+++ b/src/components/middle/message/hooks/useInnerHandlers.ts
@@ -5,7 +5,7 @@ import type {
ApiMessage, ApiPeer, ApiStory, ApiTopic, ApiUser,
} from '../../../../api/types';
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 { MediaViewerOrigin } from '../../../../types';
@@ -18,7 +18,7 @@ export default function useInnerHandlers(
selectMessage: (e: React.MouseEvent, groupedId?: string) => void,
message: ApiMessage,
chatId: string,
- threadId: number,
+ threadId: ThreadId,
isInDocumentGroup: boolean,
asForwarded?: boolean,
isScheduled?: boolean,
diff --git a/src/components/right/AddChatMembers.tsx b/src/components/right/AddChatMembers.tsx
index ee01a7c73..a210497c9 100644
--- a/src/components/right/AddChatMembers.tsx
+++ b/src/components/right/AddChatMembers.tsx
@@ -5,15 +5,16 @@ import React, {
import { getActions, getGlobal, withGlobal } from '../../global';
import type {
- ApiChat, ApiChatMember,
+ ApiChatMember,
} from '../../api/types';
import { NewChatMembersProgress } from '../../types';
import {
- filterUsersByName, isChatChannel, isUserBot, sortChatIds,
+ filterUsersByName, isChatChannel, isUserBot,
} from '../../global/helpers';
import { selectChat, selectChatFullInfo, selectTabState } from '../../global/selectors';
import { unique } from '../../util/iteratees';
+import sortChatIds from '../common/helpers/sortChatIds';
import useHistoryBack from '../../hooks/useHistoryBack';
import useLang from '../../hooks/useLang';
@@ -36,7 +37,6 @@ type StateProps = {
isChannel?: boolean;
members?: ApiChatMember[];
currentUserId?: string;
- chatsById: Record;
localContactIds?: string[];
searchQuery?: string;
isLoading: boolean;
@@ -50,7 +50,6 @@ const AddChatMembers: FC = ({
members,
onNextStep,
currentUserId,
- chatsById,
localContactIds,
isLoading,
searchQuery,
@@ -104,11 +103,8 @@ const AddChatMembers: FC = ({
&& (!user || !isUserBot(user) || (!isChannel && user.canBeInvitedToGroup))
);
}),
- chatsById,
);
- }, [
- localContactIds, chatsById, searchQuery, localUserIds, globalUserIds, currentUserId, memberIds, isChannel,
- ]);
+ }, [localContactIds, searchQuery, localUserIds, globalUserIds, currentUserId, memberIds, isChannel]);
const handleNextStep = useCallback(() => {
if (selectedMemberIds.length) {
@@ -154,7 +150,6 @@ export default memo(withGlobal(
(global, { chatId }): StateProps => {
const chat = selectChat(global, chatId);
const { userIds: localContactIds } = global.contactList || {};
- const { byId: chatsById } = global.chats;
const { newChatMembersProgress } = selectTabState(global);
const { currentUserId } = global;
const isChannel = chat && isChatChannel(chat);
@@ -170,7 +165,6 @@ export default memo(withGlobal(
isChannel,
members: selectChatFullInfo(global, chatId)?.members,
currentUserId,
- chatsById,
localContactIds,
searchQuery,
isSearching: fetchingStatus,
diff --git a/src/components/right/Profile.scss b/src/components/right/Profile.scss
index dc3738a15..62619fd44 100644
--- a/src/components/right/Profile.scss
+++ b/src/components/right/Profile.scss
@@ -63,6 +63,10 @@
flex: 1;
}
+ .saved-dialogs {
+ height: 100% !important;
+ }
+
.content {
&.empty-list {
height: 100%;
@@ -158,6 +162,6 @@
margin-top: 1rem;
font-size: 0.8125rem;
}
- }
+ }
}
}
diff --git a/src/components/right/Profile.tsx b/src/components/right/Profile.tsx
index 8171310c6..34698913b 100644
--- a/src/components/right/Profile.tsx
+++ b/src/components/right/Profile.tsx
@@ -14,7 +14,7 @@ import type {
ApiUserStatus,
} from '../../api/types';
import type {
- ISettings, ProfileState, ProfileTabType, SharedMediaType,
+ ISettings, ProfileState, ProfileTabType, SharedMediaType, ThreadId,
} from '../../types';
import { MAIN_THREAD_ID } from '../../api/types';
import { AudioOrigin, MediaViewerOrigin, NewChatMembersProgress } from '../../types';
@@ -26,7 +26,7 @@ import {
SLIDE_TRANSITION_DURATION,
} from '../../config';
import {
- getHasAdminRight, isChatAdmin, isChatChannel, isChatGroup, isUserBot, isUserId, isUserRightBanned,
+ getHasAdminRight, getIsSavedDialog, isChatAdmin, isChatChannel, isChatGroup, isUserBot, isUserId, isUserRightBanned,
} from '../../global/helpers';
import {
selectActiveDownloads,
@@ -70,6 +70,7 @@ import NothingFound from '../common/NothingFound';
import PrivateChatInfo from '../common/PrivateChatInfo';
import ProfileInfo from '../common/ProfileInfo';
import WebLink from '../common/WebLink';
+import ChatList from '../left/main/ChatList';
import MediaStory from '../story/MediaStory';
import Button from '../ui/Button';
import FloatingActionButton from '../ui/FloatingActionButton';
@@ -84,7 +85,7 @@ import './Profile.scss';
type OwnProps = {
chatId: string;
- topicId?: number;
+ threadId?: ThreadId;
profileState: ProfileState;
isMobile?: boolean;
onProfileStateChange: (state: ProfileState) => void;
@@ -122,9 +123,16 @@ type StateProps = {
similarChannels?: string[];
isCurrentUserPremium?: boolean;
limitSimilarChannels: number;
+ isTopicInfo?: boolean;
+ isSavedDialog?: boolean;
};
-const TABS = [
+type TabProps = {
+ type: ProfileTabType;
+ title: string;
+};
+
+const TABS: TabProps[] = [
{ type: 'media', title: 'SharedMediaTab2' },
{ type: 'documents', title: 'SharedFilesTab2' },
{ type: 'links', title: 'SharedLinksTab2' },
@@ -136,7 +144,7 @@ const INTERSECTION_THROTTLE = 500;
const Profile: FC = ({
chatId,
- topicId,
+ threadId,
profileState,
onProfileStateChange,
theme,
@@ -170,6 +178,8 @@ const Profile: FC = ({
similarChannels,
isCurrentUserPremium,
limitSimilarChannels,
+ isTopicInfo,
+ isSavedDialog,
}) => {
const {
setLocalMediaSearchType,
@@ -195,29 +205,33 @@ const Profile: FC = ({
const lang = useLang();
const [deletingUserId, setDeletingUserId] = useState();
+ const profileId = isSavedDialog ? String(threadId) : (resolvedUserId || chatId);
+ const isSavedMessages = profileId === currentUserId && !isSavedDialog;
+
const tabs = useMemo(() => ([
- ...(hasStoriesTab ? [{ type: 'stories', title: 'ProfileStories' }] : []),
- ...(hasStoriesTab && currentUserId === chatId ? [{ type: 'storiesArchive', title: 'ProfileStoriesArchive' }] : []),
+ ...(isSavedMessages && !isSavedDialog ? [{ type: 'dialogs' as const, title: 'SavedDialogsTab' }] : []),
+ ...(hasStoriesTab ? [{ type: 'stories' as const, title: 'ProfileStories' }] : []),
+ ...(hasStoriesTab && isSavedMessages ? [{ type: 'storiesArchive' as const, title: 'ProfileStoriesArchive' }] : []),
...(hasMembersTab ? [{
- type: 'members', title: isChannel ? 'ChannelSubscribers' : 'GroupMembers',
+ type: 'members' as const, title: isChannel ? 'ChannelSubscribers' : 'GroupMembers',
}] : []),
...TABS,
// TODO The filter for voice messages currently does not work
// in forum topics. Return it when it's fixed on the server side.
- ...(!topicId ? [{ type: 'voice', title: 'SharedVoiceTab2' }] : []),
- ...(hasCommonChatsTab ? [{ type: 'commonChats', title: 'SharedGroupsTab2' }] : []),
+ ...(!isTopicInfo ? [{ type: 'voice' as const, title: 'SharedVoiceTab2' }] : []),
+ ...(hasCommonChatsTab ? [{ type: 'commonChats' as const, title: 'SharedGroupsTab2' }] : []),
...(isChannel && similarChannels?.length
- ? [{ type: 'similarChannels', title: 'SimilarChannelsTab' }]
+ ? [{ type: 'similarChannels' as const, title: 'SimilarChannelsTab' }]
: []),
]), [
- chatId,
- currentUserId,
hasCommonChatsTab,
hasMembersTab,
hasStoriesTab,
isChannel,
- topicId,
+ isTopicInfo,
similarChannels,
+ isSavedMessages,
+ isSavedDialog,
]);
const initialTab = useMemo(() => {
@@ -269,12 +283,13 @@ const Profile: FC = ({
chatsById,
messagesById,
foundIds,
- topicId,
+ threadId,
storyIds,
archiveStoryIds,
similarChannels,
);
- const isFirstTab = (hasStoriesTab && resultType === 'stories')
+ const isFirstTab = (isSavedMessages && resultType === 'dialogs')
+ || (hasStoriesTab && resultType === 'stories')
|| resultType === 'members'
|| (!hasMembersTab && resultType === 'media');
const activeKey = tabs.findIndex(({ type }) => type === resultType);
@@ -304,9 +319,7 @@ const Profile: FC = ({
// Update search type when switching tabs or forum topics
useEffect(() => {
setLocalMediaSearchType({ mediaType: tabType as SharedMediaType });
- }, [setLocalMediaSearchType, tabType, topicId]);
-
- const profileId = resolvedUserId || chatId;
+ }, [setLocalMediaSearchType, tabType, threadId]);
useEffect(() => {
loadProfilePhotos({ profileId });
@@ -376,7 +389,7 @@ const Profile: FC = ({
} else if (!viewportIds) {
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 {
return memberId === currentUserId || !canDeleteMembers ? undefined : [{
@@ -389,6 +402,12 @@ const Profile: FC = ({
}
function renderContent() {
+ if (resultType === 'dialogs') {
+ return (
+
+ );
+ }
+
if (!viewportIds || !canRenderContent || !messagesById) {
const noSpinner = isFirstTab && !canRenderContent;
const forceRenderHiddenMembers = Boolean(resultType === 'members' && areMembersHidden);
@@ -594,7 +613,9 @@ const Profile: FC = ({
onLoadMore={getMore}
onScroll={handleScroll}
>
- {!noProfileInfo && renderProfileInfo(chatId, resolvedUserId, isRightColumnShown && canRenderContent)}
+ {!noProfileInfo && !isSavedMessages && (
+ renderProfileInfo(profileId, isRightColumnShown && canRenderContent, isSavedDialog)
+ )}
{!isRestricted && (
= ({
);
};
-function renderProfileInfo(chatId: string, resolvedUserId: string | undefined, isReady: boolean) {
+function renderProfileInfo(profileId: string, isReady: boolean, isSavedDialog?: boolean) {
return (
);
}
export default memo(withGlobal
(
- (global, { chatId, topicId, isMobile }): StateProps => {
+ (global, {
+ chatId, threadId, isMobile,
+ }): StateProps => {
const chat = selectChat(global, chatId);
const chatFullInfo = selectChatFullInfo(global, chatId);
const messagesById = selectChatMessages(global, chatId);
const { currentType: mediaSearchType, resultsByType } = selectCurrentMediaSearch(global) || {};
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: chatsById } = global.chats;
+ const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
+
const isGroup = chat && isChatGroup(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 adminMembersById = chatFullInfo?.adminMembersById;
const areMembersHidden = hasMembersTab && chat
@@ -675,12 +702,13 @@ export default memo(withGlobal(
if (isUserId(chatId)) {
resolvedUserId = chatId;
user = selectUser(global, resolvedUserId);
- hasCommonChatsTab = user && !user.isSelf && !isUserBot(user);
+ hasCommonChatsTab = user && !user.isSelf && !isUserBot(user) && !isSavedDialog;
}
const peer = user || chat;
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 storyIds = peerStories?.pinnedIds;
const storyByIds = peerStories?.byId;
@@ -714,6 +742,8 @@ export default memo(withGlobal(
shouldWarnAboutSvg: global.settings.byKey.shouldWarnAboutSvg,
similarChannels,
isCurrentUserPremium,
+ isTopicInfo,
+ isSavedDialog,
limitSimilarChannels: selectPremiumLimit(global, 'recommendedChannels'),
...(hasMembersTab && members && { members, adminMembersById }),
...(hasCommonChatsTab && user && { commonChatIds: user.commonChats?.ids }),
diff --git a/src/components/right/RightColumn.tsx b/src/components/right/RightColumn.tsx
index 79d6c2fbe..204de998f 100644
--- a/src/components/right/RightColumn.tsx
+++ b/src/components/right/RightColumn.tsx
@@ -2,15 +2,19 @@ import type { FC } from '../../lib/teact/teact';
import React, { memo, useEffect, useState } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global';
-import type { ProfileTabType } from '../../types';
-import { MAIN_THREAD_ID } from '../../api/types';
+import type { ProfileTabType, ThreadId } from '../../types';
import {
ManagementScreens, NewChatMembersProgress, ProfileState, RightColumnContent,
} from '../../types';
import { ANIMATION_END_DELAY, MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN } from '../../config';
+import { getIsSavedDialog } from '../../global/helpers';
import {
- selectAreActiveChatsLoaded, selectChat, selectCurrentMessageList, selectRightColumnContentKey, selectTabState,
+ selectAreActiveChatsLoaded,
+ selectCurrentMessageList,
+ selectIsChatWithSelf,
+ selectRightColumnContentKey,
+ selectTabState,
} from '../../global/selectors';
import captureEscKeyListener from '../../util/captureEscKeyListener';
@@ -45,12 +49,14 @@ interface OwnProps {
type StateProps = {
contentKey?: RightColumnContent;
chatId?: string;
- threadId?: number;
+ threadId?: ThreadId;
isInsideTopic?: boolean;
isChatSelected: boolean;
shouldSkipHistoryAnimations?: boolean;
nextManagementScreen?: ManagementScreens;
nextProfileTab?: ProfileTabType;
+ isSavedMessages?: boolean;
+ isSavedDialog?: boolean;
};
const ANIMATION_DURATION = 450 + ANIMATION_END_DELAY;
@@ -69,11 +75,12 @@ const RightColumn: FC = ({
chatId,
threadId,
isMobile,
- isInsideTopic,
isChatSelected,
shouldSkipHistoryAnimations,
nextManagementScreen,
nextProfileTab,
+ isSavedMessages,
+ isSavedDialog,
}) => {
const {
toggleChatInfo,
@@ -97,7 +104,9 @@ const RightColumn: FC = ({
} = getActions();
const { width: windowWidth } = useWindowSize();
- const [profileState, setProfileState] = useState(ProfileState.Profile);
+ const [profileState, setProfileState] = useState(
+ isSavedMessages && !isSavedDialog ? ProfileState.SavedDialogs : ProfileState.Profile,
+ );
const [managementScreen, setManagementScreen] = useState(ManagementScreens.Initial);
const [selectedChatMemberId, setSelectedChatMemberId] = useState();
const [isPromotedByCurrentUser, setIsPromotedByCurrentUser] = useState();
@@ -129,7 +138,7 @@ const RightColumn: FC = ({
setNewChatMembersDialogState({ newChatMembersProgress: NewChatMembersProgress.Closed });
break;
case RightColumnContent.ChatInfo:
- if (isScrolledDown && shouldScrollUp) {
+ if (isScrolledDown && shouldScrollUp && !isSavedMessages) {
setProfileState(ProfileState.Profile);
break;
}
@@ -253,12 +262,14 @@ const RightColumn: FC = ({
}, [isOverlaying]);
// We need to clear profile state and management screen state, when changing chats
- useLayoutEffectWithPrevDeps(([prevChatId]) => {
- if (prevChatId !== chatId) {
- setProfileState(ProfileState.Profile);
+ useLayoutEffectWithPrevDeps(([prevChatId, prevThreadId]) => {
+ if (prevChatId !== chatId || prevThreadId !== threadId) {
+ setProfileState(
+ isSavedMessages && !isSavedDialog ? ProfileState.SavedDialogs : ProfileState.Profile,
+ );
setManagementScreen(ManagementScreens.Initial);
}
- }, [chatId]);
+ }, [chatId, threadId, isSavedDialog, isSavedMessages]);
useHistoryBack({
isActive: isChatSelected && (
@@ -289,9 +300,9 @@ const RightColumn: FC = ({
case RightColumnContent.ChatInfo:
return (
(
const areActiveChatsLoaded = selectAreActiveChatsLoaded(global);
const { management, shouldSkipHistoryAnimations, nextProfileTab } = selectTabState(global);
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 {
contentKey: selectRightColumnContentKey(global, isMobile),
chatId,
threadId,
- isInsideTopic,
isChatSelected: Boolean(chatId && areActiveChatsLoaded),
shouldSkipHistoryAnimations,
nextManagementScreen,
nextProfileTab,
+ isSavedMessages,
+ isSavedDialog,
};
},
)(RightColumn));
diff --git a/src/components/right/RightHeader.scss b/src/components/right/RightHeader.scss
index b849f66fc..77cbc27b7 100644
--- a/src/components/right/RightHeader.scss
+++ b/src/components/right/RightHeader.scss
@@ -23,13 +23,27 @@
}
}
- h3 {
+ .title {
margin-bottom: 0;
font-size: 1.25rem;
font-weight: 500;
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 {
display: flex;
margin-left: auto;
diff --git a/src/components/right/RightHeader.tsx b/src/components/right/RightHeader.tsx
index bd6e50b26..2c6fcaf2e 100644
--- a/src/components/right/RightHeader.tsx
+++ b/src/components/right/RightHeader.tsx
@@ -4,9 +4,9 @@ import { getActions, withGlobal } from '../../global';
import type { ApiExportedInvite } 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 {
getCanAddContact, getCanManageTopic, isChatChannel, isUserBot, isUserId,
} from '../../global/helpers';
@@ -17,6 +17,7 @@ import {
selectCurrentGifSearch,
selectCurrentStickerSearch,
selectCurrentTextSearch,
+ selectIsChatWithSelf,
selectTabState,
selectUser,
} from '../../global/selectors';
@@ -28,6 +29,7 @@ import useAppLayout from '../../hooks/useAppLayout';
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
import useElectronDrag from '../../hooks/useElectronDrag';
import useFlag from '../../hooks/useFlag';
+import { useFolderManagerForChatsCount } from '../../hooks/useFolderManager';
import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback';
@@ -41,7 +43,7 @@ import './RightHeader.scss';
type OwnProps = {
chatId?: string;
- threadId?: number;
+ threadId?: ThreadId;
isColumnOpen?: boolean;
isProfile?: boolean;
isSearch?: boolean;
@@ -58,7 +60,7 @@ type OwnProps = {
isAddingChatMembers?: boolean;
profileState?: ProfileState;
managementScreen?: ManagementScreens;
- onClose: () => void;
+ onClose: (shouldScrollUp?: boolean) => void;
onScreenSelect: (screen: ManagementScreens) => void;
};
@@ -79,6 +81,7 @@ type StateProps = {
canEditBot?: boolean;
isInsideTopic?: boolean;
canEditTopic?: boolean;
+ isSavedMessages?: boolean;
};
const COLUMN_ANIMATION_DURATION = 450 + ANIMATION_END_DELAY;
@@ -121,6 +124,7 @@ enum HeaderContent {
ManageJoinRequests,
CreateTopic,
EditTopic,
+ SavedDialogs,
}
const RightHeader: FC = ({
@@ -157,6 +161,7 @@ const RightHeader: FC = ({
isBot,
isInsideTopic,
canEditTopic,
+ isSavedMessages,
onClose,
onScreenSelect,
canEditBot,
@@ -178,6 +183,8 @@ const RightHeader: FC = ({
const [isDeleteDialogOpen, openDeleteDialog, closeDeleteDialog] = useFlag();
const { isMobile } = useAppLayout();
+ const foldersChatCount = useFolderManagerForChatsCount();
+
const handleEditInviteClick = useLastCallback(() => {
setEditingExportedInvite({ chatId: chatId!, invite: currentInviteInfo! });
onScreenSelect(ManagementScreens.EditInvite);
@@ -211,7 +218,7 @@ const RightHeader: FC = ({
const toggleEditTopic = useLastCallback(() => {
if (!chatId || !threadId) return;
- openEditTopicPanel({ chatId, topicId: threadId });
+ openEditTopicPanel({ chatId, topicId: Number(threadId) });
});
const handleToggleManagement = useLastCallback(() => {
@@ -222,6 +229,10 @@ const RightHeader: FC = ({
toggleStatistics();
});
+ const handleClose = useLastCallback(() => {
+ onClose(!isSavedMessages);
+ });
+
const [shouldSkipTransition, setShouldSkipTransition] = useState(!isColumnOpen);
useEffect(() => {
@@ -240,6 +251,8 @@ const RightHeader: FC = ({
HeaderContent.MemberList
) : profileState === ProfileState.StoryList ? (
HeaderContent.StoryList
+ ) : profileState === ProfileState.SavedDialogs ? (
+ HeaderContent.SavedDialogs
) : -1 // Never reached
) : isSearch ? (
HeaderContent.Search
@@ -310,6 +323,10 @@ const RightHeader: FC = ({
const renderingContentKey = useCurrentOrPrev(contentKey, true) ?? -1;
function getHeaderTitle() {
+ if (isSavedMessages) {
+ return lang('SavedMessages');
+ }
+
if (isInsideTopic) {
return lang('AccDescrTopic');
}
@@ -332,7 +349,7 @@ const RightHeader: FC = ({
switch (renderingContentKey) {
case HeaderContent.PollResults:
- return {lang('PollResults')}
;
+ return {lang('PollResults')}
;
case HeaderContent.Search:
return (
<>
@@ -354,39 +371,39 @@ const RightHeader: FC = ({
>
);
case HeaderContent.AddingMembers:
- return {lang(isChannel ? 'ChannelAddSubscribers' : 'GroupAddMembers')}
;
+ return {lang(isChannel ? 'ChannelAddSubscribers' : 'GroupAddMembers')}
;
case HeaderContent.ManageInitial:
- return {lang('Edit')}
;
+ return {lang('Edit')}
;
case HeaderContent.ManageChatPrivacyType:
- return {lang(isChannel ? 'ChannelTypeHeader' : 'GroupTypeHeader')}
;
+ return {lang(isChannel ? 'ChannelTypeHeader' : 'GroupTypeHeader')}
;
case HeaderContent.ManageDiscussion:
- return {lang('Discussion')}
;
+ return {lang('Discussion')}
;
case HeaderContent.ManageChatAdministrators:
- return {lang('ChannelAdministrators')}
;
+ return {lang('ChannelAdministrators')}
;
case HeaderContent.ManageGroupRecentActions:
- return {lang('Group.Info.AdminLog')}
;
+ return {lang('Group.Info.AdminLog')}
;
case HeaderContent.ManageGroupAdminRights:
- return {lang('EditAdminRights')}
;
+ return {lang('EditAdminRights')}
;
case HeaderContent.ManageGroupNewAdminRights:
- return {lang('SetAsAdmin')}
;
+ return {lang('SetAsAdmin')}
;
case HeaderContent.ManageGroupPermissions:
- return {lang('ChannelPermissions')}
;
+ return {lang('ChannelPermissions')}
;
case HeaderContent.ManageGroupRemovedUsers:
- return {lang('BlockedUsers')}
;
+ return {lang('BlockedUsers')}
;
case HeaderContent.ManageChannelRemovedUsers:
- return {lang('ChannelBlockedUsers')}
;
+ return {lang('ChannelBlockedUsers')}
;
case HeaderContent.ManageGroupUserPermissionsCreate:
- return {lang('ChannelAddException')}
;
+ return {lang('ChannelAddException')}
;
case HeaderContent.ManageGroupUserPermissions:
- return {lang('UserRestrictions')}
;
+ return {lang('UserRestrictions')}
;
case HeaderContent.ManageInvites:
- return {lang('lng_group_invite_title')}
;
+ return {lang('lng_group_invite_title')}
;
case HeaderContent.ManageEditInvite:
- return {isEditingInvite ? lang('EditLink') : lang('NewLink')}
;
+ return {isEditingInvite ? lang('EditLink') : lang('NewLink')}
;
case HeaderContent.ManageInviteInfo:
return (
<>
- {lang('InviteLink')}
+ {lang('InviteLink')}
{currentInviteInfo && !currentInviteInfo.isRevoked && (