Introduce Saved dialogs (#4177)
This commit is contained in:
parent
44261055e6
commit
c3c71cbc9e
@ -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);
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 }),
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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[];
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
1
src/assets/font-icons/author-hidden.svg
Normal file
1
src/assets/font-icons/author-hidden.svg
Normal 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 |
1
src/assets/font-icons/my-notes.svg
Normal file
1
src/assets/font-icons/my-notes.svg
Normal 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 |
@ -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',
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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]);
|
||||
|
||||
39
src/components/common/helpers/sortChatIds.ts
Normal file
39
src/components/common/helpers/sortChatIds.ts
Normal 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');
|
||||
}
|
||||
@ -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));
|
||||
|
||||
@ -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];
|
||||
|
||||
@ -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!}
|
||||
/>
|
||||
)
|
||||
) : (
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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),
|
||||
};
|
||||
},
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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]);
|
||||
|
||||
|
||||
@ -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(() => {
|
||||
|
||||
@ -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',
|
||||
|
||||
@ -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>> = {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
});
|
||||
|
||||
@ -77,7 +77,8 @@
|
||||
}
|
||||
|
||||
&.select-mode-active,
|
||||
&.type-pinned {
|
||||
&.type-pinned,
|
||||
&.saved-dialog {
|
||||
margin-bottom: 0;
|
||||
|
||||
.last-in-list {
|
||||
|
||||
@ -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 }),
|
||||
};
|
||||
},
|
||||
|
||||
@ -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 && (
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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[];
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
};
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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>();
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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} />
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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 }),
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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,
|
||||
};
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -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],
|
||||
};
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -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 });
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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)];
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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}`;
|
||||
}
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -103,6 +103,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
|
||||
isFullyLoaded: {},
|
||||
orderedPinnedIds: {},
|
||||
totalCount: {},
|
||||
lastMessageIds: {},
|
||||
byId: {},
|
||||
fullInfoById: {},
|
||||
similarChannelsById: {},
|
||||
|
||||
@ -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]),
|
||||
});
|
||||
|
||||
@ -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
Loading…
x
Reference in New Issue
Block a user