Introduce Saved dialogs (#4177)

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

View File

@ -1,7 +1,9 @@
export class ChatAbortController extends AbortController {
private threads = new Map<number, AbortController>();
import type { ThreadId } from '../../types';
public getThreadSignal(threadId: number): AbortSignal {
export class ChatAbortController extends AbortController {
private threads = new Map<ThreadId, AbortController>();
public getThreadSignal(threadId: ThreadId): AbortSignal {
let controller = this.threads.get(threadId);
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);
}

View File

@ -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<LimitKey, number>;
@ -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,

View File

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

View File

@ -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 }),

View File

@ -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<string, ApiUserStatus>;
draftsById: Record<string, ApiDraft>;
orderedPinnedIds: string[] | undefined;
totalChatCount: number;
messages: ApiMessage[];
lastMessageByChatId: Record<string, number>;
};
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<ChatListData | undefined> {
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<string, number> = {};
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<ChatListData | undefined> {
const result = await invokeRequest(new GramJs.messages.GetSavedDialogs({
offsetPeer: new GramJs.InputPeerEmpty(),
limit,
offsetDate,
...(withPinned && { excludePinned: true }),
}));
const resultPinned = withPinned
? await invokeRequest(new GramJs.messages.GetPinnedSavedDialogs())
: undefined;
if (!result || result instanceof GramJs.messages.SavedDialogsNotModified) {
return undefined;
}
const hasPinned = resultPinned && !(resultPinned instanceof GramJs.messages.SavedDialogsNotModified);
if (hasPinned) {
updateLocalDb(resultPinned);
}
updateLocalDb(result);
dispatchThreadInfoUpdates(result.messages);
const messages = (hasPinned ? resultPinned.messages : [])
.concat(result.messages)
.map(buildApiMessage)
.filter(Boolean);
const peersByKey = preparePeers(result);
if (hasPinned) {
Object.assign(peersByKey, preparePeers(resultPinned, peersByKey));
}
const dialogs = (hasPinned ? resultPinned.dialogs : []).concat(result.dialogs);
const chatIds: string[] = [];
const orderedPinnedIds: string[] = [];
const lastMessageByChatId: Record<string, number> = {};
const chats: ApiChat[] = [];
dialogs.forEach((dialog) => {
const peerEntity = peersByKey[getPeerKey(dialog.peer)];
const chat = buildApiChatFromSavedDialog(dialog, peerEntity);
const chatId = getApiChatIdFromMtpPeer(dialog.peer);
chatIds.push(chatId);
if (withPinned && dialog.pinned) {
orderedPinnedIds.push(chatId);
}
lastMessageByChatId[chatId] = dialog.topMessage;
chats.push(chat);
});
const { users, userStatusesById } = buildApiUsersAndStatuses((hasPinned ? resultPinned.users : [])
.concat(result.users));
let totalChatCount: number;
if (result instanceof GramJs.messages.SavedDialogsSlice) {
totalChatCount = result.count;
} else {
totalChatCount = chatIds.length;
}
return {
chatIds,
chats,
users,
userStatusesById,
orderedPinnedIds: withPinned ? orderedPinnedIds : undefined,
totalChatCount,
lastMessageByChatId,
messages,
draftsById: {},
};
}
@ -348,10 +448,7 @@ export async function requestChatUpdate({
? lastLocalMessage
: 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<string, GramJs.TypeChat | GramJs.TypeUser>,
) {
const store: Record<string, GramJs.TypeChat | GramJs.TypeUser> = {};
@ -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

View File

@ -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) {

View File

@ -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 {

View File

@ -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,

View File

@ -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',

View File

@ -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;

View File

@ -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<number, ApiTopic>;
topics?: Record<ThreadId, ApiTopic>;
listedTopicIds?: number[];
topicsCount?: number;
orderedPinnedTopicIds?: number[];

View File

@ -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;

View File

@ -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;

View File

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

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

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

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

@ -1,6 +1,6 @@
import type { MouseEvent as ReactMouseEvent } from 'react';
import type { 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<OwnProps> = ({
photo,
text,
isSavedMessages,
isSavedDialog,
withVideo,
withStory,
forPremiumPromo,
@ -94,6 +98,7 @@ const Avatar: FC<OwnProps> = ({
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<OwnProps> = ({
}
}
const specialIcon = useMemo(() => {
if (isSavedMessages) {
return isSavedDialog ? 'my-notes' : 'avatar-saved-messages';
}
if (isDeleted) {
return 'avatar-deleted-account';
}
if (isReplies) {
return 'reply-filled';
}
if (isAnonymousForwards) {
return 'author-hidden';
}
return undefined;
}, [isAnonymousForwards, isDeleted, isSavedDialog, isReplies, isSavedMessages]);
const imgBlobUrl = useMedia(imageHash, false, ApiMediaFormat.BlobUrl);
const videoBlobUrl = useMedia(videoHash, !shouldLoadVideo, ApiMediaFormat.BlobUrl);
const hasBlobUrl = Boolean(imgBlobUrl || videoBlobUrl);
@ -137,40 +162,13 @@ const Avatar: FC<OwnProps> = ({
let content: TeactNode | undefined;
const author = user ? getUserFullName(user) : (chat ? getChatTitle(lang, chat) : text);
if (isSavedMessages) {
if (specialIcon) {
content = (
<i
className={buildClassName(
cn.icon,
'icon',
'icon-avatar-saved-messages',
)}
<Icon
name={specialIcon}
className={cn.icon}
role="img"
aria-label={author}
/>
);
} else if (isDeleted) {
content = (
<i
className={buildClassName(
cn.icon,
'icon',
'icon-avatar-deleted-account',
)}
role="img"
aria-label={author}
/>
);
} else if (isReplies) {
content = (
<i
className={buildClassName(
cn.icon,
'icon',
'icon-reply-filled',
)}
role="img"
aria-label={author}
ariaLabel={author}
/>
);
} else if (hasBlobUrl) {
@ -214,6 +212,7 @@ const Avatar: FC<OwnProps> = ({
className,
getPeerColorClass(peer),
isSavedMessages && 'saved-messages',
isAnonymousForwards && 'anonymous-forwards',
isDeleted && 'deleted-account',
isReplies && 'replies-bot-account',
isForum && 'forum',

View File

@ -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<OwnProps & StateProps> = ({
chatOrUserId,
user,
chat,
forceShowSelf,
@ -72,6 +75,7 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
description,
chatInviteLink,
topicLink,
hasSavedMessages,
}) => {
const {
loadFullUser,
@ -79,6 +83,7 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
updateChatMutedState,
updateTopicMutedState,
loadPeerStories,
openSavedDialog,
} = getActions();
const {
@ -151,6 +156,10 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
});
});
const handleOpenSavedDialog = useLastCallback(() => {
openSavedDialog({ chatId: chatOrUserId });
});
if (!chat || chat.isRestricted || (isSelf && !forceShowSelf)) {
return undefined;
}
@ -264,12 +273,17 @@ const ChatExtra: FC<OwnProps & StateProps> = ({
/>
</ListItem>
)}
{hasSavedMessages && (
<ListItem icon="saved-messages" ripple onClick={handleOpenSavedDialog}>
<span>{lang('SavedMessagesTab')}</span>
</ListItem>
)}
</div>
);
};
export default memo(withGlobal<OwnProps>(
(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<OwnProps>(
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<OwnProps>(
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<OwnProps>(
chatInviteLink,
description,
topicLink,
hasSavedMessages,
};
},
)(ChatExtra));

View File

@ -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;
};

View File

@ -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;

View File

@ -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<OwnProps> = ({
withEmojiStatus,
emojiStatusSize,
isSavedMessages,
isSavedDialog,
noLoopLimit,
canCopyTitle,
onEmojiStatusClick,
@ -65,10 +69,26 @@ const FullNameTitle: FC<OwnProps> = ({
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 (
<div className={buildClassName('title', styles.root, className)}>
<h3>{lang('SavedMessages')}</h3>
<h3>{specialTitle}</h3>
</div>
);
}

View File

@ -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<OwnProps & StateProps> = ({
@ -97,6 +100,8 @@ const GroupChatInfo: FC<OwnProps & StateProps> = ({
storyViewerOrigin,
noEmojiStatus,
emojiStatusSize,
isSavedDialog,
self,
onClick,
onEmojiStatusClick,
}) => {
@ -199,15 +204,28 @@ const GroupChatInfo: FC<OwnProps & StateProps> = ({
onClick={onClick}
>
{!noAvatar && !isTopic && (
<Avatar
key={chat.id}
size={avatarSize}
peer={chat}
withStory={withStory}
storyViewerOrigin={storyViewerOrigin}
storyViewerMode="single-peer"
onClick={withMediaViewer ? handleAvatarViewerOpen : undefined}
/>
<>
{isSavedDialog && self && (
<Avatar
key="saved-messages"
size={avatarSize}
peer={self}
isSavedMessages
className="saved-dialog-avatar"
/>
)}
<Avatar
key={chat.id}
className={buildClassName(isSavedDialog && 'overlay-avatar')}
size={avatarSize}
peer={chat}
withStory={withStory}
storyViewerOrigin={storyViewerOrigin}
storyViewerMode="single-peer"
isSavedDialog={isSavedDialog}
onClick={withMediaViewer ? handleAvatarViewerOpen : undefined}
/>
</>
)}
{isTopic && (
<TopicIcon
@ -224,6 +242,7 @@ const GroupChatInfo: FC<OwnProps & StateProps> = ({
peer={chat}
emojiStatusSize={emojiStatusSize}
withEmojiStatus={!noEmojiStatus}
isSavedDialog={isSavedDialog}
onEmojiStatusClick={onEmojiStatusClick}
/>
)}
@ -258,6 +277,7 @@ export default memo(withGlobal<OwnProps>(
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<OwnProps>(
topic,
areMessagesLoaded,
messagesCount,
self,
};
},
)(GroupChatInfo));

View File

@ -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 (
<i
className={buildClassName(`icon icon-${name}`, className)}
style={style}
aria-hidden
aria-hidden={!ariaLabel}
aria-label={ariaLabel}
role={role}
/>
);
};

View File

@ -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<OwnProps & StateProps> = ({
noRtl,
user,
userStatus,
self,
isSavedMessages,
isSavedDialog,
areMessagesLoaded,
adminMember,
ripple,
@ -166,6 +170,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
withEmojiStatus={!noEmojiStatus}
emojiStatusSize={emojiStatusSize}
isSavedMessages={isSavedMessages}
isSavedDialog={isSavedDialog}
onEmojiStatusClick={onEmojiStatusClick}
/>
{customTitle && <span className="custom-title">{customTitle}</span>}
@ -179,6 +184,7 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
withEmojiStatus={!noEmojiStatus}
emojiStatusSize={emojiStatusSize}
isSavedMessages={isSavedMessages}
isSavedDialog={isSavedDialog}
onEmojiStatusClick={onEmojiStatusClick}
/>
);
@ -186,11 +192,22 @@ const PrivateChatInfo: FC<OwnProps & StateProps> = ({
return (
<div className={buildClassName('ChatInfo', className)} dir={!noRtl && lang.isRtl ? 'rtl' : undefined}>
{isSavedDialog && self && (
<Avatar
key="saved-messages"
size={avatarSize}
peer={self}
isSavedMessages
className="saved-dialog-avatar"
/>
)}
<Avatar
key={user.id}
size={avatarSize}
peer={user}
className={buildClassName(isSavedDialog && 'overlay-avatar')}
isSavedMessages={isSavedMessages}
isSavedDialog={isSavedDialog}
withStory={withStory}
storyViewerOrigin={storyViewerOrigin}
storyViewerMode="single-peer"
@ -210,6 +227,7 @@ export default memo(withGlobal<OwnProps>(
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<OwnProps>(
userStatus,
isSavedMessages,
areMessagesLoaded,
self,
};
},
)(PrivateChatInfo));

View File

@ -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<OwnProps & StateProps> = ({
user,
userStatus,
chat,
isSavedMessages,
connectionState,
mediaId,
avatarOwnerId,
@ -107,8 +105,8 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
}
function renderPhotoTabs() {
if (isSavedMessages || !photos || photos.length <= 1) {
if (!photos || photos.length <= 1) {
return undefined;
}
@ -241,7 +239,7 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
}
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<OwnProps & StateProps> = ({
user={user}
chat={chat}
photo={profilePhoto}
isSavedMessages={isSavedMessages}
canPlayVideo={Boolean(isActive && canPlayVideo)}
onClick={handleProfilePhotoClick}
/>
@ -260,6 +257,11 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
}
function renderStatus() {
const peerId = (chatId || userId)!;
const isAnonymousForwards = isAnonymousForwardsChat(peerId);
if (isAnonymousForwards) return undefined;
if (user) {
return (
<div className={buildClassName(styles.status, 'status', isUserOnline(user, userStatus) && 'online')}>
@ -348,26 +350,24 @@ const ProfileInfo: FC<OwnProps & StateProps> = ({
peer={(user || chat)!}
withEmojiStatus
emojiStatusSize={EMOJI_STATUS_SIZE}
isSavedMessages={isSavedMessages}
onEmojiStatusClick={handleStatusClick}
noLoopLimit
canCopyTitle
/>
)}
{!isSavedMessages && renderStatus()}
{renderStatus()}
</div>
</div>
);
};
export default memo(withGlobal<OwnProps>(
(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<OwnProps>(
userProfilePhoto: userFullInfo?.profilePhoto,
userFallbackPhoto: userFullInfo?.fallbackPhoto,
chatProfilePhoto: chatFullInfo?.profilePhoto,
isSavedMessages,
mediaId,
avatarOwnerId,
emojiStatusSticker,

View File

@ -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<OwnProps> = ({
user,
photo,
isSavedMessages,
isSavedDialog,
canPlayVideo,
onClick,
}) => {
@ -55,8 +61,9 @@ const ProfilePhoto: FC<OwnProps> = ({
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<OwnProps> = ({
}
}, [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 = <i className="icon icon-avatar-saved-messages" />;
} else if (isDeleted) {
content = <i className="icon icon-avatar-deleted-account" />;
} else if (isRepliesChat) {
content = <i className="icon icon-reply-filled" />;
if (specialIcon) {
content = <Icon name={specialIcon} role="img" />;
} else if (hasMedia) {
content = (
<>
@ -141,6 +164,7 @@ const ProfilePhoto: FC<OwnProps> = ({
'ProfilePhoto',
getPeerColorClass(peer),
isSavedMessages && 'saved-messages',
isAnonymousForwards && 'anonymous-forwards',
isDeleted && 'deleted-account',
isRepliesChat && 'replies-bot-account',
(!isSavedMessages && !hasMedia) && 'no-photo',

View File

@ -3,6 +3,7 @@ import React, { memo, useMemo, useState } from '../../lib/teact/teact';
import { getGlobal, withGlobal } from '../../global';
import 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<OwnProps & StateProps> = ({
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]);

View File

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

View File

@ -1,5 +1,5 @@
import type { FC } from '../../../lib/teact/teact';
import 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<OwnProps & StateProps> = ({
@ -127,10 +135,16 @@ const Chat: FC<OwnProps & StateProps> = ({
canChangeFolder,
lastMessageTopic,
typingStatus,
lastMessageId,
lastMessage,
isSavedDialog,
currentUserId,
onDragEnter,
}) => {
const {
openChat,
openSavedDialog,
toggleChatInfo,
focusLastMessage,
loadTopics,
openForumPanel,
@ -147,7 +161,9 @@ const Chat: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
animationType,
withInterfaceAnimations,
orderDiff,
isSavedDialog,
});
const getIsForumPanelClosed = useSelectorSignal(selectIsForumPanelClosed);
@ -171,6 +188,15 @@ const Chat: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
isPinned,
isMuted,
canChangeFolder,
isSavedDialog,
currentUserId,
});
const isIntersecting = useIsIntersecting(ref, chat ? observeIntersection : undefined);
@ -242,6 +270,16 @@ const Chat: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
<ListItem
ref={ref}
className={className}
href={IS_OPEN_IN_NEW_TAB_SUPPORTED ? `#${createLocationHash(chatId, 'thread', MAIN_THREAD_ID)}` : undefined}
href={href}
style={`top: ${offsetTop}px`}
ripple={!isForum && !isMobile}
contextActions={contextActions}
@ -272,6 +310,7 @@ const Chat: FC<OwnProps & StateProps> = ({
<Avatar
peer={peer}
isSavedMessages={user?.isSelf}
isSavedDialog={isSavedDialog}
withStory={!user?.isSelf}
withStoryGap={isAvatarOnlineShown}
storyViewerOrigin={StoryViewerOrigin.ChatList}
@ -291,21 +330,22 @@ const Chat: FC<OwnProps & StateProps> = ({
peer={peer}
withEmojiStatus
isSavedMessages={chatId === user?.id && user?.isSelf}
isSavedDialog={isSavedDialog}
observeIntersection={observeIntersection}
/>
{isMuted && <i className="icon icon-muted" />}
{isMuted && !isSavedDialog && <i className="icon icon-muted" />}
<div className="separator" />
{chat.lastMessage && (
{lastMessage && (
<LastMessageMeta
message={chat.lastMessage}
outgoingStatus={lastMessageOutgoingStatus}
message={lastMessage}
outgoingStatus={!isSavedDialog ? lastMessageOutgoingStatus : undefined}
draftDate={draft?.date}
/>
)}
</div>
<div className="subtitle">
{renderSubtitle()}
<ChatBadge chat={chat} isPinned={isPinned} isMuted={isMuted} />
<ChatBadge chat={chat} isPinned={isPinned} isMuted={isMuted} isSavedDialog={isSavedDialog} />
</div>
</div>
{shouldRenderDeleteModal && (
@ -346,29 +386,34 @@ const Chat: FC<OwnProps & StateProps> = ({
};
export default memo(withGlobal<OwnProps>(
(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<OwnProps>(
lastMessageTopic,
typingStatus,
withInterfaceAnimations: selectCanAnimateInterface(global),
lastMessage,
lastMessageId,
currentUserId: global.currentUserId!,
};
},
)(Chat));

View File

@ -21,12 +21,13 @@ type OwnProps = {
wasTopicOpened?: boolean;
isPinned?: boolean;
isMuted?: boolean;
isSavedDialog?: boolean;
shouldShowOnlyMostImportant?: boolean;
forceHidden?: boolean | Signal<boolean>;
};
const ChatBadge: FC<OwnProps> = ({
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<OwnProps> = ({
|| 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<OwnProps> = ({
</div>
) : undefined;
const pinnedElement = isPinned && !unreadCountElement && !unreadMentionsElement && !unreadReactionsElement && (
const pinnedElement = isPinned && (
<div className={className}>
<i className="icon icon-pinned-chat" />
</div>
);
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];

View File

@ -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<string, ApiSession>;
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<OwnProps> = ({
className,
folderType,
folderId,
isActive,
@ -82,18 +85,19 @@ const ChatList: FC<OwnProps> = ({
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<OwnProps> = ({
// Support <Cmd>+<Digit> 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<OwnProps> = ({
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<OwnProps> = ({
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<OwnProps> = ({
});
const handleArchivedClick = useLastCallback(() => {
onLeftColumnContentChange(LeftColumnContent.Archived);
onLeftColumnContentChange!(LeftColumnContent.Archived);
closeForumPanel();
});
@ -199,7 +206,7 @@ const ChatList: FC<OwnProps> = ({
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<OwnProps> = ({
return (
<Chat
key={id}
teactOrderKey={isPinned ? i : getOrderKey(id)}
teactOrderKey={isPinned ? i : getOrderKey(id, isSaved)}
chatId={id}
isPinned={isPinned}
folderId={folderId}
isSavedDialog={isSaved}
animationType={getAnimationType(id)}
orderDiff={orderDiffById[id]}
offsetTop={offsetTop}
@ -229,7 +237,7 @@ const ChatList: FC<OwnProps> = ({
return (
<InfiniteScroll
className={buildClassName('chat-list custom-scroll', isForumPanelOpen && 'forum-panel-open')}
className={buildClassName('chat-list custom-scroll', isForumPanelOpen && 'forum-panel-open', className)}
ref={containerRef}
items={viewportIds}
itemSelector=".ListItem:not(.chat-item-archive)"
@ -257,13 +265,13 @@ const ChatList: FC<OwnProps> = ({
)}
{viewportIds?.length ? (
renderChats()
) : viewportIds && !viewportIds.length ? (
) : viewportIds && !viewportIds.length && !isSaved ? (
(
<EmptyFolder
folderId={folderId}
folderType={folderType}
foldersDispatch={foldersDispatch}
onSettingsScreenSelect={onSettingsScreenSelect}
foldersDispatch={foldersDispatch!}
onSettingsScreenSelect={onSettingsScreenSelect!}
/>
)
) : (

View File

@ -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;
};

View File

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

View File

@ -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 <TypingStatus typingStatus={typingStatus} />;
}
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({
<span className="colon">:</span>
</>
)}
{lastMessage.forwardInfo && (<i className="icon icon-share-filled chat-prefix-icon" />)}
{!isSavedDialog && lastMessage.forwardInfo && (<i className="icon icon-share-filled chat-prefix-icon" />)}
{lastMessage.replyInfo?.type === 'story' && (<i className="icon icon-story-reply chat-prefix-icon" />)}
{renderSummary(lang, lastMessage, observeIntersection, mediaBlobUrl || mediaThumbnail, isRoundVideo)}
</p>
@ -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() {

View File

@ -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<string, ApiChat>;
localContactIds?: string[];
searchQuery?: string;
isSearching?: boolean;
@ -40,7 +38,6 @@ const NewChatStep1: FC<OwnProps & StateProps> = ({
onSelectedMemberIdsChange,
onNextStep,
onReset,
chatsById,
localContactIds,
searchQuery,
isSearching,
@ -80,11 +77,10 @@ const NewChatStep1: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>(
(global): StateProps => {
const { userIds: localContactIds } = global.contactList || {};
const { byId: chatsById } = global.chats;
const {
query: searchQuery,
@ -145,7 +140,6 @@ export default memo(withGlobal<OwnProps>(
const { userIds: localUserIds } = localResults || {};
return {
chatsById,
localContactIds,
searchQuery,
isSearching: fetchingStatus?.chats,

View File

@ -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<OwnProps & StateProps> = ({
].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<OwnProps & StateProps> = ({
return sortChatIds(
unique([...globalChatIds, ...globalUserIds]),
chatsById,
true,
);
}, [chatsById, globalChatIds, globalUserIds, searchQuery]);
}, [globalChatIds, globalUserIds, searchQuery]);
const foundMessages = useMemo(() => {
if ((!searchQuery && !searchDate) || !foundIds || foundIds.length === 0) {

View File

@ -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<OwnProps> = ({
}
}, [isOpen, markIsShown]);
const handleSelectRecipient = useCallback((recipientId: string, threadId?: number) => {
const handleSelectRecipient = useCallback((recipientId: string, threadId?: ThreadId) => {
openChatWithDraft({
chatId: recipientId,
threadId,

View File

@ -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<OwnProps & StateProps> = ({
}
}, [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<OwnProps & StateProps> = ({
forwardToSavedMessages();
showNotification({ message });
} else {
setForwardChatOrTopic({ chatId: recipientId, topicId: threadId });
setForwardChatOrTopic({ chatId: recipientId, topicId: Number(threadId) });
}
}, [currentUserId, isManyMessages, isStory, lang]);

View File

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

View File

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

View File

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

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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<OwnProps>(
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),

View File

@ -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<OwnProps>(
(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;

View File

@ -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<string, { icon: IconName; label: string }> = {
export type OwnProps = {
chatId: string;
threadId: number;
threadId: ThreadId;
isOpen: boolean;
withExtraActions: boolean;
anchor: IAnchorPosition;
@ -276,7 +276,7 @@ const HeaderMenuContainer: FC<OwnProps & StateProps> = ({
});
const handleEditTopicClick = useLastCallback(() => {
openEditTopicPanel({ chatId, topicId: threadId });
openEditTopicPanel({ chatId, topicId: Number(threadId) });
setShouldCloseFast(!isRightColumnShown);
closeMenu();
});

View File

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

View File

@ -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<OwnProps & StateProps> = ({
isReady,
isChatWithSelf,
isRepliesChat,
isAnonymousForwards,
isCreator,
isBot,
messageIds,
@ -171,8 +178,9 @@ const MessageList: FC<OwnProps & StateProps> = ({
topic,
noMessageSendingAnimation,
isServiceNotificationsChat,
onPinnedIntersectionChange,
currentUserId,
getForceNextPinnedInHeader,
onPinnedIntersectionChange,
}) => {
const {
loadViewportMessages, setScrollOffset, loadSponsoredMessages, loadMessageReactions, copyMessagesByIds,
@ -199,6 +207,9 @@ const MessageList: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
? groupMessages(
orderBy(listedMessages, orderRule),
memoUnreadDividerBeforeIdRef.current,
!isForum ? threadId : undefined,
!isForum ? Number(threadId) : undefined,
isChatWithSelf,
)
: undefined;
@ -547,9 +558,9 @@ const MessageList: FC<OwnProps & StateProps> = ({
}, [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<OwnProps & StateProps> = ({
isSelectModeActive && 'select-mode-active',
isScrolled && 'scrolled',
!isReady && 'is-animating',
hasOpenChatButton && 'saved-dialog',
);
const hasMessages = (messageIds && messageGroups) || lastMessage;
@ -610,6 +622,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>(
(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<OwnProps>(
? 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<OwnProps>(
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<OwnProps>(
isServiceNotificationsChat: chatId === SERVICE_NOTIFICATIONS_USER_ID,
isForum: chat.isForum,
isEmptyThread,
currentUserId,
...(withLastMessageWhenPreloading && { lastMessage }),
};
},

View File

@ -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<number | undefined>;
@ -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<OwnProps> = ({
isSchedule,
shouldRenderBotInfo,
noAppearanceAnimation,
isSavedDialog,
onFabToggle,
onNotchToggle,
onPinnedIntersectionChange,
@ -92,6 +95,7 @@ const MessageListContent: FC<OwnProps> = ({
const { openHistoryCalendar } = getActions();
const getIsReady = useDerivedSignal(isReady);
const areDatesClickable = !isSavedDialog && !isSchedule;
const {
observeIntersectionForReading,
@ -162,7 +166,7 @@ const MessageListContent: FC<OwnProps> = ({
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<OwnProps> = ({
teactFastList
>
<div
className={buildClassName('sticky-date', !isSchedule && 'interactive')}
className={buildClassName('sticky-date', areDatesClickable && 'interactive')}
key="date-header"
onMouseDown={preventMessageInputBlur}
onClick={!isSchedule ? () => openHistoryCalendar({ selectedAt: dateGroup.datetime }) : undefined}
onClick={areDatesClickable ? () => openHistoryCalendar({ selectedAt: dateGroup.datetime }) : undefined}
>
<span dir="auto">
{isSchedule && dateGroup.originalDate === SCHEDULED_WHEN_ONLINE && (

View File

@ -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;

View File

@ -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}
>
<i className="icon icon-unpin" />
@ -571,6 +581,19 @@ function MiddleColumn({
</Button>
</div>
)}
{canShowOpenChatButton && (
<div className="middle-column-footer-button-container" dir={lang.isRtl ? 'rtl' : undefined}>
<Button
size="tiny"
fluid
color="secondary"
className="composer-button open-chat-button"
onClick={handleOpenChatFromSaved}
>
<span>{lang('SavedOpenChat')}</span>
</Button>
</div>
)}
{isMessagingDisabled && (
<div className={messagingDisabledClassName}>
<div className="messaging-disabled-inner">
@ -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<OwnProps>(
? 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<OwnProps>(
&& (!chat || canPost)
&& !isBotNotStarted
&& !(shouldJoinToSend && chat?.isNotJoined)
&& !shouldBlockSendInForum,
&& !shouldBlockSendInForum
&& !isSavedDialog,
isPinnedMessageList,
currentUserBannedRights: chat?.currentUserBannedRights,
defaultBannedRights: chat?.defaultBannedRights,
@ -807,6 +834,8 @@ export default memo(withGlobal<OwnProps>(
topMessageId,
canUnpin,
canUnblock,
isSavedDialog,
canShowOpenChatButton,
};
},
)(MiddleColumn));

View File

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

View File

@ -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<number, ApiMessage>;
canUnpin?: boolean;
isSavedDialog?: boolean;
topMessageSender?: ApiPeer;
typingStatus?: ApiTypingStatus;
isSelectModeActive?: boolean;
@ -145,6 +147,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
getCurrentPinnedIndexes,
getLoadingPinnedId,
emojiStatusSticker,
isSavedDialog,
onFocusPinnedMessage,
}) => {
const {
@ -352,7 +355,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
}
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)}
<div
className="chat-info-wrapper"
onClick={handleHeaderClick}
onMouseDown={handleHeaderMouseDown}
>
{isUserId(chatId) ? (
{isUserId(realChatId) ? (
<PrivateChatInfo
key={chatId}
userId={chatId}
key={realChatId}
userId={realChatId}
typingStatus={typingStatus}
status={connectionStatusText}
status={connectionStatusText || savedMessagesStatus}
withDots={Boolean(connectionStatusText)}
withFullInfo
withMediaViewer
withStory={!isChatWithSelf}
withUpdatingStatus
isSavedDialog={isSavedDialog}
storyViewerOrigin={StoryViewerOrigin.MiddleHeaderAvatar}
emojiStatusSize={EMOJI_STATUS_SIZE}
noRtl
@ -403,16 +411,17 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
/>
) : (
<GroupChatInfo
key={chatId}
chatId={chatId}
threadId={threadId}
key={realChatId}
chatId={realChatId}
threadId={!isSavedDialog ? threadId : undefined}
typingStatus={typingStatus}
status={connectionStatusText}
status={connectionStatusText || savedMessagesStatus}
withDots={Boolean(connectionStatusText)}
withMediaViewer={threadId === MAIN_THREAD_ID}
withFullInfo={threadId === MAIN_THREAD_ID}
withUpdatingStatus
withStory
isSavedDialog={isSavedDialog}
storyViewerOrigin={StoryViewerOrigin.MiddleHeaderAvatar}
emojiStatusSize={EMOJI_STATUS_SIZE}
onEmojiStatusClick={handleChannelStatusClick}
@ -552,6 +561,8 @@ export default memo(withGlobal<OwnProps>(
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<OwnProps>(
isFetchingDifference: global.isFetchingDifference,
emojiStatusSticker,
hasButtonInHeader: canStartBot || canRestartBot || canSubscribe || shouldSendJoinRequest,
isSavedDialog,
};
const messagesById = selectChatMessages(global, chatId);
@ -576,8 +588,8 @@ export default memo(withGlobal<OwnProps>(
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<OwnProps>(
};
}
const pinnedMessageIds = selectPinnedIds(global, chatId, threadId);
const pinnedMessageIds = !isSavedDialog ? selectPinnedIds(global, chatId, threadId) : undefined;
if (pinnedMessageIds?.length) {
const firstPinnedMessage = messagesById[pinnedMessageIds[0]];
const {

View File

@ -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[];

View File

@ -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;

View File

@ -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;

View File

@ -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<string>;
canShowCustomSendMenu?: boolean;

View File

@ -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<HTMLDivElement>;
id: string;
chatId: string;
threadId: number;
threadId: ThreadId;
isAttachmentModalInput?: boolean;
isStoryInput?: boolean;
customEmojiPrefix: string;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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<string>;
isDisabled?: boolean;
};

View File

@ -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<string>;
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();

View File

@ -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,

View File

@ -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);

View File

@ -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<Record<string, number>>({});
const [getForceNextPinnedInHeader, setForceNextPinnedInHeader] = useSignal<boolean | undefined>();

View File

@ -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<OwnProps & StateProps> = ({
isForwarding,
isChatWithSelf,
isRepliesChat,
isAnonymousForwards,
isChannel,
isGroup,
canReply,
@ -387,6 +394,7 @@ const Message: FC<OwnProps & StateProps> = ({
isConnected,
getIsMessageListReady,
shouldWarnAboutSvg,
isInSavedDialog,
onPinnedIntersectionChange,
}) => {
const {
@ -483,6 +491,7 @@ const Message: FC<OwnProps & StateProps> = ({
forwardInfo
&& (!isChatWithSelf || isScheduled)
&& !isRepliesChat
&& !isAnonymousForwards
&& !forwardInfo.isLinkedChannelPost
&& !isCustomShape
) || Boolean(message.content.storyData && !message.content.storyData.isMention);
@ -500,7 +509,7 @@ const Message: FC<OwnProps & StateProps> = ({
const canForward = isChannel && !isScheduled && message.isForwardingAllowed && !isChatProtected;
const canFocus = Boolean(isPinnedList
|| (forwardInfo
&& (forwardInfo.isChannelPost || (isChatWithSelf && !isOwn) || isRepliesChat)
&& (forwardInfo.isChannelPost || (isChatWithSelf && !isOwn) || isRepliesChat || isAnonymousForwards)
&& forwardInfo.fromMessageId
));
@ -520,7 +529,8 @@ const Message: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
<MessageMeta
message={message}
isPinned={isPinned}
isInSavedDialog={isInSavedDialog}
noReplies={noReplies}
repliesThreadInfo={repliesThreadInfo}
outgoingStatus={outgoingStatus}
@ -1461,6 +1472,7 @@ export default memo(withGlobal<OwnProps>(
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<OwnProps>(
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<OwnProps>(
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<OwnProps>(
reactionMessage,
isChatWithSelf,
isRepliesChat,
isAnonymousForwards,
isChannel,
isGroup,
canReply,
@ -1621,6 +1637,7 @@ export default memo(withGlobal<OwnProps>(
withStickerEffects: selectPerformanceSettingsValue(global, 'stickerEffects'),
webPageStory,
isConnected,
isInSavedDialog,
isLoadingComments: repliesThreadInfo?.isCommentsInfo
&& loadingThread?.loadingChatId === repliesThreadInfo?.originChannelId
&& loadingThread?.loadingMessageId === repliesThreadInfo?.originMessageId,

View File

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

View File

@ -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<HTMLDivElement, MouseEvent>, groupedId?: string) => void,
message: ApiMessage,
chatId: string,
threadId: number,
threadId: ThreadId,
isInDocumentGroup: boolean,
asForwarded?: boolean,
isScheduled?: boolean,

View File

@ -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<string, ApiChat>;
localContactIds?: string[];
searchQuery?: string;
isLoading: boolean;
@ -50,7 +50,6 @@ const AddChatMembers: FC<OwnProps & StateProps> = ({
members,
onNextStep,
currentUserId,
chatsById,
localContactIds,
isLoading,
searchQuery,
@ -104,11 +103,8 @@ const AddChatMembers: FC<OwnProps & StateProps> = ({
&& (!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<OwnProps>(
(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<OwnProps>(
isChannel,
members: selectChatFullInfo(global, chatId)?.members,
currentUserId,
chatsById,
localContactIds,
searchQuery,
isSearching: fetchingStatus,

View File

@ -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;
}
}
}
}
}

View File

@ -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<OwnProps & StateProps> = ({
chatId,
topicId,
threadId,
profileState,
onProfileStateChange,
theme,
@ -170,6 +178,8 @@ const Profile: FC<OwnProps & StateProps> = ({
similarChannels,
isCurrentUserPremium,
limitSimilarChannels,
isTopicInfo,
isSavedDialog,
}) => {
const {
setLocalMediaSearchType,
@ -195,29 +205,33 @@ const Profile: FC<OwnProps & StateProps> = ({
const lang = useLang();
const [deletingUserId, setDeletingUserId] = useState<string | undefined>();
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<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
// 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<OwnProps & StateProps> = ({
} 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<OwnProps & StateProps> = ({
}
function renderContent() {
if (resultType === 'dialogs') {
return (
<ChatList className="saved-dialogs" folderType="saved" isActive />
);
}
if (!viewportIds || !canRenderContent || !messagesById) {
const noSpinner = isFirstTab && !canRenderContent;
const forceRenderHiddenMembers = Boolean(resultType === 'members' && areMembersHidden);
@ -594,7 +613,9 @@ const Profile: FC<OwnProps & StateProps> = ({
onLoadMore={getMore}
onScroll={handleScroll}
>
{!noProfileInfo && renderProfileInfo(chatId, resolvedUserId, isRightColumnShown && canRenderContent)}
{!noProfileInfo && !isSavedMessages && (
renderProfileInfo(profileId, isRightColumnShown && canRenderContent, isSavedDialog)
)}
{!isRestricted && (
<div
className="shared-media"
@ -635,29 +656,35 @@ const Profile: FC<OwnProps & StateProps> = ({
);
};
function renderProfileInfo(chatId: string, resolvedUserId: string | undefined, isReady: boolean) {
function renderProfileInfo(profileId: string, isReady: boolean, isSavedDialog?: boolean) {
return (
<div className="profile-info">
<ProfileInfo userId={resolvedUserId || chatId} canPlayVideo={isReady} />
<ChatExtra chatOrUserId={resolvedUserId || chatId} />
<ProfileInfo userId={profileId} canPlayVideo={isReady} />
<ChatExtra chatOrUserId={profileId} isSavedDialog={isSavedDialog} />
</div>
);
}
export default memo(withGlobal<OwnProps>(
(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<OwnProps>(
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<OwnProps>(
shouldWarnAboutSvg: global.settings.byKey.shouldWarnAboutSvg,
similarChannels,
isCurrentUserPremium,
isTopicInfo,
isSavedDialog,
limitSimilarChannels: selectPremiumLimit(global, 'recommendedChannels'),
...(hasMembersTab && members && { members, adminMembersById }),
...(hasCommonChatsTab && user && { commonChatIds: user.commonChats?.ids }),

View File

@ -2,15 +2,19 @@ import type { FC } from '../../lib/teact/teact';
import React, { memo, useEffect, useState } from '../../lib/teact/teact';
import { 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<OwnProps & StateProps> = ({
chatId,
threadId,
isMobile,
isInsideTopic,
isChatSelected,
shouldSkipHistoryAnimations,
nextManagementScreen,
nextProfileTab,
isSavedMessages,
isSavedDialog,
}) => {
const {
toggleChatInfo,
@ -97,7 +104,9 @@ const RightColumn: FC<OwnProps & StateProps> = ({
} = getActions();
const { width: windowWidth } = useWindowSize();
const [profileState, setProfileState] = useState<ProfileState>(ProfileState.Profile);
const [profileState, setProfileState] = useState<ProfileState>(
isSavedMessages && !isSavedDialog ? ProfileState.SavedDialogs : ProfileState.Profile,
);
const [managementScreen, setManagementScreen] = useState<ManagementScreens>(ManagementScreens.Initial);
const [selectedChatMemberId, setSelectedChatMemberId] = useState<string | undefined>();
const [isPromotedByCurrentUser, setIsPromotedByCurrentUser] = useState<boolean | undefined>();
@ -129,7 +138,7 @@ const RightColumn: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
}, [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<OwnProps & StateProps> = ({
case RightColumnContent.ChatInfo:
return (
<Profile
key={`profile_${chatId!}`}
key={`profile_${chatId!}_${threadId}`}
chatId={chatId!}
topicId={isInsideTopic ? threadId : undefined}
threadId={threadId}
profileState={profileState}
isMobile={isMobile}
onProfileStateChange={setProfileState}
@ -400,18 +411,20 @@ export default memo(withGlobal<OwnProps>(
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));

View File

@ -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;

View File

@ -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<OwnProps & StateProps> = ({
@ -157,6 +161,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
isBot,
isInsideTopic,
canEditTopic,
isSavedMessages,
onClose,
onScreenSelect,
canEditBot,
@ -178,6 +183,8 @@ const RightHeader: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
toggleStatistics();
});
const handleClose = useLastCallback(() => {
onClose(!isSavedMessages);
});
const [shouldSkipTransition, setShouldSkipTransition] = useState(!isColumnOpen);
useEffect(() => {
@ -240,6 +251,8 @@ const RightHeader: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
switch (renderingContentKey) {
case HeaderContent.PollResults:
return <h3>{lang('PollResults')}</h3>;
return <h3 className="title">{lang('PollResults')}</h3>;
case HeaderContent.Search:
return (
<>
@ -354,39 +371,39 @@ const RightHeader: FC<OwnProps & StateProps> = ({
</>
);
case HeaderContent.AddingMembers:
return <h3>{lang(isChannel ? 'ChannelAddSubscribers' : 'GroupAddMembers')}</h3>;
return <h3 className="title">{lang(isChannel ? 'ChannelAddSubscribers' : 'GroupAddMembers')}</h3>;
case HeaderContent.ManageInitial:
return <h3>{lang('Edit')}</h3>;
return <h3 className="title">{lang('Edit')}</h3>;
case HeaderContent.ManageChatPrivacyType:
return <h3>{lang(isChannel ? 'ChannelTypeHeader' : 'GroupTypeHeader')}</h3>;
return <h3 className="title">{lang(isChannel ? 'ChannelTypeHeader' : 'GroupTypeHeader')}</h3>;
case HeaderContent.ManageDiscussion:
return <h3>{lang('Discussion')}</h3>;
return <h3 className="title">{lang('Discussion')}</h3>;
case HeaderContent.ManageChatAdministrators:
return <h3>{lang('ChannelAdministrators')}</h3>;
return <h3 className="title">{lang('ChannelAdministrators')}</h3>;
case HeaderContent.ManageGroupRecentActions:
return <h3>{lang('Group.Info.AdminLog')}</h3>;
return <h3 className="title">{lang('Group.Info.AdminLog')}</h3>;
case HeaderContent.ManageGroupAdminRights:
return <h3>{lang('EditAdminRights')}</h3>;
return <h3 className="title">{lang('EditAdminRights')}</h3>;
case HeaderContent.ManageGroupNewAdminRights:
return <h3>{lang('SetAsAdmin')}</h3>;
return <h3 className="title">{lang('SetAsAdmin')}</h3>;
case HeaderContent.ManageGroupPermissions:
return <h3>{lang('ChannelPermissions')}</h3>;
return <h3 className="title">{lang('ChannelPermissions')}</h3>;
case HeaderContent.ManageGroupRemovedUsers:
return <h3>{lang('BlockedUsers')}</h3>;
return <h3 className="title">{lang('BlockedUsers')}</h3>;
case HeaderContent.ManageChannelRemovedUsers:
return <h3>{lang('ChannelBlockedUsers')}</h3>;
return <h3 className="title">{lang('ChannelBlockedUsers')}</h3>;
case HeaderContent.ManageGroupUserPermissionsCreate:
return <h3>{lang('ChannelAddException')}</h3>;
return <h3 className="title">{lang('ChannelAddException')}</h3>;
case HeaderContent.ManageGroupUserPermissions:
return <h3>{lang('UserRestrictions')}</h3>;
return <h3 className="title">{lang('UserRestrictions')}</h3>;
case HeaderContent.ManageInvites:
return <h3>{lang('lng_group_invite_title')}</h3>;
return <h3 className="title">{lang('lng_group_invite_title')}</h3>;
case HeaderContent.ManageEditInvite:
return <h3>{isEditingInvite ? lang('EditLink') : lang('NewLink')}</h3>;
return <h3 className="title">{isEditingInvite ? lang('EditLink') : lang('NewLink')}</h3>;
case HeaderContent.ManageInviteInfo:
return (
<>
<h3>{lang('InviteLink')}</h3>
<h3 className="title">{lang('InviteLink')}</h3>
<section className="tools">
{currentInviteInfo && !currentInviteInfo.isRevoked && (
<Button
@ -425,9 +442,9 @@ const RightHeader: FC<OwnProps & StateProps> = ({
</>
);
case HeaderContent.ManageJoinRequests:
return <h3>{isChannel ? lang('SubscribeRequests') : lang('MemberRequests')}</h3>;
return <h3 className="title">{isChannel ? lang('SubscribeRequests') : lang('MemberRequests')}</h3>;
case HeaderContent.ManageGroupAddAdmins:
return <h3>{lang('Channel.Management.AddModerator')}</h3>;
return <h3 className="title">{lang('Channel.Management.AddModerator')}</h3>;
case HeaderContent.StickerSearch:
return (
<SearchInput
@ -447,32 +464,40 @@ const RightHeader: FC<OwnProps & StateProps> = ({
/>
);
case HeaderContent.Statistics:
return <h3>{lang(isChannel ? 'ChannelStats.Title' : 'GroupStats.Title')}</h3>;
return <h3 className="title">{lang(isChannel ? 'ChannelStats.Title' : 'GroupStats.Title')}</h3>;
case HeaderContent.MessageStatistics:
return <h3>{lang('Stats.MessageTitle')}</h3>;
return <h3 className="title">{lang('Stats.MessageTitle')}</h3>;
case HeaderContent.StoryStatistics:
return <h3>{lang('Stats.StoryTitle')}</h3>;
return <h3 className="title">{lang('Stats.StoryTitle')}</h3>;
case HeaderContent.BoostStatistics:
return <h3>{lang('Boosts')}</h3>;
return <h3 className="title">{lang('Boosts')}</h3>;
case HeaderContent.SharedMedia:
return <h3>{lang('SharedMedia')}</h3>;
return <h3 className="title">{lang('SharedMedia')}</h3>;
case HeaderContent.ManageChannelSubscribers:
return <h3>{lang('ChannelSubscribers')}</h3>;
return <h3 className="title">{lang('ChannelSubscribers')}</h3>;
case HeaderContent.MemberList:
case HeaderContent.ManageGroupMembers:
return <h3>{lang('GroupMembers')}</h3>;
return <h3 className="title">{lang('GroupMembers')}</h3>;
case HeaderContent.StoryList:
return <h3>{lang(isSelf ? 'Settings.MyStories' : 'PeerInfo.PaneStories')}</h3>;
return <h3 className="title">{lang(isSelf ? 'Settings.MyStories' : 'PeerInfo.PaneStories')}</h3>;
case HeaderContent.SavedDialogs:
return (
<div className="header">
<h3 className="title">{lang('SavedMessagesTab')}</h3>
<div className="subtitle">{lang('Chats', foldersChatCount[SAVED_FOLDER_ID])}</div>
</div>
);
case HeaderContent.ManageReactions:
return <h3>{lang('Reactions')}</h3>;
return <h3 className="title">{lang('Reactions')}</h3>;
case HeaderContent.CreateTopic:
return <h3>{lang('NewTopic')}</h3>;
return <h3 className="title">{lang('NewTopic')}</h3>;
case HeaderContent.EditTopic:
return <h3>{lang('EditTopic')}</h3>;
return <h3 className="title">{lang('EditTopic')}</h3>;
default:
return (
<>
<h3>{getHeaderTitle()}
<h3 className="title">
{getHeaderTitle()}
</h3>
<section className="tools">
{canAddContact && (
@ -536,15 +561,16 @@ const RightHeader: FC<OwnProps & StateProps> = ({
}
}
const isBackButton = (
isMobile
|| contentKey === HeaderContent.SharedMedia
|| contentKey === HeaderContent.MemberList
|| contentKey === HeaderContent.StoryList
|| contentKey === HeaderContent.AddingMembers
|| contentKey === HeaderContent.MessageStatistics
|| contentKey === HeaderContent.StoryStatistics
|| isManagement
const isBackButton = isMobile || (
!isSavedMessages && (
contentKey === HeaderContent.SharedMedia
|| contentKey === HeaderContent.MemberList
|| contentKey === HeaderContent.StoryList
|| contentKey === HeaderContent.AddingMembers
|| contentKey === HeaderContent.MessageStatistics
|| contentKey === HeaderContent.StoryStatistics
|| isManagement
)
);
const buttonClassName = buildClassName(
@ -564,7 +590,7 @@ const RightHeader: FC<OwnProps & StateProps> = ({
round
color="translucent"
size="smaller"
onClick={onClose}
onClick={handleClose}
ariaLabel={isBackButton ? lang('Common.Back') : lang('Common.Close')}
>
<div className={buttonClassName} />
@ -594,6 +620,7 @@ export default withGlobal<OwnProps>(
const topic = isInsideTopic ? chat.topics?.[threadId!] : undefined;
const canEditTopic = isInsideTopic && topic && getCanManageTopic(chat, topic);
const isBot = user && isUserBot(user);
const isSavedMessages = chatId ? selectIsChatWithSelf(global, chatId) : undefined;
const canEditBot = isBot && user?.canEditBot;
const canAddContact = user && getCanAddContact(user);
@ -621,6 +648,7 @@ export default withGlobal<OwnProps>(
gifSearchQuery,
isEditingInvite,
currentInviteInfo,
isSavedMessages,
shouldSkipHistoryAnimations: tabState.shouldSkipHistoryAnimations,
canEditBot,
};

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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