Search: New design (#4718)
This commit is contained in:
parent
9bf26bbb1c
commit
5f5536b6a0
@ -100,6 +100,9 @@ type ChatListData = {
|
|||||||
totalChatCount: number;
|
totalChatCount: number;
|
||||||
messages: ApiMessage[];
|
messages: ApiMessage[];
|
||||||
lastMessageByChatId: Record<string, number>;
|
lastMessageByChatId: Record<string, number>;
|
||||||
|
nextOffsetId?: number;
|
||||||
|
nextOffsetPeerId?: string;
|
||||||
|
nextOffsetDate?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
let onUpdate: OnApiUpdate;
|
let onUpdate: OnApiUpdate;
|
||||||
@ -111,18 +114,24 @@ export function init(_onUpdate: OnApiUpdate) {
|
|||||||
export async function fetchChats({
|
export async function fetchChats({
|
||||||
limit,
|
limit,
|
||||||
offsetDate,
|
offsetDate,
|
||||||
|
offsetPeer,
|
||||||
|
offsetId,
|
||||||
archived,
|
archived,
|
||||||
withPinned,
|
withPinned,
|
||||||
lastLocalServiceMessageId,
|
lastLocalServiceMessageId,
|
||||||
}: {
|
}: {
|
||||||
limit: number;
|
limit: number;
|
||||||
offsetDate?: number;
|
offsetDate?: number;
|
||||||
|
offsetPeer?: ApiPeer;
|
||||||
|
offsetId?: number;
|
||||||
archived?: boolean;
|
archived?: boolean;
|
||||||
withPinned?: boolean;
|
withPinned?: boolean;
|
||||||
lastLocalServiceMessageId?: number;
|
lastLocalServiceMessageId?: number;
|
||||||
}): Promise<ChatListData | undefined> {
|
}): Promise<ChatListData | undefined> {
|
||||||
|
const peer = (offsetPeer && buildInputPeer(offsetPeer.id, offsetPeer.accessHash)) || new GramJs.InputPeerEmpty();
|
||||||
const result = await invokeRequest(new GramJs.messages.GetDialogs({
|
const result = await invokeRequest(new GramJs.messages.GetDialogs({
|
||||||
offsetPeer: new GramJs.InputPeerEmpty(),
|
offsetPeer: peer,
|
||||||
|
offsetId,
|
||||||
limit,
|
limit,
|
||||||
offsetDate,
|
offsetDate,
|
||||||
...(withPinned && { excludePinned: true }),
|
...(withPinned && { excludePinned: true }),
|
||||||
@ -217,6 +226,13 @@ export async function fetchChats({
|
|||||||
totalChatCount = chatIds.length;
|
totalChatCount = chatIds.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const lastDialog = chats[chats.length - 1];
|
||||||
|
const lastMessageId = lastMessageByChatId[lastDialog?.id];
|
||||||
|
const nextOffsetId = lastMessageId;
|
||||||
|
const nextOffsetPeerId = lastDialog?.id;
|
||||||
|
const nextOffsetDate = messages.reverse()
|
||||||
|
.find((message) => message.chatId === lastDialog?.id && message.id === lastMessageId)?.date;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
chatIds,
|
chatIds,
|
||||||
chats,
|
chats,
|
||||||
@ -227,20 +243,29 @@ export async function fetchChats({
|
|||||||
totalChatCount,
|
totalChatCount,
|
||||||
lastMessageByChatId,
|
lastMessageByChatId,
|
||||||
messages,
|
messages,
|
||||||
|
nextOffsetId,
|
||||||
|
nextOffsetPeerId,
|
||||||
|
nextOffsetDate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function fetchSavedChats({
|
export async function fetchSavedChats({
|
||||||
limit,
|
limit,
|
||||||
offsetDate,
|
offsetDate,
|
||||||
|
offsetPeer,
|
||||||
|
offsetId,
|
||||||
withPinned,
|
withPinned,
|
||||||
}: {
|
}: {
|
||||||
limit: number;
|
limit: number;
|
||||||
offsetDate?: number;
|
offsetDate?: number;
|
||||||
|
offsetPeer?: ApiPeer;
|
||||||
|
offsetId?: number;
|
||||||
withPinned?: boolean;
|
withPinned?: boolean;
|
||||||
}): Promise<ChatListData | undefined> {
|
}): Promise<ChatListData | undefined> {
|
||||||
|
const peer = (offsetPeer && buildInputPeer(offsetPeer.id, offsetPeer.accessHash)) || new GramJs.InputPeerEmpty();
|
||||||
const result = await invokeRequest(new GramJs.messages.GetSavedDialogs({
|
const result = await invokeRequest(new GramJs.messages.GetSavedDialogs({
|
||||||
offsetPeer: new GramJs.InputPeerEmpty(),
|
offsetPeer: peer,
|
||||||
|
offsetId,
|
||||||
limit,
|
limit,
|
||||||
offsetDate,
|
offsetDate,
|
||||||
...(withPinned && { excludePinned: true }),
|
...(withPinned && { excludePinned: true }),
|
||||||
@ -305,6 +330,13 @@ export async function fetchSavedChats({
|
|||||||
totalChatCount = chatIds.length;
|
totalChatCount = chatIds.length;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const lastDialog = chats[chats.length - 1];
|
||||||
|
const lastMessageId = lastMessageByChatId[lastDialog?.id];
|
||||||
|
const nextOffsetId = lastMessageId;
|
||||||
|
const nextOffsetPeerId = lastDialog?.id;
|
||||||
|
const nextOffsetDate = messages.reverse()
|
||||||
|
.find((message) => message.chatId === lastDialog?.id && message.id === lastMessageId)?.date;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
chatIds,
|
chatIds,
|
||||||
chats,
|
chats,
|
||||||
@ -315,6 +347,9 @@ export async function fetchSavedChats({
|
|||||||
lastMessageByChatId,
|
lastMessageByChatId,
|
||||||
messages,
|
messages,
|
||||||
draftsById: {},
|
draftsById: {},
|
||||||
|
nextOffsetId,
|
||||||
|
nextOffsetPeerId,
|
||||||
|
nextOffsetDate,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -27,7 +27,7 @@ export {
|
|||||||
|
|
||||||
export {
|
export {
|
||||||
fetchMessages, fetchMessage, sendMessage, pinMessage, unpinAllMessages, deleteMessages, deleteHistory,
|
fetchMessages, fetchMessage, sendMessage, pinMessage, unpinAllMessages, deleteMessages, deleteHistory,
|
||||||
markMessageListRead, markMessagesRead, searchMessagesLocal, searchMessagesGlobal,
|
markMessageListRead, markMessagesRead, searchMessagesInChat, searchMessagesGlobal, searchHashtagPosts,
|
||||||
fetchWebPagePreview, editMessage, forwardMessages, loadPollOptionResults, sendPollVote, findFirstMessageIdAfterDate,
|
fetchWebPagePreview, editMessage, forwardMessages, loadPollOptionResults, sendPollVote, findFirstMessageIdAfterDate,
|
||||||
fetchPinnedMessages, fetchScheduledHistory, sendScheduledMessages, rescheduleMessage, deleteScheduledMessages,
|
fetchPinnedMessages, fetchScheduledHistory, sendScheduledMessages, rescheduleMessage, deleteScheduledMessages,
|
||||||
reportMessages, sendMessageAction, fetchSeenBy, fetchSponsoredMessages, viewSponsoredMessage, fetchSendAs,
|
reportMessages, sendMessageAction, fetchSeenBy, fetchSponsoredMessages, viewSponsoredMessage, fetchSendAs,
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import type {
|
|||||||
ApiSticker,
|
ApiSticker,
|
||||||
ApiStory,
|
ApiStory,
|
||||||
ApiStorySkipped,
|
ApiStorySkipped,
|
||||||
|
ApiUser,
|
||||||
ApiVideo,
|
ApiVideo,
|
||||||
MediaContent,
|
MediaContent,
|
||||||
OnApiUpdate,
|
OnApiUpdate,
|
||||||
@ -43,7 +44,7 @@ import {
|
|||||||
import { getEmojiOnlyCountForMessage } from '../../../global/helpers/getEmojiOnlyCountForMessage';
|
import { getEmojiOnlyCountForMessage } from '../../../global/helpers/getEmojiOnlyCountForMessage';
|
||||||
import { fetchFile } from '../../../util/files';
|
import { fetchFile } from '../../../util/files';
|
||||||
import { compact, split } from '../../../util/iteratees';
|
import { compact, split } from '../../../util/iteratees';
|
||||||
import { getMessageKey } from '../../../util/messageKey';
|
import { getMessageKey } from '../../../util/keys/messageKey';
|
||||||
import { getServerTimeOffset } from '../../../util/serverTime';
|
import { getServerTimeOffset } from '../../../util/serverTime';
|
||||||
import { interpolateArray } from '../../../util/waveform';
|
import { interpolateArray } from '../../../util/waveform';
|
||||||
import { buildApiChatFromPreview, buildApiSendAsPeerId } from '../apiBuilders/chats';
|
import { buildApiChatFromPreview, buildApiSendAsPeerId } from '../apiBuilders/chats';
|
||||||
@ -83,6 +84,7 @@ import {
|
|||||||
addEntitiesToLocalDb,
|
addEntitiesToLocalDb,
|
||||||
addMessageToLocalDb,
|
addMessageToLocalDb,
|
||||||
deserializeBytes,
|
deserializeBytes,
|
||||||
|
resolveMessageApiChatId,
|
||||||
} from '../helpers';
|
} from '../helpers';
|
||||||
import { processAffectedHistory, updateChannelState } from '../updates/updateManager';
|
import { processAffectedHistory, updateChannelState } from '../updates/updateManager';
|
||||||
import { dispatchThreadInfoUpdates } from '../updates/updater';
|
import { dispatchThreadInfoUpdates } from '../updates/updater';
|
||||||
@ -101,6 +103,16 @@ type TranslateTextParams = ({
|
|||||||
toLanguageCode: string;
|
toLanguageCode: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type SearchResults = {
|
||||||
|
messages: ApiMessage[];
|
||||||
|
users: ApiUser[];
|
||||||
|
chats: ApiChat[];
|
||||||
|
totalCount: number;
|
||||||
|
nextOffsetRate?: number;
|
||||||
|
nextOffsetPeerId?: string;
|
||||||
|
nextOffsetId?: number;
|
||||||
|
};
|
||||||
|
|
||||||
let onUpdate: OnApiUpdate;
|
let onUpdate: OnApiUpdate;
|
||||||
|
|
||||||
export function init(_onUpdate: OnApiUpdate) {
|
export function init(_onUpdate: OnApiUpdate) {
|
||||||
@ -1135,8 +1147,8 @@ export async function fetchDiscussionMessage({
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function searchMessagesLocal({
|
export async function searchMessagesInChat({
|
||||||
chat, isSavedDialog, savedTag, type, query, threadId, minDate, maxDate, ...pagination
|
chat, isSavedDialog, savedTag, type, query = '', threadId, minDate, maxDate, ...pagination
|
||||||
}: {
|
}: {
|
||||||
chat: ApiChat;
|
chat: ApiChat;
|
||||||
isSavedDialog?: boolean;
|
isSavedDialog?: boolean;
|
||||||
@ -1149,7 +1161,7 @@ export async function searchMessagesLocal({
|
|||||||
limit: number;
|
limit: number;
|
||||||
minDate?: number;
|
minDate?: number;
|
||||||
maxDate?: number;
|
maxDate?: number;
|
||||||
}) {
|
}): Promise<SearchResults | undefined> {
|
||||||
let filter;
|
let filter;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'media':
|
case 'media':
|
||||||
@ -1184,7 +1196,7 @@ export async function searchMessagesLocal({
|
|||||||
savedReaction: savedTag && [buildInputReaction(savedTag)],
|
savedReaction: savedTag && [buildInputReaction(savedTag)],
|
||||||
topMsgId: threadId !== MAIN_THREAD_ID && !isSavedDialog ? Number(threadId) : undefined,
|
topMsgId: threadId !== MAIN_THREAD_ID && !isSavedDialog ? Number(threadId) : undefined,
|
||||||
filter,
|
filter,
|
||||||
q: query || '',
|
q: query,
|
||||||
minDate,
|
minDate,
|
||||||
maxDate,
|
maxDate,
|
||||||
...pagination,
|
...pagination,
|
||||||
@ -1228,15 +1240,17 @@ export async function searchMessagesLocal({
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function searchMessagesGlobal({
|
export async function searchMessagesGlobal({
|
||||||
query, offsetRate = 0, limit, type = 'text', minDate, maxDate,
|
query, offsetRate = 0, offsetPeer, offsetId, limit, type = 'text', minDate, maxDate,
|
||||||
}: {
|
}: {
|
||||||
query: string;
|
query: string;
|
||||||
offsetRate?: number;
|
offsetRate?: number;
|
||||||
|
offsetPeer?: ApiPeer;
|
||||||
|
offsetId?: number;
|
||||||
limit: number;
|
limit: number;
|
||||||
type?: ApiGlobalMessageSearchType;
|
type?: ApiGlobalMessageSearchType;
|
||||||
minDate?: number;
|
minDate?: number;
|
||||||
maxDate?: number;
|
maxDate?: number;
|
||||||
}) {
|
}): Promise<SearchResults | undefined> {
|
||||||
let filter;
|
let filter;
|
||||||
switch (type) {
|
switch (type) {
|
||||||
case 'media':
|
case 'media':
|
||||||
@ -1264,10 +1278,13 @@ export async function searchMessagesGlobal({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const peer = (offsetPeer && buildInputPeer(offsetPeer.id, offsetPeer.accessHash)) || new GramJs.InputPeerEmpty();
|
||||||
|
|
||||||
const result = await invokeRequest(new GramJs.messages.SearchGlobal({
|
const result = await invokeRequest(new GramJs.messages.SearchGlobal({
|
||||||
q: query,
|
q: query,
|
||||||
offsetRate,
|
offsetRate,
|
||||||
offsetPeer: new GramJs.InputPeerEmpty(),
|
offsetPeer: peer,
|
||||||
|
offsetId,
|
||||||
broadcastsOnly: type === 'channels' || undefined,
|
broadcastsOnly: type === 'channels' || undefined,
|
||||||
limit,
|
limit,
|
||||||
filter,
|
filter,
|
||||||
@ -1284,11 +1301,7 @@ export async function searchMessagesGlobal({
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateLocalDb({
|
updateLocalDb(result);
|
||||||
chats: result.chats,
|
|
||||||
users: result.users,
|
|
||||||
messages: result.messages,
|
|
||||||
} as GramJs.messages.Messages);
|
|
||||||
|
|
||||||
const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean);
|
const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean);
|
||||||
const users = result.users.map(buildApiUser).filter(Boolean);
|
const users = result.users.map(buildApiUser).filter(Boolean);
|
||||||
@ -1296,21 +1309,77 @@ export async function searchMessagesGlobal({
|
|||||||
dispatchThreadInfoUpdates(result.messages);
|
dispatchThreadInfoUpdates(result.messages);
|
||||||
|
|
||||||
let totalCount = messages.length;
|
let totalCount = messages.length;
|
||||||
let nextRate: number | undefined;
|
|
||||||
if (result instanceof GramJs.messages.MessagesSlice || result instanceof GramJs.messages.ChannelMessages) {
|
if (result instanceof GramJs.messages.MessagesSlice || result instanceof GramJs.messages.ChannelMessages) {
|
||||||
totalCount = result.count;
|
totalCount = result.count;
|
||||||
|
} else {
|
||||||
if (messages.length) {
|
totalCount = result.messages.length;
|
||||||
nextRate = messages[messages.length - 1].id;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const lastMessage = result.messages[result.messages.length - 1];
|
||||||
|
const nextOffsetPeerId = resolveMessageApiChatId(lastMessage);
|
||||||
|
const nextOffsetRate = 'nextRate' in result && result.nextRate ? result.nextRate : undefined;
|
||||||
|
const nextOffsetId = lastMessage?.id;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
messages,
|
messages,
|
||||||
users,
|
users,
|
||||||
chats,
|
chats,
|
||||||
totalCount,
|
totalCount,
|
||||||
nextRate: 'nextRate' in result && result.nextRate ? result.nextRate : nextRate,
|
nextOffsetRate,
|
||||||
|
nextOffsetPeerId,
|
||||||
|
nextOffsetId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function searchHashtagPosts({
|
||||||
|
hashtag, offsetRate, offsetPeer, offsetId, limit,
|
||||||
|
}: {
|
||||||
|
hashtag: string;
|
||||||
|
offsetRate?: number;
|
||||||
|
offsetPeer?: ApiPeer;
|
||||||
|
offsetId?: number;
|
||||||
|
limit?: number;
|
||||||
|
}): Promise<SearchResults | undefined> {
|
||||||
|
const peer = (offsetPeer && buildInputPeer(offsetPeer.id, offsetPeer.accessHash)) || new GramJs.InputPeerEmpty();
|
||||||
|
const result = await invokeRequest(new GramJs.channels.SearchPosts({
|
||||||
|
hashtag,
|
||||||
|
offsetRate,
|
||||||
|
offsetId,
|
||||||
|
offsetPeer: peer,
|
||||||
|
limit,
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (!result || result instanceof GramJs.messages.MessagesNotModified) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateLocalDb(result);
|
||||||
|
|
||||||
|
const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean);
|
||||||
|
const users = result.users.map(buildApiUser).filter(Boolean);
|
||||||
|
const messages = result.messages.map(buildApiMessage).filter(Boolean);
|
||||||
|
dispatchThreadInfoUpdates(result.messages);
|
||||||
|
|
||||||
|
let totalCount = messages.length;
|
||||||
|
if (result instanceof GramJs.messages.MessagesSlice || result instanceof GramJs.messages.ChannelMessages) {
|
||||||
|
totalCount = result.count;
|
||||||
|
} else {
|
||||||
|
totalCount = result.messages.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastMessage = result.messages[result.messages.length - 1];
|
||||||
|
const nextOffsetPeerId = resolveMessageApiChatId(lastMessage);
|
||||||
|
const nextOffsetRate = 'nextRate' in result && result.nextRate ? result.nextRate : undefined;
|
||||||
|
const nextOffsetId = lastMessage?.id;
|
||||||
|
|
||||||
|
return {
|
||||||
|
messages,
|
||||||
|
users,
|
||||||
|
chats,
|
||||||
|
totalCount,
|
||||||
|
nextOffsetRate,
|
||||||
|
nextOffsetPeerId,
|
||||||
|
nextOffsetId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -22,7 +22,7 @@ import {
|
|||||||
import { addEntitiesToLocalDb, addPhotoToLocalDb, addUserToLocalDb } from '../helpers';
|
import { addEntitiesToLocalDb, addPhotoToLocalDb, addUserToLocalDb } from '../helpers';
|
||||||
import localDb from '../localDb';
|
import localDb from '../localDb';
|
||||||
import { invokeRequest } from './client';
|
import { invokeRequest } from './client';
|
||||||
import { searchMessagesLocal } from './messages';
|
import { searchMessagesInChat } from './messages';
|
||||||
|
|
||||||
let onUpdate: OnApiUpdate;
|
let onUpdate: OnApiUpdate;
|
||||||
|
|
||||||
@ -139,7 +139,7 @@ export async function fetchTopUsers() {
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
const users = topPeers.users.map(buildApiUser).filter((user) => Boolean(user) && !user.isSelf) as ApiUser[];
|
const users = topPeers.users.map(buildApiUser).filter((user): user is ApiUser => Boolean(user) && !user.isSelf);
|
||||||
const ids = users.map(({ id }) => id);
|
const ids = users.map(({ id }) => id);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -295,7 +295,7 @@ export async function fetchProfilePhotos({
|
|||||||
|
|
||||||
if (chat?.isRestricted) return undefined;
|
if (chat?.isRestricted) return undefined;
|
||||||
|
|
||||||
const result = await searchMessagesLocal({
|
const result = await searchMessagesInChat({
|
||||||
chat: chat!,
|
chat: chat!,
|
||||||
type: 'profilePhoto',
|
type: 'profilePhoto',
|
||||||
limit,
|
limit,
|
||||||
|
|||||||
@ -1271,3 +1271,5 @@
|
|||||||
"MenuInstallApp" = "Install App";
|
"MenuInstallApp" = "Install App";
|
||||||
"RemoveEffect" = "Remove effect";
|
"RemoveEffect" = "Remove effect";
|
||||||
"ReplyInPrivateMessage" = "Reply In Private Message";
|
"ReplyInPrivateMessage" = "Reply In Private Message";
|
||||||
|
"AriaSearchOlderResult" = "Focus next result";
|
||||||
|
"AriaSearchNewerResult" = "Focus previous result";
|
||||||
|
|||||||
@ -57,7 +57,7 @@ export { default as SponsoredMessageContextMenuContainer }
|
|||||||
export { default as StickerSetModal } from '../components/common/StickerSetModal';
|
export { default as StickerSetModal } from '../components/common/StickerSetModal';
|
||||||
export { default as CustomEmojiSetsModal } from '../components/common/CustomEmojiSetsModal';
|
export { default as CustomEmojiSetsModal } from '../components/common/CustomEmojiSetsModal';
|
||||||
export { default as HeaderMenuContainer } from '../components/middle/HeaderMenuContainer';
|
export { default as HeaderMenuContainer } from '../components/middle/HeaderMenuContainer';
|
||||||
export { default as MobileSearch } from '../components/middle/MobileSearch';
|
export { default as MiddleSearch } from '../components/middle/search/MiddleSearch';
|
||||||
export { default as ReactionPicker } from '../components/middle/message/reactions/ReactionPicker';
|
export { default as ReactionPicker } from '../components/middle/message/reactions/ReactionPicker';
|
||||||
|
|
||||||
export { default as AttachmentModal } from '../components/middle/composer/AttachmentModal';
|
export { default as AttachmentModal } from '../components/middle/composer/AttachmentModal';
|
||||||
@ -75,7 +75,6 @@ export { default as EmojiTooltip } from '../components/middle/composer/EmojiTool
|
|||||||
export { default as InlineBotTooltip } from '../components/middle/composer/InlineBotTooltip';
|
export { default as InlineBotTooltip } from '../components/middle/composer/InlineBotTooltip';
|
||||||
export { default as SendAsMenu } from '../components/middle/composer/SendAsMenu';
|
export { default as SendAsMenu } from '../components/middle/composer/SendAsMenu';
|
||||||
|
|
||||||
export { default as RightSearch } from '../components/right/RightSearch';
|
|
||||||
export { default as StickerSearch } from '../components/right/StickerSearch';
|
export { default as StickerSearch } from '../components/right/StickerSearch';
|
||||||
export { default as GifSearch } from '../components/right/GifSearch';
|
export { default as GifSearch } from '../components/right/GifSearch';
|
||||||
export { default as Statistics } from '../components/right/statistics/Statistics';
|
export { default as Statistics } from '../components/right/statistics/Statistics';
|
||||||
|
|||||||
@ -4,8 +4,7 @@ import React, { memo, useMemo, useRef } from '../../lib/teact/teact';
|
|||||||
import { getActions } from '../../global';
|
import { getActions } from '../../global';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ApiChat, ApiPeer, ApiPhoto, ApiUser,
|
ApiPeer, ApiPhoto, ApiWebDocument,
|
||||||
ApiWebDocument,
|
|
||||||
} from '../../api/types';
|
} from '../../api/types';
|
||||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||||
import type { CustomPeer, StoryViewerOrigin } from '../../types';
|
import type { CustomPeer, StoryViewerOrigin } from '../../types';
|
||||||
@ -22,6 +21,8 @@ import {
|
|||||||
isAnonymousForwardsChat,
|
isAnonymousForwardsChat,
|
||||||
isChatWithRepliesBot,
|
isChatWithRepliesBot,
|
||||||
isDeletedUser,
|
isDeletedUser,
|
||||||
|
isPeerChat,
|
||||||
|
isPeerUser,
|
||||||
isUserId,
|
isUserId,
|
||||||
} from '../../global/helpers';
|
} from '../../global/helpers';
|
||||||
import buildClassName, { createClassNameBuilder } from '../../util/buildClassName';
|
import buildClassName, { createClassNameBuilder } from '../../util/buildClassName';
|
||||||
@ -102,9 +103,8 @@ const Avatar: FC<OwnProps> = ({
|
|||||||
const videoLoopCountRef = useRef(0);
|
const videoLoopCountRef = useRef(0);
|
||||||
const isCustomPeer = peer && 'isCustomPeer' in peer;
|
const isCustomPeer = peer && 'isCustomPeer' in peer;
|
||||||
const realPeer = peer && !isCustomPeer ? peer : undefined;
|
const realPeer = peer && !isCustomPeer ? peer : undefined;
|
||||||
const isPeerChat = realPeer && 'title' in realPeer;
|
const user = realPeer && isPeerUser(realPeer) ? realPeer : undefined;
|
||||||
const user = peer && !isPeerChat ? peer as ApiUser : undefined;
|
const chat = realPeer && isPeerChat(realPeer) ? realPeer : undefined;
|
||||||
const chat = peer && isPeerChat ? peer as ApiChat : undefined;
|
|
||||||
const isDeleted = user && isDeletedUser(user);
|
const isDeleted = user && isDeletedUser(user);
|
||||||
const isReplies = realPeer && isChatWithRepliesBot(realPeer.id);
|
const isReplies = realPeer && isChatWithRepliesBot(realPeer.id);
|
||||||
const isAnonymousForwards = realPeer && isAnonymousForwardsChat(realPeer.id);
|
const isAnonymousForwards = realPeer && isAnonymousForwardsChat(realPeer.id);
|
||||||
|
|||||||
@ -3,14 +3,14 @@ import React, { memo, useMemo } from '../../lib/teact/teact';
|
|||||||
import { getActions } from '../../global';
|
import { getActions } from '../../global';
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
ApiChat, ApiPeer, ApiUser,
|
ApiPeer,
|
||||||
} from '../../api/types';
|
} from '../../api/types';
|
||||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||||
import type { CustomPeer } from '../../types';
|
import type { CustomPeer } from '../../types';
|
||||||
|
|
||||||
import { EMOJI_STATUS_LOOP_LIMIT } from '../../config';
|
import { EMOJI_STATUS_LOOP_LIMIT } from '../../config';
|
||||||
import {
|
import {
|
||||||
getChatTitle, getUserFullName, isAnonymousForwardsChat, isChatWithRepliesBot, isUserId,
|
getChatTitle, getUserFullName, isAnonymousForwardsChat, isChatWithRepliesBot, isPeerUser,
|
||||||
} from '../../global/helpers';
|
} from '../../global/helpers';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
import { copyTextToClipboard } from '../../util/clipboard';
|
import { copyTextToClipboard } from '../../util/clipboard';
|
||||||
@ -62,9 +62,9 @@ const FullNameTitle: FC<OwnProps> = ({
|
|||||||
const { showNotification } = getActions();
|
const { showNotification } = getActions();
|
||||||
const realPeer = 'id' in peer ? peer : undefined;
|
const realPeer = 'id' in peer ? peer : undefined;
|
||||||
const customPeer = 'isCustomPeer' in peer ? peer : undefined;
|
const customPeer = 'isCustomPeer' in peer ? peer : undefined;
|
||||||
const isUser = realPeer && isUserId(realPeer.id);
|
const isUser = realPeer && isPeerUser(realPeer);
|
||||||
const title = realPeer && (isUser ? getUserFullName(realPeer as ApiUser) : getChatTitle(lang, realPeer as ApiChat));
|
const title = realPeer && (isUser ? getUserFullName(realPeer) : getChatTitle(lang, realPeer));
|
||||||
const isPremium = isUser && (peer as ApiUser).isPremium;
|
const isPremium = isUser && realPeer.isPremium;
|
||||||
|
|
||||||
const handleTitleClick = useLastCallback((e) => {
|
const handleTitleClick = useLastCallback((e) => {
|
||||||
if (!title || !canCopyTitle) {
|
if (!title || !canCopyTitle) {
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import type { FC } from '../../lib/teact/teact';
|
|
||||||
import React, { memo } from '../../lib/teact/teact';
|
import React, { memo } from '../../lib/teact/teact';
|
||||||
|
|
||||||
import type { ApiMessage, ApiMessageOutgoingStatus } from '../../api/types';
|
import type { ApiMessage, ApiMessageOutgoingStatus } from '../../api/types';
|
||||||
|
|
||||||
|
import buildClassName from '../../util/buildClassName';
|
||||||
import { formatPastTimeShort } from '../../util/dates/dateFormat';
|
import { formatPastTimeShort } from '../../util/dates/dateFormat';
|
||||||
|
|
||||||
import useOldLang from '../../hooks/useOldLang';
|
import useOldLang from '../../hooks/useOldLang';
|
||||||
@ -12,17 +12,20 @@ import MessageOutgoingStatus from './MessageOutgoingStatus';
|
|||||||
import './LastMessageMeta.scss';
|
import './LastMessageMeta.scss';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
|
className?: string;
|
||||||
message: ApiMessage;
|
message: ApiMessage;
|
||||||
outgoingStatus?: ApiMessageOutgoingStatus;
|
outgoingStatus?: ApiMessageOutgoingStatus;
|
||||||
draftDate?: number;
|
draftDate?: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const LastMessageMeta: FC<OwnProps> = ({ message, outgoingStatus, draftDate }) => {
|
const LastMessageMeta = ({
|
||||||
|
className, message, outgoingStatus, draftDate,
|
||||||
|
}: OwnProps) => {
|
||||||
const lang = useOldLang();
|
const lang = useOldLang();
|
||||||
|
|
||||||
const shouldUseDraft = draftDate && draftDate > message.date;
|
const shouldUseDraft = draftDate && draftDate > message.date;
|
||||||
return (
|
return (
|
||||||
<div className="LastMessageMeta">
|
<div className={buildClassName('LastMessageMeta', className)}>
|
||||||
{outgoingStatus && !shouldUseDraft && (
|
{outgoingStatus && !shouldUseDraft && (
|
||||||
<MessageOutgoingStatus status={outgoingStatus} />
|
<MessageOutgoingStatus status={outgoingStatus} />
|
||||||
)}
|
)}
|
||||||
|
|||||||
@ -2,7 +2,6 @@ import React, { memo } from '../../lib/teact/teact';
|
|||||||
|
|
||||||
import type { ApiFormattedText, ApiMessage } from '../../api/types';
|
import type { ApiFormattedText, ApiMessage } from '../../api/types';
|
||||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||||
import type { LangFn } from '../../hooks/useOldLang';
|
|
||||||
import { ApiMessageEntityTypes } from '../../api/types';
|
import { ApiMessageEntityTypes } from '../../api/types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -18,42 +17,43 @@ import {
|
|||||||
import trimText from '../../util/trimText';
|
import trimText from '../../util/trimText';
|
||||||
import renderText from './helpers/renderText';
|
import renderText from './helpers/renderText';
|
||||||
|
|
||||||
|
import useOldLang from '../../hooks/useOldLang';
|
||||||
|
|
||||||
import MessageText from './MessageText';
|
import MessageText from './MessageText';
|
||||||
|
|
||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
lang: LangFn;
|
|
||||||
message: ApiMessage;
|
message: ApiMessage;
|
||||||
translatedText?: ApiFormattedText;
|
translatedText?: ApiFormattedText;
|
||||||
noEmoji?: boolean;
|
noEmoji?: boolean;
|
||||||
highlight?: string;
|
highlight?: string;
|
||||||
truncateLength?: number;
|
truncateLength?: number;
|
||||||
observeIntersectionForLoading?: ObserveFn;
|
|
||||||
observeIntersectionForPlaying?: ObserveFn;
|
|
||||||
withTranslucentThumbs?: boolean;
|
withTranslucentThumbs?: boolean;
|
||||||
inChatList?: boolean;
|
inChatList?: boolean;
|
||||||
emojiSize?: number;
|
emojiSize?: number;
|
||||||
|
observeIntersectionForLoading?: ObserveFn;
|
||||||
|
observeIntersectionForPlaying?: ObserveFn;
|
||||||
}
|
}
|
||||||
|
|
||||||
function MessageSummary({
|
function MessageSummary({
|
||||||
lang,
|
|
||||||
message,
|
message,
|
||||||
translatedText,
|
translatedText,
|
||||||
noEmoji = false,
|
noEmoji = false,
|
||||||
highlight,
|
highlight,
|
||||||
truncateLength = TRUNCATED_SUMMARY_LENGTH,
|
truncateLength = TRUNCATED_SUMMARY_LENGTH,
|
||||||
observeIntersectionForLoading,
|
|
||||||
observeIntersectionForPlaying,
|
|
||||||
withTranslucentThumbs = false,
|
withTranslucentThumbs = false,
|
||||||
inChatList = false,
|
inChatList = false,
|
||||||
emojiSize,
|
emojiSize,
|
||||||
|
observeIntersectionForLoading,
|
||||||
|
observeIntersectionForPlaying,
|
||||||
}: OwnProps) {
|
}: OwnProps) {
|
||||||
|
const lang = useOldLang();
|
||||||
const { text, entities } = extractMessageText(message, inChatList) || {};
|
const { text, entities } = extractMessageText(message, inChatList) || {};
|
||||||
const hasSpoilers = entities?.some((e) => e.type === ApiMessageEntityTypes.Spoiler);
|
const hasSpoilers = entities?.some((e) => e.type === ApiMessageEntityTypes.Spoiler);
|
||||||
const hasCustomEmoji = entities?.some((e) => e.type === ApiMessageEntityTypes.CustomEmoji);
|
const hasCustomEmoji = entities?.some((e) => e.type === ApiMessageEntityTypes.CustomEmoji);
|
||||||
const hasPoll = Boolean(getMessagePoll(message));
|
const hasPoll = Boolean(getMessagePoll(message));
|
||||||
|
|
||||||
if ((!text || (!hasSpoilers && !hasCustomEmoji)) && !hasPoll) {
|
if ((!text || (!hasSpoilers && !hasCustomEmoji)) && !hasPoll) {
|
||||||
const summaryText = translatedText?.text || getMessageSummaryText(lang, message, noEmoji);
|
const summaryText = translatedText?.text || getMessageSummaryText(lang, message, noEmoji, truncateLength);
|
||||||
const trimmedText = trimText(summaryText, truncateLength);
|
const trimmedText = trimText(summaryText, truncateLength);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|||||||
@ -4,8 +4,8 @@
|
|||||||
background: var(--color-chat-hover);
|
background: var(--color-chat-hover);
|
||||||
height: 2rem;
|
height: 2rem;
|
||||||
min-width: 2rem;
|
min-width: 2rem;
|
||||||
margin-left: 0.5rem;
|
margin-left: 0.25rem;
|
||||||
margin-bottom: 0.5rem;
|
margin-right: 0.25rem;
|
||||||
padding-right: 1rem;
|
padding-right: 1rem;
|
||||||
border-radius: 1rem;
|
border-radius: 1rem;
|
||||||
cursor: var(--custom-cursor, pointer);
|
cursor: var(--custom-cursor, pointer);
|
||||||
@ -55,15 +55,6 @@
|
|||||||
max-width: unset;
|
max-width: unset;
|
||||||
}
|
}
|
||||||
|
|
||||||
.SearchInput & {
|
|
||||||
flex: 1 0 auto;
|
|
||||||
position: relative;
|
|
||||||
top: 0.25rem;
|
|
||||||
left: -0.125rem;
|
|
||||||
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.Avatar,
|
.Avatar,
|
||||||
.item-icon {
|
.item-icon {
|
||||||
width: 2rem;
|
width: 2rem;
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type { FC, TeactNode } from '../../lib/teact/teact';
|
import type { TeactNode } from '../../lib/teact/teact';
|
||||||
import React, { memo } from '../../lib/teact/teact';
|
import React, { memo } from '../../lib/teact/teact';
|
||||||
import { withGlobal } from '../../global';
|
import { withGlobal } from '../../global';
|
||||||
|
|
||||||
@ -19,19 +19,21 @@ import Icon from './icons/Icon';
|
|||||||
|
|
||||||
import './PickerSelectedItem.scss';
|
import './PickerSelectedItem.scss';
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps<T = undefined> = {
|
||||||
|
// eslint-disable-next-line react/no-unused-prop-types
|
||||||
peerId?: string;
|
peerId?: string;
|
||||||
|
// eslint-disable-next-line react/no-unused-prop-types
|
||||||
|
forceShowSelf?: boolean;
|
||||||
customPeer?: CustomPeer;
|
customPeer?: CustomPeer;
|
||||||
icon?: IconName;
|
icon?: IconName;
|
||||||
title?: string;
|
title?: string;
|
||||||
isMinimized?: boolean;
|
isMinimized?: boolean;
|
||||||
canClose?: boolean;
|
canClose?: boolean;
|
||||||
forceShowSelf?: boolean;
|
|
||||||
clickArg?: any;
|
|
||||||
className?: string;
|
className?: string;
|
||||||
fluid?: boolean;
|
fluid?: boolean;
|
||||||
withPeerColors?: boolean;
|
withPeerColors?: boolean;
|
||||||
onClick: (arg: any) => void;
|
clickArg: T;
|
||||||
|
onClick: (arg: T) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
@ -40,7 +42,8 @@ type StateProps = {
|
|||||||
isSavedMessages?: boolean;
|
isSavedMessages?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const PickerSelectedItem: FC<OwnProps & StateProps> = ({
|
// eslint-disable-next-line @typescript-eslint/comma-dangle
|
||||||
|
const PickerSelectedItem = <T,>({
|
||||||
icon,
|
icon,
|
||||||
title,
|
title,
|
||||||
isMinimized,
|
isMinimized,
|
||||||
@ -54,7 +57,7 @@ const PickerSelectedItem: FC<OwnProps & StateProps> = ({
|
|||||||
isSavedMessages,
|
isSavedMessages,
|
||||||
withPeerColors,
|
withPeerColors,
|
||||||
onClick,
|
onClick,
|
||||||
}) => {
|
}: OwnProps<T> & StateProps) => {
|
||||||
const lang = useOldLang();
|
const lang = useOldLang();
|
||||||
|
|
||||||
let iconElement: TeactNode | undefined;
|
let iconElement: TeactNode | undefined;
|
||||||
@ -82,7 +85,7 @@ const PickerSelectedItem: FC<OwnProps & StateProps> = ({
|
|||||||
? getUserFirstOrLastName(user)
|
? getUserFirstOrLastName(user)
|
||||||
: getChatTitle(lang, chat, isSavedMessages));
|
: getChatTitle(lang, chat, isSavedMessages));
|
||||||
|
|
||||||
titleText = name ? renderText(name) : undefined;
|
titleText = title || (name ? renderText(name) : undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullClassName = buildClassName(
|
const fullClassName = buildClassName(
|
||||||
@ -133,4 +136,4 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
isSavedMessages,
|
isSavedMessages,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(PickerSelectedItem));
|
)(PickerSelectedItem)) as typeof PickerSelectedItem;
|
||||||
|
|||||||
@ -152,7 +152,6 @@ const EmbeddedMessage: FC<OwnProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<MessageSummary
|
<MessageSummary
|
||||||
lang={lang}
|
|
||||||
message={message}
|
message={message}
|
||||||
noEmoji={Boolean(mediaThumbnail)}
|
noEmoji={Boolean(mediaThumbnail)}
|
||||||
translatedText={translatedText}
|
translatedText={translatedText}
|
||||||
|
|||||||
@ -254,7 +254,6 @@ function renderMessageContent(
|
|||||||
|
|
||||||
const messageSummary = (
|
const messageSummary = (
|
||||||
<MessageSummary
|
<MessageSummary
|
||||||
lang={lang}
|
|
||||||
message={message}
|
message={message}
|
||||||
truncateLength={MAX_LENGTH}
|
truncateLength={MAX_LENGTH}
|
||||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||||
|
|||||||
@ -12,7 +12,7 @@ import {
|
|||||||
getMessageSummaryText,
|
getMessageSummaryText,
|
||||||
TRUNCATED_SUMMARY_LENGTH,
|
TRUNCATED_SUMMARY_LENGTH,
|
||||||
} from '../../../global/helpers/messageSummary';
|
} from '../../../global/helpers/messageSummary';
|
||||||
import { getMessageKey } from '../../../util/messageKey';
|
import { getMessageKey } from '../../../util/keys/messageKey';
|
||||||
import trimText from '../../../util/trimText';
|
import trimText from '../../../util/trimText';
|
||||||
import renderText from './renderText';
|
import renderText from './renderText';
|
||||||
import { renderTextWithEntities } from './renderTextWithEntities';
|
import { renderTextWithEntities } from './renderTextWithEntities';
|
||||||
|
|||||||
@ -656,8 +656,7 @@ function handleBotCommandClick(e: React.MouseEvent<HTMLAnchorElement>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleHashtagClick(e: React.MouseEvent<HTMLAnchorElement>) {
|
function handleHashtagClick(e: React.MouseEvent<HTMLAnchorElement>) {
|
||||||
getActions().setLocalTextSearchQuery({ query: e.currentTarget.innerText });
|
getActions().searchHashtag({ hashtag: e.currentTarget.innerText });
|
||||||
getActions().searchTextMessagesLocal();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleCodeClick(e: React.MouseEvent<HTMLElement>) {
|
function handleCodeClick(e: React.MouseEvent<HTMLElement>) {
|
||||||
|
|||||||
@ -562,7 +562,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
const {
|
const {
|
||||||
globalSearch: {
|
globalSearch: {
|
||||||
query,
|
query,
|
||||||
date,
|
minDate,
|
||||||
},
|
},
|
||||||
shouldSkipHistoryAnimations,
|
shouldSkipHistoryAnimations,
|
||||||
activeChatFolder,
|
activeChatFolder,
|
||||||
@ -589,7 +589,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
searchQuery: query,
|
searchQuery: query,
|
||||||
searchDate: date,
|
searchDate: minDate,
|
||||||
isFirstChatFolderActive: activeChatFolder === 0,
|
isFirstChatFolderActive: activeChatFolder === 0,
|
||||||
shouldSkipHistoryAnimations,
|
shouldSkipHistoryAnimations,
|
||||||
currentUserId,
|
currentUserId,
|
||||||
|
|||||||
@ -141,4 +141,10 @@
|
|||||||
pointer-events: none;
|
pointer-events: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.left-search-picker-item {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
font-weight: 500;
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -222,18 +222,22 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
|||||||
<PickerSelectedItem
|
<PickerSelectedItem
|
||||||
icon="calendar"
|
icon="calendar"
|
||||||
title={selectedSearchDate}
|
title={selectedSearchDate}
|
||||||
|
fluid
|
||||||
canClose
|
canClose
|
||||||
isMinimized={Boolean(globalSearchChatId)}
|
isMinimized={Boolean(globalSearchChatId)}
|
||||||
className="search-date"
|
className="left-search-picker-item search-date"
|
||||||
onClick={setGlobalSearchDate}
|
onClick={setGlobalSearchDate}
|
||||||
clickArg={CLEAR_DATE_SEARCH_PARAM}
|
clickArg={CLEAR_DATE_SEARCH_PARAM}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{globalSearchChatId && (
|
{globalSearchChatId && (
|
||||||
<PickerSelectedItem
|
<PickerSelectedItem
|
||||||
|
className="left-search-picker-item"
|
||||||
peerId={globalSearchChatId}
|
peerId={globalSearchChatId}
|
||||||
onClick={setGlobalSearchChatId}
|
onClick={setGlobalSearchChatId}
|
||||||
|
fluid
|
||||||
canClose
|
canClose
|
||||||
|
isMinimized
|
||||||
clickArg={CLEAR_CHAT_SEARCH_PARAM}
|
clickArg={CLEAR_CHAT_SEARCH_PARAM}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
@ -269,7 +273,7 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
<SearchInput
|
<SearchInput
|
||||||
inputId="telegram-search-input"
|
inputId="telegram-search-input"
|
||||||
parentContainerClassName="LeftSearch"
|
resultsItemSelector=".LeftSearch .ListItem-button"
|
||||||
className={buildClassName(
|
className={buildClassName(
|
||||||
(globalSearchChatId || searchDate) ? 'with-picker-item' : undefined,
|
(globalSearchChatId || searchDate) ? 'with-picker-item' : undefined,
|
||||||
shouldHideSearch && 'SearchInput--hidden',
|
shouldHideSearch && 'SearchInput--hidden',
|
||||||
@ -324,7 +328,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
(global): StateProps => {
|
(global): StateProps => {
|
||||||
const tabState = selectTabState(global);
|
const tabState = selectTabState(global);
|
||||||
const {
|
const {
|
||||||
query: searchQuery, fetchingStatus, chatId, date,
|
query: searchQuery, fetchingStatus, chatId, minDate,
|
||||||
} = tabState.globalSearch;
|
} = tabState.globalSearch;
|
||||||
const {
|
const {
|
||||||
connectionState, isSyncing, isFetchingDifference,
|
connectionState, isSyncing, isFetchingDifference,
|
||||||
@ -335,7 +339,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
searchQuery,
|
searchQuery,
|
||||||
isLoading: fetchingStatus ? Boolean(fetchingStatus.chats || fetchingStatus.messages) : false,
|
isLoading: fetchingStatus ? Boolean(fetchingStatus.chats || fetchingStatus.messages) : false,
|
||||||
globalSearchChatId: chatId,
|
globalSearchChatId: chatId,
|
||||||
searchDate: date,
|
searchDate: minDate,
|
||||||
theme: selectTheme(global),
|
theme: selectTheme(global),
|
||||||
connectionState,
|
connectionState,
|
||||||
isSyncing,
|
isSyncing,
|
||||||
|
|||||||
@ -8,7 +8,6 @@ import type {
|
|||||||
} from '../../../../api/types';
|
} from '../../../../api/types';
|
||||||
import type { ApiDraft } from '../../../../global/types';
|
import type { ApiDraft } from '../../../../global/types';
|
||||||
import type { ObserveFn } from '../../../../hooks/useIntersectionObserver';
|
import type { ObserveFn } from '../../../../hooks/useIntersectionObserver';
|
||||||
import type { LangFn } from '../../../../hooks/useOldLang';
|
|
||||||
|
|
||||||
import { ANIMATION_END_DELAY, CHAT_HEIGHT_PX } from '../../../../config';
|
import { ANIMATION_END_DELAY, CHAT_HEIGHT_PX } from '../../../../config';
|
||||||
import { requestMutation } from '../../../../lib/fasterdom/fasterdom';
|
import { requestMutation } from '../../../../lib/fasterdom/fasterdom';
|
||||||
@ -177,7 +176,7 @@ export default function useChatListEntry({
|
|||||||
)}
|
)}
|
||||||
{!isSavedDialog && lastMessage.forwardInfo && (<i className="icon icon-share-filled chat-prefix-icon" />)}
|
{!isSavedDialog && lastMessage.forwardInfo && (<i className="icon icon-share-filled chat-prefix-icon" />)}
|
||||||
{lastMessage.replyInfo?.type === 'story' && (<i className="icon icon-story-reply chat-prefix-icon" />)}
|
{lastMessage.replyInfo?.type === 'story' && (<i className="icon icon-story-reply chat-prefix-icon" />)}
|
||||||
{renderSummary(lang, lastMessage, observeIntersection, mediaBlobUrl || mediaThumbnail, isRoundVideo)}
|
{renderSummary(lastMessage, observeIntersection, mediaBlobUrl || mediaThumbnail, isRoundVideo)}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
}, [
|
}, [
|
||||||
@ -243,11 +242,10 @@ export default function useChatListEntry({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function renderSummary(
|
function renderSummary(
|
||||||
lang: LangFn, message: ApiMessage, observeIntersection?: ObserveFn, blobUrl?: string, isRoundVideo?: boolean,
|
message: ApiMessage, observeIntersection?: ObserveFn, blobUrl?: string, isRoundVideo?: boolean,
|
||||||
) {
|
) {
|
||||||
const messageSummary = (
|
const messageSummary = (
|
||||||
<MessageSummary
|
<MessageSummary
|
||||||
lang={lang}
|
|
||||||
message={message}
|
message={message}
|
||||||
noEmoji={Boolean(blobUrl)}
|
noEmoji={Boolean(blobUrl)}
|
||||||
observeIntersectionForLoading={observeIntersection}
|
observeIntersectionForLoading={observeIntersection}
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import { SLIDE_TRANSITION_DURATION } from '../../../config';
|
|||||||
import { getIsDownloading, getMessageDownloadableMedia } from '../../../global/helpers';
|
import { getIsDownloading, getMessageDownloadableMedia } from '../../../global/helpers';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import { formatMonthAndYear, toYearMonth } from '../../../util/dates/dateFormat';
|
import { formatMonthAndYear, toYearMonth } from '../../../util/dates/dateFormat';
|
||||||
|
import { parseSearchResultKey } from '../../../util/keys/searchResultKey';
|
||||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||||
import { throttle } from '../../../util/schedulers';
|
import { throttle } from '../../../util/schedulers';
|
||||||
import { createMapStateToProps } from './helpers/createMapStateToProps';
|
import { createMapStateToProps } from './helpers/createMapStateToProps';
|
||||||
@ -66,9 +67,9 @@ const AudioResults: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return foundIds.map((id) => {
|
return foundIds.map((id) => {
|
||||||
const [chatId, messageId] = id.split('_');
|
const [chatId, messageId] = parseSearchResultKey(id);
|
||||||
|
|
||||||
return globalMessagesByChatId[chatId]?.byId[Number(messageId)];
|
return globalMessagesByChatId[chatId]?.byId[messageId];
|
||||||
}).filter(Boolean);
|
}).filter(Boolean);
|
||||||
}, [globalMessagesByChatId, foundIds]);
|
}, [globalMessagesByChatId, foundIds]);
|
||||||
|
|
||||||
|
|||||||
@ -6,6 +6,7 @@ import type { ApiChat, ApiMessage } from '../../../api/types';
|
|||||||
import { LoadMoreDirection } from '../../../types';
|
import { LoadMoreDirection } from '../../../types';
|
||||||
|
|
||||||
import { selectTabState } from '../../../global/selectors';
|
import { selectTabState } from '../../../global/selectors';
|
||||||
|
import { parseSearchResultKey, type SearchResultKey } from '../../../util/keys/searchResultKey';
|
||||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||||
import { throttle } from '../../../util/schedulers';
|
import { throttle } from '../../../util/schedulers';
|
||||||
import { renderMessageSummary } from '../../common/helpers/renderMessageText';
|
import { renderMessageSummary } from '../../common/helpers/renderMessageText';
|
||||||
@ -28,7 +29,7 @@ export type OwnProps = {
|
|||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
currentUserId?: string;
|
currentUserId?: string;
|
||||||
foundIds?: string[];
|
foundIds?: SearchResultKey[];
|
||||||
globalMessagesByChatId?: Record<string, { byId: Record<number, ApiMessage> }>;
|
globalMessagesByChatId?: Record<string, { byId: Record<number, ApiMessage> }>;
|
||||||
chatsById: Record<string, ApiChat>;
|
chatsById: Record<string, ApiChat>;
|
||||||
fetchingStatus?: { chats?: boolean; messages?: boolean };
|
fetchingStatus?: { chats?: boolean; messages?: boolean };
|
||||||
@ -85,9 +86,9 @@ const ChatMessageResults: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
return foundIds
|
return foundIds
|
||||||
.map((id) => {
|
.map((id) => {
|
||||||
const [chatId, messageId] = id.split('_');
|
const [chatId, messageId] = parseSearchResultKey(id);
|
||||||
|
|
||||||
return globalMessagesByChatId?.[chatId]?.byId[Number(messageId)];
|
return globalMessagesByChatId?.[chatId]?.byId[messageId];
|
||||||
})
|
})
|
||||||
.filter(Boolean)
|
.filter(Boolean)
|
||||||
.sort((a, b) => b.date - a.date);
|
.sort((a, b) => b.date - a.date);
|
||||||
|
|||||||
@ -16,6 +16,7 @@ import {
|
|||||||
import { selectSimilarChannelIds, selectTabState } from '../../../global/selectors';
|
import { selectSimilarChannelIds, selectTabState } from '../../../global/selectors';
|
||||||
import { getOrderedIds } from '../../../util/folderManager';
|
import { getOrderedIds } from '../../../util/folderManager';
|
||||||
import { unique } from '../../../util/iteratees';
|
import { unique } from '../../../util/iteratees';
|
||||||
|
import { parseSearchResultKey, type SearchResultKey } from '../../../util/keys/searchResultKey';
|
||||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||||
import { throttle } from '../../../util/schedulers';
|
import { throttle } from '../../../util/schedulers';
|
||||||
import { renderMessageSummary } from '../../common/helpers/renderMessageText';
|
import { renderMessageSummary } from '../../common/helpers/renderMessageText';
|
||||||
@ -49,7 +50,7 @@ type StateProps = {
|
|||||||
contactIds?: string[];
|
contactIds?: string[];
|
||||||
accountPeerIds?: string[];
|
accountPeerIds?: string[];
|
||||||
globalPeerIds?: string[];
|
globalPeerIds?: string[];
|
||||||
foundIds?: string[];
|
foundIds?: SearchResultKey[];
|
||||||
globalMessagesByChatId?: Record<string, { byId: Record<number, ApiMessage> }>;
|
globalMessagesByChatId?: Record<string, { byId: Record<number, ApiMessage> }>;
|
||||||
fetchingStatus?: { chats?: boolean; messages?: boolean };
|
fetchingStatus?: { chats?: boolean; messages?: boolean };
|
||||||
suggestedChannelIds?: string[];
|
suggestedChannelIds?: string[];
|
||||||
@ -191,12 +192,12 @@ const ChatResults: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
return foundIds
|
return foundIds
|
||||||
.map((id) => {
|
.map((id) => {
|
||||||
const [chatId, messageId] = id.split('_');
|
const [chatId, messageId] = parseSearchResultKey(id);
|
||||||
const chat = chatsById[chatId];
|
const chat = chatsById[chatId];
|
||||||
if (!chat) return undefined;
|
if (!chat) return undefined;
|
||||||
if (isChannelList && !isChatChannel(chat)) return undefined;
|
if (isChannelList && !isChatChannel(chat)) return undefined;
|
||||||
|
|
||||||
return globalMessagesByChatId?.[chatId]?.byId[Number(messageId)];
|
return globalMessagesByChatId?.[chatId]?.byId[messageId];
|
||||||
})
|
})
|
||||||
.filter(Boolean);
|
.filter(Boolean);
|
||||||
}, [searchQuery, searchDate, foundIds, isChannelList, globalMessagesByChatId]);
|
}, [searchQuery, searchDate, foundIds, isChannelList, globalMessagesByChatId]);
|
||||||
|
|||||||
@ -4,7 +4,6 @@
|
|||||||
flex-direction: row;
|
flex-direction: row;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
margin-left: 0.5rem;
|
margin-left: 0.5rem;
|
||||||
margin-bottom: 0.5rem;
|
|
||||||
|
|
||||||
.date-item {
|
.date-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import { SLIDE_TRANSITION_DURATION } from '../../../config';
|
|||||||
import { getIsDownloading, getMessageDocument } from '../../../global/helpers';
|
import { getIsDownloading, getMessageDocument } from '../../../global/helpers';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import { formatMonthAndYear, toYearMonth } from '../../../util/dates/dateFormat';
|
import { formatMonthAndYear, toYearMonth } from '../../../util/dates/dateFormat';
|
||||||
|
import { parseSearchResultKey } from '../../../util/keys/searchResultKey';
|
||||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||||
import { throttle } from '../../../util/schedulers';
|
import { throttle } from '../../../util/schedulers';
|
||||||
import { createMapStateToProps } from './helpers/createMapStateToProps';
|
import { createMapStateToProps } from './helpers/createMapStateToProps';
|
||||||
@ -77,8 +78,8 @@ const FileResults: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return foundIds.map((id) => {
|
return foundIds.map((id) => {
|
||||||
const [chatId, messageId] = id.split('_');
|
const [chatId, messageId] = parseSearchResultKey(id);
|
||||||
const message = globalMessagesByChatId[chatId]?.byId[Number(messageId)];
|
const message = globalMessagesByChatId[chatId]?.byId[messageId];
|
||||||
|
|
||||||
return message && getMessageDocument(message) ? message : undefined;
|
return message && getMessageDocument(message) ? message : undefined;
|
||||||
}).filter(Boolean) as ApiMessage[];
|
}).filter(Boolean) as ApiMessage[];
|
||||||
|
|||||||
@ -24,9 +24,9 @@
|
|||||||
|
|
||||||
.section-heading {
|
.section-heading {
|
||||||
position: relative;
|
position: relative;
|
||||||
padding-top: 1.25rem;
|
padding-top: 0.25rem;
|
||||||
padding-left: 1.25rem;
|
padding-left: 1.25rem;
|
||||||
margin: 0 0 1rem -1.25rem !important;
|
margin: 0 0 0.5rem -1.25rem !important;
|
||||||
|
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
font-size: 0.9375rem;
|
font-size: 0.9375rem;
|
||||||
@ -177,17 +177,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.ListItem.search-result-message {
|
|
||||||
.sender-name {
|
|
||||||
color: var(--color-text);
|
|
||||||
|
|
||||||
&::after {
|
|
||||||
content: ": ";
|
|
||||||
white-space: pre;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
.ListItem {
|
.ListItem {
|
||||||
margin: 0 -0.125rem 0 -0.5rem;
|
margin: 0 -0.125rem 0 -0.5rem;
|
||||||
@ -233,7 +222,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.chat-selection {
|
.chat-selection {
|
||||||
padding-top: 0.5rem;
|
padding-block: 0.5rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
@ -246,18 +235,7 @@
|
|||||||
overflow-y: hidden;
|
overflow-y: hidden;
|
||||||
|
|
||||||
> .PickerSelectedItem {
|
> .PickerSelectedItem {
|
||||||
flex: 0 0 auto;
|
flex-shrink: 0;
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[dir="rtl"] {
|
|
||||||
> .PickerSelectedItem:last-child {
|
|
||||||
margin-left: auto;
|
|
||||||
margin-right: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { LoadMoreDirection } from '../../../types';
|
|||||||
import { SLIDE_TRANSITION_DURATION } from '../../../config';
|
import { SLIDE_TRANSITION_DURATION } from '../../../config';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import { formatMonthAndYear, toYearMonth } from '../../../util/dates/dateFormat';
|
import { formatMonthAndYear, toYearMonth } from '../../../util/dates/dateFormat';
|
||||||
|
import { parseSearchResultKey } from '../../../util/keys/searchResultKey';
|
||||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||||
import { throttle } from '../../../util/schedulers';
|
import { throttle } from '../../../util/schedulers';
|
||||||
import { createMapStateToProps } from './helpers/createMapStateToProps';
|
import { createMapStateToProps } from './helpers/createMapStateToProps';
|
||||||
@ -75,9 +76,9 @@ const LinkResults: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return foundIds.map((id) => {
|
return foundIds.map((id) => {
|
||||||
const [chatId, messageId] = id.split('_');
|
const [chatId, messageId] = parseSearchResultKey(id);
|
||||||
|
|
||||||
return globalMessagesByChatId[chatId]?.byId[Number(messageId)];
|
return globalMessagesByChatId[chatId]?.byId[messageId];
|
||||||
}).filter(Boolean);
|
}).filter(Boolean);
|
||||||
}, [globalMessagesByChatId, foundIds]);
|
}, [globalMessagesByChatId, foundIds]);
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,7 @@ import { LoadMoreDirection, MediaViewerOrigin } from '../../../types';
|
|||||||
|
|
||||||
import { SLIDE_TRANSITION_DURATION } from '../../../config';
|
import { SLIDE_TRANSITION_DURATION } from '../../../config';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import { parseSearchResultKey } from '../../../util/keys/searchResultKey';
|
||||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||||
import { throttle } from '../../../util/schedulers';
|
import { throttle } from '../../../util/schedulers';
|
||||||
import { createMapStateToProps } from './helpers/createMapStateToProps';
|
import { createMapStateToProps } from './helpers/createMapStateToProps';
|
||||||
@ -71,9 +72,9 @@ const MediaResults: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return foundIds.map((id) => {
|
return foundIds.map((id) => {
|
||||||
const [chatId, messageId] = id.split('_');
|
const [chatId, messageId] = parseSearchResultKey(id);
|
||||||
|
|
||||||
return globalMessagesByChatId[chatId]?.byId[Number(messageId)];
|
return globalMessagesByChatId[chatId]?.byId[messageId];
|
||||||
}).filter(Boolean);
|
}).filter(Boolean);
|
||||||
}, [globalMessagesByChatId, foundIds]);
|
}, [globalMessagesByChatId, foundIds]);
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import type {
|
|||||||
} from '../../../../api/types';
|
} from '../../../../api/types';
|
||||||
import type { GlobalState, TabState } from '../../../../global/types';
|
import type { GlobalState, TabState } from '../../../../global/types';
|
||||||
import type { ISettings } from '../../../../types';
|
import type { ISettings } from '../../../../types';
|
||||||
|
import type { SearchResultKey } from '../../../../util/keys/searchResultKey';
|
||||||
|
|
||||||
import { selectChat, selectTabState, selectTheme } from '../../../../global/selectors';
|
import { selectChat, selectTabState, selectTheme } from '../../../../global/selectors';
|
||||||
|
|
||||||
@ -12,7 +13,7 @@ export type StateProps = {
|
|||||||
chatsById: Record<string, ApiChat>;
|
chatsById: Record<string, ApiChat>;
|
||||||
usersById: Record<string, ApiUser>;
|
usersById: Record<string, ApiUser>;
|
||||||
globalMessagesByChatId?: Record<string, { byId: Record<number, ApiMessage> }>;
|
globalMessagesByChatId?: Record<string, { byId: Record<number, ApiMessage> }>;
|
||||||
foundIds?: string[];
|
foundIds?: SearchResultKey[];
|
||||||
searchChatId?: string;
|
searchChatId?: string;
|
||||||
activeDownloads: TabState['activeDownloads'];
|
activeDownloads: TabState['activeDownloads'];
|
||||||
isChatProtected?: boolean;
|
isChatProtected?: boolean;
|
||||||
|
|||||||
@ -22,7 +22,7 @@ const HistoryCalendar: FC<OwnProps & StateProps> = ({
|
|||||||
const { searchMessagesByDate, closeHistoryCalendar } = getActions();
|
const { searchMessagesByDate, closeHistoryCalendar } = getActions();
|
||||||
|
|
||||||
const handleJumpToDate = useCallback((date: Date) => {
|
const handleJumpToDate = useCallback((date: Date) => {
|
||||||
searchMessagesByDate({ timestamp: date.valueOf() / 1000 });
|
searchMessagesByDate({ timestamp: date.getTime() / 1000 });
|
||||||
closeHistoryCalendar();
|
closeHistoryCalendar();
|
||||||
}, [closeHistoryCalendar, searchMessagesByDate]);
|
}, [closeHistoryCalendar, searchMessagesByDate]);
|
||||||
|
|
||||||
|
|||||||
@ -10,6 +10,7 @@ import type {
|
|||||||
import { type MediaViewerMedia, MediaViewerOrigin, type ThreadId } from '../../types';
|
import { type MediaViewerMedia, MediaViewerOrigin, type ThreadId } from '../../types';
|
||||||
|
|
||||||
import { ANIMATION_END_DELAY } from '../../config';
|
import { ANIMATION_END_DELAY } from '../../config';
|
||||||
|
import { requestMutation } from '../../lib/fasterdom/fasterdom';
|
||||||
import {
|
import {
|
||||||
getChatMediaMessageIds, getMessagePaidMedia, isChatAdmin, isUserId,
|
getChatMediaMessageIds, getMessagePaidMedia, isChatAdmin, isUserId,
|
||||||
} from '../../global/helpers';
|
} from '../../global/helpers';
|
||||||
@ -180,7 +181,9 @@ const MediaViewer = ({
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
document.body.classList.toggle('is-media-viewer-open', isOpen);
|
requestMutation(() => {
|
||||||
|
document.body.classList.toggle('is-media-viewer-open', isOpen);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}, [isMobile, isOpen]);
|
}, [isMobile, isOpen]);
|
||||||
|
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import { getActions, withGlobal } from '../../global';
|
|||||||
import type { MessageListType } from '../../global/types';
|
import type { MessageListType } from '../../global/types';
|
||||||
import { MAIN_THREAD_ID } from '../../api/types';
|
import { MAIN_THREAD_ID } from '../../api/types';
|
||||||
|
|
||||||
import { selectChat, selectCurrentMessageList } from '../../global/selectors';
|
import { selectChat, selectCurrentMessageList, selectCurrentMiddleSearch } from '../../global/selectors';
|
||||||
import animateScroll from '../../util/animateScroll';
|
import animateScroll from '../../util/animateScroll';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
|
||||||
@ -153,8 +153,10 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
|
|
||||||
const { chatId, threadId, type: messageListType } = currentMessageList;
|
const { chatId, threadId, type: messageListType } = currentMessageList;
|
||||||
const chat = selectChat(global, chatId);
|
const chat = selectChat(global, chatId);
|
||||||
|
const hasActiveMiddleSearch = Boolean(selectCurrentMiddleSearch(global));
|
||||||
|
|
||||||
const shouldShowCount = chat && threadId === MAIN_THREAD_ID && messageListType === 'thread';
|
const shouldShowCount = chat && threadId === MAIN_THREAD_ID && messageListType === 'thread'
|
||||||
|
&& !hasActiveMiddleSearch;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
messageListType,
|
messageListType,
|
||||||
|
|||||||
@ -123,7 +123,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
|||||||
const {
|
const {
|
||||||
joinChannel,
|
joinChannel,
|
||||||
sendBotCommand,
|
sendBotCommand,
|
||||||
openLocalTextSearch,
|
openMiddleSearch,
|
||||||
restartBot,
|
restartBot,
|
||||||
requestMasterAndRequestCall,
|
requestMasterAndRequestCall,
|
||||||
requestNextManagementScreen,
|
requestNextManagementScreen,
|
||||||
@ -196,12 +196,11 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
openLocalTextSearch();
|
openMiddleSearch();
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
// iOS requires synchronous focus on user event.
|
// iOS requires synchronous focus on user event.
|
||||||
const searchInput = document.querySelector<HTMLInputElement>('#MobileSearch input')!;
|
setFocusInSearchInput();
|
||||||
searchInput.focus();
|
|
||||||
} else if (noAnimation) {
|
} else if (noAnimation) {
|
||||||
// The second RAF is necessary because Teact must update the state and render the async component
|
// The second RAF is necessary because Teact must update the state and render the async component
|
||||||
requestMeasure(() => {
|
requestMeasure(() => {
|
||||||
@ -543,6 +542,6 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
)(HeaderActions));
|
)(HeaderActions));
|
||||||
|
|
||||||
function setFocusInSearchInput() {
|
function setFocusInSearchInput() {
|
||||||
const searchInput = document.querySelector<HTMLInputElement>('.RightHeader .SearchInput input');
|
const searchInput = document.querySelector<HTMLInputElement>('#MiddleSearch input');
|
||||||
searchInput?.focus();
|
searchInput?.focus();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -184,7 +184,6 @@ const HeaderPinnedMessage: FC<OwnProps> = ({
|
|||||||
<Transition activeKey={message.id} name="slideVerticalFade" className={styles.messageTextTransition}>
|
<Transition activeKey={message.id} name="slideVerticalFade" className={styles.messageTextTransition}>
|
||||||
<p dir="auto" className={styles.summary}>
|
<p dir="auto" className={styles.summary}>
|
||||||
<MessageSummary
|
<MessageSummary
|
||||||
lang={lang}
|
|
||||||
message={message}
|
message={message}
|
||||||
noEmoji={Boolean(mediaThumbnail)}
|
noEmoji={Boolean(mediaThumbnail)}
|
||||||
emojiSize={EMOJI_SIZE}
|
emojiSize={EMOJI_SIZE}
|
||||||
|
|||||||
@ -57,7 +57,7 @@ import {
|
|||||||
import animateScroll, { isAnimatingScroll, restartCurrentScrollAnimation } from '../../util/animateScroll';
|
import animateScroll, { isAnimatingScroll, restartCurrentScrollAnimation } from '../../util/animateScroll';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
import { orderBy } from '../../util/iteratees';
|
import { orderBy } from '../../util/iteratees';
|
||||||
import { isLocalMessageId } from '../../util/messageKey';
|
import { isLocalMessageId } from '../../util/keys/messageKey';
|
||||||
import resetScroll from '../../util/resetScroll';
|
import resetScroll from '../../util/resetScroll';
|
||||||
import { debounce, onTickEnd } from '../../util/schedulers';
|
import { debounce, onTickEnd } from '../../util/schedulers';
|
||||||
import { groupMessages } from './helpers/groupMessages';
|
import { groupMessages } from './helpers/groupMessages';
|
||||||
|
|||||||
@ -45,7 +45,7 @@ import {
|
|||||||
selectChatFullInfo,
|
selectChatFullInfo,
|
||||||
selectChatMessage,
|
selectChatMessage,
|
||||||
selectCurrentMessageList,
|
selectCurrentMessageList,
|
||||||
selectCurrentTextSearch,
|
selectCurrentMiddleSearch,
|
||||||
selectDraft,
|
selectDraft,
|
||||||
selectIsChatBotNotStarted,
|
selectIsChatBotNotStarted,
|
||||||
selectIsInSelectMode,
|
selectIsInSelectMode,
|
||||||
@ -92,9 +92,9 @@ import FloatingActionButtons from './FloatingActionButtons';
|
|||||||
import MessageList from './MessageList';
|
import MessageList from './MessageList';
|
||||||
import MessageSelectToolbar from './MessageSelectToolbar.async';
|
import MessageSelectToolbar from './MessageSelectToolbar.async';
|
||||||
import MiddleHeader from './MiddleHeader';
|
import MiddleHeader from './MiddleHeader';
|
||||||
import MobileSearch from './MobileSearch.async';
|
|
||||||
import PremiumRequiredPlaceholder from './PremiumRequiredPlaceholder';
|
import PremiumRequiredPlaceholder from './PremiumRequiredPlaceholder';
|
||||||
import ReactorListModal from './ReactorListModal.async';
|
import ReactorListModal from './ReactorListModal.async';
|
||||||
|
import MiddleSearch from './search/MiddleSearch.async';
|
||||||
|
|
||||||
import './MiddleColumn.scss';
|
import './MiddleColumn.scss';
|
||||||
import styles from './MiddleColumn.module.scss';
|
import styles from './MiddleColumn.module.scss';
|
||||||
@ -128,7 +128,7 @@ type StateProps = {
|
|||||||
isRightColumnShown?: boolean;
|
isRightColumnShown?: boolean;
|
||||||
isBackgroundBlurred?: boolean;
|
isBackgroundBlurred?: boolean;
|
||||||
leftColumnWidth?: number;
|
leftColumnWidth?: number;
|
||||||
hasCurrentTextSearch?: boolean;
|
hasActiveMiddleSearch?: boolean;
|
||||||
isSelectModeActive?: boolean;
|
isSelectModeActive?: boolean;
|
||||||
isSeenByModalOpen: boolean;
|
isSeenByModalOpen: boolean;
|
||||||
isPrivacySettingsNoticeModalOpen: boolean;
|
isPrivacySettingsNoticeModalOpen: boolean;
|
||||||
@ -188,7 +188,7 @@ function MiddleColumn({
|
|||||||
isRightColumnShown,
|
isRightColumnShown,
|
||||||
isBackgroundBlurred,
|
isBackgroundBlurred,
|
||||||
leftColumnWidth,
|
leftColumnWidth,
|
||||||
hasCurrentTextSearch,
|
hasActiveMiddleSearch,
|
||||||
isSelectModeActive,
|
isSelectModeActive,
|
||||||
isSeenByModalOpen,
|
isSeenByModalOpen,
|
||||||
isPrivacySettingsNoticeModalOpen,
|
isPrivacySettingsNoticeModalOpen,
|
||||||
@ -221,7 +221,6 @@ function MiddleColumn({
|
|||||||
unpinAllMessages,
|
unpinAllMessages,
|
||||||
loadUser,
|
loadUser,
|
||||||
loadChatSettings,
|
loadChatSettings,
|
||||||
closeLocalTextSearch,
|
|
||||||
exitMessageSelectMode,
|
exitMessageSelectMode,
|
||||||
joinChannel,
|
joinChannel,
|
||||||
sendBotCommand,
|
sendBotCommand,
|
||||||
@ -238,7 +237,8 @@ function MiddleColumn({
|
|||||||
|
|
||||||
const lang = useOldLang();
|
const lang = useOldLang();
|
||||||
const [dropAreaState, setDropAreaState] = useState(DropAreaState.None);
|
const [dropAreaState, setDropAreaState] = useState(DropAreaState.None);
|
||||||
const [isScrollDownShown, setIsScrollDownShown] = useState(false);
|
const [isScrollDownNeeded, setIsScrollDownShown] = useState(false);
|
||||||
|
const isScrollDownShown = isScrollDownNeeded && (!isMobile || !hasActiveMiddleSearch);
|
||||||
const [isNotchShown, setIsNotchShown] = useState<boolean | undefined>();
|
const [isNotchShown, setIsNotchShown] = useState<boolean | undefined>();
|
||||||
const [isUnpinModalOpen, setIsUnpinModalOpen] = useState(false);
|
const [isUnpinModalOpen, setIsUnpinModalOpen] = useState(false);
|
||||||
|
|
||||||
@ -250,7 +250,6 @@ function MiddleColumn({
|
|||||||
getForceNextPinnedInHeader,
|
getForceNextPinnedInHeader,
|
||||||
} = usePinnedMessage(chatId, threadId, pinnedIds, topMessageId);
|
} = usePinnedMessage(chatId, threadId, pinnedIds, topMessageId);
|
||||||
|
|
||||||
const isMobileSearchActive = isMobile && hasCurrentTextSearch;
|
|
||||||
const closeAnimationDuration = isMobile ? LAYER_ANIMATION_DURATION_MS : undefined;
|
const closeAnimationDuration = isMobile ? LAYER_ANIMATION_DURATION_MS : undefined;
|
||||||
const hasTools = hasPinned && (
|
const hasTools = hasPinned && (
|
||||||
windowWidth < MOBILE_SCREEN_MAX_WIDTH
|
windowWidth < MOBILE_SCREEN_MAX_WIDTH
|
||||||
@ -480,11 +479,6 @@ function MiddleColumn({
|
|||||||
onBack: exitMessageSelectMode,
|
onBack: exitMessageSelectMode,
|
||||||
});
|
});
|
||||||
|
|
||||||
useHistoryBack({
|
|
||||||
isActive: isMobileSearchActive,
|
|
||||||
onBack: closeLocalTextSearch,
|
|
||||||
});
|
|
||||||
|
|
||||||
const isMessagingDisabled = Boolean(
|
const isMessagingDisabled = Boolean(
|
||||||
!isPinnedMessageList && !isSavedDialog && !renderingCanPost && !renderingCanRestartBot && !renderingCanStartBot
|
!isPinnedMessageList && !isSavedDialog && !renderingCanPost && !renderingCanRestartBot && !renderingCanStartBot
|
||||||
&& !renderingCanSubscribe && composerRestrictionMessage,
|
&& !renderingCanSubscribe && composerRestrictionMessage,
|
||||||
@ -700,7 +694,7 @@ function MiddleColumn({
|
|||||||
withExtraShift={withExtraShift}
|
withExtraShift={withExtraShift}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{isMobile && <MobileSearch isActive={Boolean(isMobileSearchActive)} />}
|
<MiddleSearch isActive={Boolean(hasActiveMiddleSearch)} />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
{chatId && (
|
{chatId && (
|
||||||
@ -749,7 +743,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
isLeftColumnShown,
|
isLeftColumnShown,
|
||||||
isRightColumnShown: selectIsRightColumnShown(global, isMobile),
|
isRightColumnShown: selectIsRightColumnShown(global, isMobile),
|
||||||
isBackgroundBlurred,
|
isBackgroundBlurred,
|
||||||
hasCurrentTextSearch: Boolean(selectCurrentTextSearch(global)),
|
hasActiveMiddleSearch: Boolean(selectCurrentMiddleSearch(global)),
|
||||||
isSelectModeActive: selectIsInSelectMode(global),
|
isSelectModeActive: selectIsInSelectMode(global),
|
||||||
isSeenByModalOpen: Boolean(seenByModal),
|
isSeenByModalOpen: Boolean(seenByModal),
|
||||||
isPrivacySettingsNoticeModalOpen: Boolean(privacySettingsNoticeModal),
|
isPrivacySettingsNoticeModalOpen: Boolean(privacySettingsNoticeModal),
|
||||||
|
|||||||
@ -35,6 +35,7 @@ import {
|
|||||||
selectChat,
|
selectChat,
|
||||||
selectChatMessage,
|
selectChatMessage,
|
||||||
selectChatMessages,
|
selectChatMessages,
|
||||||
|
selectCurrentMiddleSearch,
|
||||||
selectForwardedSender,
|
selectForwardedSender,
|
||||||
selectIsChatBotNotStarted,
|
selectIsChatBotNotStarted,
|
||||||
selectIsChatWithBot,
|
selectIsChatWithBot,
|
||||||
@ -50,7 +51,7 @@ import {
|
|||||||
} from '../../global/selectors';
|
} from '../../global/selectors';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
import cycleRestrict from '../../util/cycleRestrict';
|
import cycleRestrict from '../../util/cycleRestrict';
|
||||||
import { getMessageKey } from '../../util/messageKey';
|
import { getMessageKey } from '../../util/keys/messageKey';
|
||||||
|
|
||||||
import useAppLayout from '../../hooks/useAppLayout';
|
import useAppLayout from '../../hooks/useAppLayout';
|
||||||
import useConnectionStatus from '../../hooks/useConnectionStatus';
|
import useConnectionStatus from '../../hooks/useConnectionStatus';
|
||||||
@ -58,8 +59,8 @@ import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
|
|||||||
import useDerivedState from '../../hooks/useDerivedState';
|
import useDerivedState from '../../hooks/useDerivedState';
|
||||||
import useElectronDrag from '../../hooks/useElectronDrag';
|
import useElectronDrag from '../../hooks/useElectronDrag';
|
||||||
import useEnsureMessage from '../../hooks/useEnsureMessage';
|
import useEnsureMessage from '../../hooks/useEnsureMessage';
|
||||||
import { useFastClick } from '../../hooks/useFastClick';
|
|
||||||
import useLastCallback from '../../hooks/useLastCallback';
|
import useLastCallback from '../../hooks/useLastCallback';
|
||||||
|
import useLongPress from '../../hooks/useLongPress';
|
||||||
import useOldLang from '../../hooks/useOldLang';
|
import useOldLang from '../../hooks/useOldLang';
|
||||||
import usePrevious from '../../hooks/usePrevious';
|
import usePrevious from '../../hooks/usePrevious';
|
||||||
import useShowTransition from '../../hooks/useShowTransition';
|
import useShowTransition from '../../hooks/useShowTransition';
|
||||||
@ -81,6 +82,7 @@ import './MiddleHeader.scss';
|
|||||||
const ANIMATION_DURATION = 350;
|
const ANIMATION_DURATION = 350;
|
||||||
const BACK_BUTTON_INACTIVE_TIME = 450;
|
const BACK_BUTTON_INACTIVE_TIME = 450;
|
||||||
const EMOJI_STATUS_SIZE = 22;
|
const EMOJI_STATUS_SIZE = 22;
|
||||||
|
const SEARCH_LONGTAP_THRESHOLD = 500;
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
@ -116,6 +118,7 @@ type StateProps = {
|
|||||||
isSynced?: boolean;
|
isSynced?: boolean;
|
||||||
isFetchingDifference?: boolean;
|
isFetchingDifference?: boolean;
|
||||||
emojiStatusSticker?: ApiSticker;
|
emojiStatusSticker?: ApiSticker;
|
||||||
|
isMiddleSearchOpen?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const MiddleHeader: FC<OwnProps & StateProps> = ({
|
const MiddleHeader: FC<OwnProps & StateProps> = ({
|
||||||
@ -148,6 +151,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
|||||||
getLoadingPinnedId,
|
getLoadingPinnedId,
|
||||||
emojiStatusSticker,
|
emojiStatusSticker,
|
||||||
isSavedDialog,
|
isSavedDialog,
|
||||||
|
isMiddleSearchOpen,
|
||||||
onFocusPinnedMessage,
|
onFocusPinnedMessage,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
@ -162,6 +166,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
|||||||
openPremiumModal,
|
openPremiumModal,
|
||||||
openThread,
|
openThread,
|
||||||
openStickerSet,
|
openStickerSet,
|
||||||
|
updateMiddleSearch,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const lang = useOldLang();
|
const lang = useOldLang();
|
||||||
@ -197,15 +202,28 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
|||||||
const componentRef = useRef<HTMLDivElement>(null);
|
const componentRef = useRef<HTMLDivElement>(null);
|
||||||
const shouldAnimateTools = useRef<boolean>(true);
|
const shouldAnimateTools = useRef<boolean>(true);
|
||||||
|
|
||||||
const {
|
const handleOpenSearch = useLastCallback(() => {
|
||||||
handleClick: handleHeaderClick,
|
updateMiddleSearch({ chatId, threadId, update: {} });
|
||||||
handleMouseDown: handleHeaderMouseDown,
|
});
|
||||||
} = useFastClick((e: React.MouseEvent<HTMLDivElement | HTMLButtonElement>) => {
|
|
||||||
if (e.type === 'mousedown' && (e.target as Element).closest('.title > .custom-emoji')) return;
|
const handleOpenChat = useLastCallback((event: React.MouseEvent | React.TouchEvent) => {
|
||||||
|
if ((event.target as Element).closest('.title > .custom-emoji')) return;
|
||||||
|
|
||||||
openThreadWithInfo({ chatId, threadId });
|
openThreadWithInfo({ chatId, threadId });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const {
|
||||||
|
onMouseDown: handleLongPressMouseDown,
|
||||||
|
onMouseUp: handleLongPressMouseUp,
|
||||||
|
onMouseLeave: handleLongPressMouseLeave,
|
||||||
|
onTouchStart: handleLongPressTouchStart,
|
||||||
|
onTouchEnd: handleLongPressTouchEnd,
|
||||||
|
} = useLongPress({
|
||||||
|
onStart: handleOpenSearch,
|
||||||
|
onClick: handleOpenChat,
|
||||||
|
threshold: SEARCH_LONGTAP_THRESHOLD,
|
||||||
|
});
|
||||||
|
|
||||||
const handleUnpinMessage = useLastCallback((messageId: number) => {
|
const handleUnpinMessage = useLastCallback((messageId: number) => {
|
||||||
pinMessage({ messageId, isUnpin: true });
|
pinMessage({ messageId, isUnpin: true });
|
||||||
});
|
});
|
||||||
@ -305,7 +323,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
|||||||
const {
|
const {
|
||||||
shouldRender: shouldRenderPinnedMessage,
|
shouldRender: shouldRenderPinnedMessage,
|
||||||
transitionClassNames: pinnedMessageClassNames,
|
transitionClassNames: pinnedMessageClassNames,
|
||||||
} = useShowTransition(Boolean(pinnedMessage), undefined, true);
|
} = useShowTransition(Boolean(pinnedMessage) && !isMiddleSearchOpen, undefined, true);
|
||||||
|
|
||||||
const renderingPinnedMessage = useCurrentOrPrev(pinnedMessage, true);
|
const renderingPinnedMessage = useCurrentOrPrev(pinnedMessage, true);
|
||||||
const renderingPinnedMessagesCount = useCurrentOrPrev(pinnedMessagesCount, true);
|
const renderingPinnedMessagesCount = useCurrentOrPrev(pinnedMessagesCount, true);
|
||||||
@ -389,8 +407,11 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
|||||||
{(isLeftColumnHideable || currentTransitionKey > 0) && renderBackButton(shouldShowCloseButton, !isSavedDialog)}
|
{(isLeftColumnHideable || currentTransitionKey > 0) && renderBackButton(shouldShowCloseButton, !isSavedDialog)}
|
||||||
<div
|
<div
|
||||||
className="chat-info-wrapper"
|
className="chat-info-wrapper"
|
||||||
onClick={handleHeaderClick}
|
onMouseDown={handleLongPressMouseDown}
|
||||||
onMouseDown={handleHeaderMouseDown}
|
onMouseUp={handleLongPressMouseUp}
|
||||||
|
onMouseLeave={handleLongPressMouseLeave}
|
||||||
|
onTouchStart={handleLongPressTouchStart}
|
||||||
|
onTouchEnd={handleLongPressTouchEnd}
|
||||||
>
|
>
|
||||||
{isUserId(realChatId) ? (
|
{isUserId(realChatId) ? (
|
||||||
<PrivateChatInfo
|
<PrivateChatInfo
|
||||||
@ -562,6 +583,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
const emojiStatusSticker = emojiStatus && global.customEmojis.byId[emojiStatus.documentId];
|
const emojiStatusSticker = emojiStatus && global.customEmojis.byId[emojiStatus.documentId];
|
||||||
|
|
||||||
const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
|
const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
|
||||||
|
const isMiddleSearchOpen = Boolean(selectCurrentMiddleSearch(global));
|
||||||
|
|
||||||
const state: StateProps = {
|
const state: StateProps = {
|
||||||
typingStatus,
|
typingStatus,
|
||||||
@ -581,6 +603,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
emojiStatusSticker,
|
emojiStatusSticker,
|
||||||
hasButtonInHeader: canStartBot || canRestartBot || canSubscribe || shouldSendJoinRequest,
|
hasButtonInHeader: canStartBot || canRestartBot || canSubscribe || shouldSendJoinRequest,
|
||||||
isSavedDialog,
|
isSavedDialog,
|
||||||
|
isMiddleSearchOpen,
|
||||||
};
|
};
|
||||||
|
|
||||||
const messagesById = selectChatMessages(global, chatId);
|
const messagesById = selectChatMessages(global, chatId);
|
||||||
|
|||||||
@ -1,18 +0,0 @@
|
|||||||
import type { FC } from '../../lib/teact/teact';
|
|
||||||
import React from '../../lib/teact/teact';
|
|
||||||
|
|
||||||
import type { OwnProps } from './MobileSearch';
|
|
||||||
|
|
||||||
import { Bundles } from '../../util/moduleLoader';
|
|
||||||
|
|
||||||
import useModuleLoader from '../../hooks/useModuleLoader';
|
|
||||||
|
|
||||||
const MobileSearchAsync: FC<OwnProps> = (props) => {
|
|
||||||
const { isActive } = props;
|
|
||||||
const MobileSearch = useModuleLoader(Bundles.Extra, 'MobileSearch', !isActive, true);
|
|
||||||
|
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
|
||||||
return MobileSearch ? <MobileSearch {...props} /> : undefined;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MobileSearchAsync;
|
|
||||||
@ -1,84 +0,0 @@
|
|||||||
#MobileSearch > .header {
|
|
||||||
position: absolute;
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: var(--z-mobile-search);
|
|
||||||
width: 100%;
|
|
||||||
height: 3.5rem;
|
|
||||||
background: var(--color-background);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding-left: max(0.25rem, env(safe-area-inset-left));
|
|
||||||
padding-right: max(0.5rem, env(safe-area-inset-right));
|
|
||||||
|
|
||||||
> .SearchInput {
|
|
||||||
margin-left: 0.25rem;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
body.is-electron.is-macos & {
|
|
||||||
padding-left: 4.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#MobileSearch > .tags-subheader {
|
|
||||||
--color-reaction: var(--color-background-secondary);
|
|
||||||
--hover-color-reaction: var(--color-background-secondary-accent);
|
|
||||||
--text-color-reaction: var(--color-text-secondary);
|
|
||||||
--color-reaction-chosen: var(--color-primary);
|
|
||||||
--text-color-reaction-chosen: #FFFFFF;
|
|
||||||
--hover-color-reaction-chosen: var(--color-primary-shade);
|
|
||||||
|
|
||||||
position: absolute;
|
|
||||||
top: 3.5rem;
|
|
||||||
left: 0;
|
|
||||||
z-index: var(--z-mobile-search);
|
|
||||||
width: 100%;
|
|
||||||
height: 3rem;
|
|
||||||
background: var(--color-background);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 0.375rem;
|
|
||||||
padding-left: max(0.25rem, env(safe-area-inset-left));
|
|
||||||
padding-right: max(0.5rem, env(safe-area-inset-right));
|
|
||||||
|
|
||||||
overflow-x: scroll;
|
|
||||||
}
|
|
||||||
|
|
||||||
#MobileSearch > .footer {
|
|
||||||
position: absolute;
|
|
||||||
bottom: 0;
|
|
||||||
left: 0;
|
|
||||||
z-index: var(--z-mobile-search);
|
|
||||||
width: 100%;
|
|
||||||
height: 3.5rem;
|
|
||||||
background: var(--color-background);
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
padding-left: max(1rem, env(safe-area-inset-left));
|
|
||||||
padding-right: max(0.5rem, env(safe-area-inset-right));
|
|
||||||
|
|
||||||
body:not(.keyboard-visible) & {
|
|
||||||
padding-bottom: 0;
|
|
||||||
height: 3.5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
|
||||||
body:not(.keyboard-visible) & {
|
|
||||||
padding-bottom: env(safe-area-inset-bottom);
|
|
||||||
height: calc(3.5rem + env(safe-area-inset-bottom));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
> .counter {
|
|
||||||
flex: 1;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#MobileSearch:not(.active) {
|
|
||||||
.header, .tags-subheader, .footer {
|
|
||||||
// `display: none` will prevent synchronous focus on iOS
|
|
||||||
transform: translateX(-999rem);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,322 +0,0 @@
|
|||||||
import type { FC } from '../../lib/teact/teact';
|
|
||||||
import React, {
|
|
||||||
memo, useEffect, useLayoutEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef, useState,
|
|
||||||
} from '../../lib/teact/teact';
|
|
||||||
import { getActions, withGlobal } from '../../global';
|
|
||||||
|
|
||||||
import type {
|
|
||||||
ApiChat, ApiReaction, ApiReactionKey, ApiSavedReactionTag,
|
|
||||||
} from '../../api/types';
|
|
||||||
import type { ThreadId } from '../../types';
|
|
||||||
|
|
||||||
import { requestMutation } from '../../lib/fasterdom/fasterdom';
|
|
||||||
import { getIsSavedDialog, getReactionKey, isSameReaction } from '../../global/helpers';
|
|
||||||
import {
|
|
||||||
selectChat,
|
|
||||||
selectCurrentMessageList,
|
|
||||||
selectCurrentTextSearch,
|
|
||||||
selectIsChatWithSelf,
|
|
||||||
selectIsCurrentUserPremium,
|
|
||||||
selectTabState,
|
|
||||||
} from '../../global/selectors';
|
|
||||||
import { getDayStartAt } from '../../util/dates/dateFormat';
|
|
||||||
import { debounce } from '../../util/schedulers';
|
|
||||||
import { IS_IOS } from '../../util/windowEnvironment';
|
|
||||||
|
|
||||||
import useHorizontalScroll from '../../hooks/useHorizontalScroll';
|
|
||||||
import useLastCallback from '../../hooks/useLastCallback';
|
|
||||||
|
|
||||||
import Button from '../ui/Button';
|
|
||||||
import SearchInput from '../ui/SearchInput';
|
|
||||||
import SavedTagButton from './message/reactions/SavedTagButton';
|
|
||||||
|
|
||||||
import './MobileSearch.scss';
|
|
||||||
|
|
||||||
export type OwnProps = {
|
|
||||||
isActive: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type StateProps = {
|
|
||||||
isActive?: boolean;
|
|
||||||
chat?: ApiChat;
|
|
||||||
threadId?: ThreadId;
|
|
||||||
query?: string;
|
|
||||||
savedTags?: Record<ApiReactionKey, ApiSavedReactionTag>;
|
|
||||||
searchTag?: ApiReaction;
|
|
||||||
totalCount?: number;
|
|
||||||
foundIds?: number[];
|
|
||||||
isHistoryCalendarOpen?: boolean;
|
|
||||||
isCurrentUserPremium?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const runDebouncedForSearch = debounce((cb) => cb(), 200, false);
|
|
||||||
|
|
||||||
const MobileSearchFooter: FC<StateProps> = ({
|
|
||||||
isActive,
|
|
||||||
chat,
|
|
||||||
threadId,
|
|
||||||
query,
|
|
||||||
savedTags,
|
|
||||||
searchTag,
|
|
||||||
totalCount,
|
|
||||||
foundIds,
|
|
||||||
isHistoryCalendarOpen,
|
|
||||||
isCurrentUserPremium,
|
|
||||||
}) => {
|
|
||||||
const {
|
|
||||||
setLocalTextSearchQuery,
|
|
||||||
setLocalTextSearchTag,
|
|
||||||
searchTextMessagesLocal,
|
|
||||||
focusMessage,
|
|
||||||
closeLocalTextSearch,
|
|
||||||
openHistoryCalendar,
|
|
||||||
openPremiumModal,
|
|
||||||
loadSavedReactionTags,
|
|
||||||
} = getActions();
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
|
||||||
const inputRef = useRef<HTMLInputElement>(null);
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
|
||||||
const tagsRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const [focusedIndex, setFocusedIndex] = useState(0);
|
|
||||||
|
|
||||||
const hasQueryData = Boolean(query || searchTag);
|
|
||||||
|
|
||||||
// Fix for iOS keyboard
|
|
||||||
useEffect(() => {
|
|
||||||
const { visualViewport } = window as any;
|
|
||||||
if (!visualViewport) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mainEl = document.getElementById('Main') as HTMLDivElement;
|
|
||||||
const handleResize = () => {
|
|
||||||
const { activeElement } = document;
|
|
||||||
if (activeElement && (activeElement === inputRef.current)) {
|
|
||||||
const { pageTop, height } = visualViewport;
|
|
||||||
|
|
||||||
requestMutation(() => {
|
|
||||||
mainEl.style.transform = `translateY(${pageTop}px)`;
|
|
||||||
mainEl.style.height = `${height}px`;
|
|
||||||
document.documentElement.scrollTop = pageTop;
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
requestMutation(() => {
|
|
||||||
mainEl.style.transform = '';
|
|
||||||
mainEl.style.height = '';
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
visualViewport.addEventListener('resize', handleResize);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
visualViewport.removeEventListener('resize', handleResize);
|
|
||||||
};
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// Focus message
|
|
||||||
useEffect(() => {
|
|
||||||
if (chat?.id && foundIds?.length) {
|
|
||||||
focusMessage({ chatId: chat.id, messageId: foundIds[0], threadId });
|
|
||||||
setFocusedIndex(0);
|
|
||||||
} else {
|
|
||||||
setFocusedIndex(-1);
|
|
||||||
}
|
|
||||||
}, [chat?.id, focusMessage, foundIds, threadId]);
|
|
||||||
|
|
||||||
// Disable native up/down buttons on iOS
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
if (!IS_IOS) return;
|
|
||||||
|
|
||||||
Array.from(document.querySelectorAll<HTMLInputElement>('input')).forEach((input) => {
|
|
||||||
input.disabled = Boolean(isActive && input !== inputRef.current);
|
|
||||||
});
|
|
||||||
}, [isActive]);
|
|
||||||
|
|
||||||
// Blur on exit
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isActive) {
|
|
||||||
inputRef.current!.blur();
|
|
||||||
}
|
|
||||||
}, [isActive]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const searchInput = document.querySelector<HTMLInputElement>('#MobileSearch input')!;
|
|
||||||
searchInput.blur();
|
|
||||||
}, [isHistoryCalendarOpen]);
|
|
||||||
|
|
||||||
const tags = useMemo(() => {
|
|
||||||
if (!savedTags) return undefined;
|
|
||||||
return Object.values(savedTags);
|
|
||||||
}, [savedTags]);
|
|
||||||
|
|
||||||
const hasTags = Boolean(tags?.length);
|
|
||||||
const areTagsDisabled = hasTags && !isCurrentUserPremium;
|
|
||||||
|
|
||||||
useHorizontalScroll(tagsRef, !hasTags);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isActive) loadSavedReactionTags();
|
|
||||||
}, [hasTags, isActive]);
|
|
||||||
|
|
||||||
const handleMessageSearchQueryChange = useLastCallback((newQuery: string) => {
|
|
||||||
setLocalTextSearchQuery({ query: newQuery });
|
|
||||||
|
|
||||||
if (hasQueryData) {
|
|
||||||
runDebouncedForSearch(searchTextMessagesLocal);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleTagClick = useLastCallback((tag: ApiReaction) => {
|
|
||||||
if (areTagsDisabled) {
|
|
||||||
openPremiumModal({
|
|
||||||
initialSection: 'saved_tags',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLocalTextSearchTag({ tag });
|
|
||||||
|
|
||||||
runDebouncedForSearch(searchTextMessagesLocal);
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleUp = useLastCallback(() => {
|
|
||||||
if (chat && foundIds) {
|
|
||||||
const newFocusIndex = focusedIndex + 1;
|
|
||||||
focusMessage({ chatId: chat.id, messageId: foundIds[newFocusIndex], threadId });
|
|
||||||
setFocusedIndex(newFocusIndex);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleDown = useLastCallback(() => {
|
|
||||||
if (chat && foundIds) {
|
|
||||||
const newFocusIndex = focusedIndex - 1;
|
|
||||||
focusMessage({ chatId: chat.id, messageId: foundIds[newFocusIndex], threadId });
|
|
||||||
setFocusedIndex(newFocusIndex);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleCloseLocalTextSearch = useLastCallback(() => {
|
|
||||||
closeLocalTextSearch();
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div id="MobileSearch" className={isActive ? 'active' : ''}>
|
|
||||||
<div className="header">
|
|
||||||
<Button
|
|
||||||
size="smaller"
|
|
||||||
round
|
|
||||||
color="translucent"
|
|
||||||
onClick={handleCloseLocalTextSearch}
|
|
||||||
>
|
|
||||||
<i className="icon icon-arrow-left" />
|
|
||||||
</Button>
|
|
||||||
<SearchInput
|
|
||||||
ref={inputRef}
|
|
||||||
value={query}
|
|
||||||
onChange={handleMessageSearchQueryChange}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
{hasTags && (
|
|
||||||
<div
|
|
||||||
ref={tagsRef}
|
|
||||||
className="tags-subheader custom-scroll-x no-scrollbar"
|
|
||||||
>
|
|
||||||
{tags.map((tag) => (
|
|
||||||
<SavedTagButton
|
|
||||||
containerId="mobile-search"
|
|
||||||
key={getReactionKey(tag.reaction)}
|
|
||||||
reaction={tag.reaction}
|
|
||||||
tag={tag}
|
|
||||||
withCount
|
|
||||||
isDisabled={areTagsDisabled}
|
|
||||||
isChosen={isSameReaction(tag.reaction, searchTag)}
|
|
||||||
onClick={handleTagClick}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="footer">
|
|
||||||
<div className="counter">
|
|
||||||
{hasQueryData ? (
|
|
||||||
foundIds?.length ? (
|
|
||||||
`${focusedIndex + 1} of ${totalCount}`
|
|
||||||
) : foundIds && !foundIds.length ? (
|
|
||||||
'No results'
|
|
||||||
) : (
|
|
||||||
''
|
|
||||||
)
|
|
||||||
) : (
|
|
||||||
<Button
|
|
||||||
round
|
|
||||||
size="smaller"
|
|
||||||
color="translucent"
|
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
|
||||||
onClick={() => openHistoryCalendar({ selectedAt: getDayStartAt(Date.now()) })}
|
|
||||||
ariaLabel="Search messages by date"
|
|
||||||
>
|
|
||||||
<i className="icon icon-calendar" />
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<Button
|
|
||||||
round
|
|
||||||
size="smaller"
|
|
||||||
color="translucent"
|
|
||||||
onClick={handleUp}
|
|
||||||
disabled={!foundIds || !foundIds.length || focusedIndex === foundIds.length - 1}
|
|
||||||
>
|
|
||||||
<i className="icon icon-up" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
round
|
|
||||||
size="smaller"
|
|
||||||
color="translucent"
|
|
||||||
onClick={handleDown}
|
|
||||||
disabled={!foundIds || !foundIds.length || focusedIndex === 0}
|
|
||||||
>
|
|
||||||
<i className="icon icon-down" />
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(withGlobal<OwnProps>(
|
|
||||||
(global): StateProps => {
|
|
||||||
const currentMessageList = selectCurrentMessageList(global);
|
|
||||||
if (!currentMessageList) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
const { chatId, threadId } = currentMessageList;
|
|
||||||
|
|
||||||
const chat = selectChat(global, chatId);
|
|
||||||
if (!chat) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const { query, savedTag, results } = selectCurrentTextSearch(global) || {};
|
|
||||||
const { totalCount, foundIds } = results || {};
|
|
||||||
|
|
||||||
const isSavedMessages = selectIsChatWithSelf(global, chatId);
|
|
||||||
const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
|
|
||||||
|
|
||||||
const savedTags = isSavedMessages && !isSavedDialog ? global.savedReactionTags?.byKey : undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
chat,
|
|
||||||
query,
|
|
||||||
totalCount,
|
|
||||||
threadId,
|
|
||||||
foundIds,
|
|
||||||
isHistoryCalendarOpen: Boolean(selectTabState(global).historyCalendarSelectedAt),
|
|
||||||
savedTags,
|
|
||||||
searchTag: savedTag,
|
|
||||||
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
)(MobileSearchFooter));
|
|
||||||
@ -17,7 +17,7 @@ import {
|
|||||||
selectCanAutoPlayMedia,
|
selectCanAutoPlayMedia,
|
||||||
selectTheme,
|
selectTheme,
|
||||||
} from '../../../global/selectors';
|
} from '../../../global/selectors';
|
||||||
import { getMessageKey } from '../../../util/messageKey';
|
import { getMessageKey } from '../../../util/keys/messageKey';
|
||||||
import { AlbumRectPart } from './helpers/calculateAlbumLayout';
|
import { AlbumRectPart } from './helpers/calculateAlbumLayout';
|
||||||
import withSelectControl from './hocs/withSelectControl';
|
import withSelectControl from './hocs/withSelectControl';
|
||||||
|
|
||||||
|
|||||||
@ -72,7 +72,7 @@ import {
|
|||||||
selectChatFullInfo,
|
selectChatFullInfo,
|
||||||
selectChatMessage,
|
selectChatMessage,
|
||||||
selectChatTranslations,
|
selectChatTranslations,
|
||||||
selectCurrentTextSearch,
|
selectCurrentMiddleSearch,
|
||||||
selectDefaultReaction,
|
selectDefaultReaction,
|
||||||
selectForwardedSender,
|
selectForwardedSender,
|
||||||
selectIsChatProtected,
|
selectIsChatProtected,
|
||||||
@ -105,7 +105,7 @@ import {
|
|||||||
import { isAnimatingScroll } from '../../../util/animateScroll';
|
import { isAnimatingScroll } from '../../../util/animateScroll';
|
||||||
import buildClassName from '../../../util/buildClassName';
|
import buildClassName from '../../../util/buildClassName';
|
||||||
import { isElementInViewport } from '../../../util/isElementInViewport';
|
import { isElementInViewport } from '../../../util/isElementInViewport';
|
||||||
import { getMessageKey } from '../../../util/messageKey';
|
import { getMessageKey } from '../../../util/keys/messageKey';
|
||||||
import stopEvent from '../../../util/stopEvent';
|
import stopEvent from '../../../util/stopEvent';
|
||||||
import { IS_ANDROID, IS_ELECTRON, IS_TRANSLATION_SUPPORTED } from '../../../util/windowEnvironment';
|
import { IS_ANDROID, IS_ELECTRON, IS_TRANSLATION_SUPPORTED } from '../../../util/windowEnvironment';
|
||||||
import {
|
import {
|
||||||
@ -1038,6 +1038,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
return (
|
return (
|
||||||
<Reactions
|
<Reactions
|
||||||
message={reactionMessage!}
|
message={reactionMessage!}
|
||||||
|
threadId={threadId}
|
||||||
metaChildren={meta}
|
metaChildren={meta}
|
||||||
observeIntersection={observeIntersectionForPlaying}
|
observeIntersection={observeIntersectionForPlaying}
|
||||||
noRecentReactors={isChannel}
|
noRecentReactors={isChannel}
|
||||||
@ -1641,6 +1642,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
|||||||
{reactionsPosition === 'outside' && !isStoryMention && (
|
{reactionsPosition === 'outside' && !isStoryMention && (
|
||||||
<Reactions
|
<Reactions
|
||||||
message={reactionMessage!}
|
message={reactionMessage!}
|
||||||
|
threadId={threadId}
|
||||||
isOutside
|
isOutside
|
||||||
isCurrentUserPremium={isPremium}
|
isCurrentUserPremium={isPremium}
|
||||||
maxWidth={reactionsMaxWidth}
|
maxWidth={reactionsMaxWidth}
|
||||||
@ -1738,7 +1740,9 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
quote: focusedQuote, scrollTargetPosition,
|
quote: focusedQuote, scrollTargetPosition,
|
||||||
} = (isFocused && focusedMessage) || {};
|
} = (isFocused && focusedMessage) || {};
|
||||||
|
|
||||||
const { query: highlight } = selectCurrentTextSearch(global) || {};
|
const middleSearch = selectCurrentMiddleSearch(global);
|
||||||
|
const highlight = middleSearch?.results?.query
|
||||||
|
&& `${middleSearch.isHashtag ? '#' : ''}${middleSearch.results.query}`;
|
||||||
|
|
||||||
const singleEmoji = getMessageSingleRegularEmoji(message);
|
const singleEmoji = getMessageSingleRegularEmoji(message);
|
||||||
const animatedEmoji = singleEmoji && selectAnimatedEmoji(global, singleEmoji) ? singleEmoji : undefined;
|
const animatedEmoji = singleEmoji && selectAnimatedEmoji(global, singleEmoji) ? singleEmoji : undefined;
|
||||||
|
|||||||
@ -173,6 +173,7 @@
|
|||||||
line-height: 1;
|
line-height: 1;
|
||||||
height: calc(var(--message-meta-height, 1rem));
|
height: calc(var(--message-meta-height, 1rem));
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
margin-top: -0.5rem;
|
||||||
margin-right: -0.5rem;
|
margin-right: -0.5rem;
|
||||||
align-self: flex-end;
|
align-self: flex-end;
|
||||||
|
|
||||||
|
|||||||
@ -9,9 +9,6 @@
|
|||||||
--reaction-background: var(--color-reaction-chosen);
|
--reaction-background: var(--color-reaction-chosen);
|
||||||
--reaction-background-hover: var(--hover-color-reaction-chosen);
|
--reaction-background-hover: var(--hover-color-reaction-chosen);
|
||||||
--reaction-text-color: var(--text-color-reaction-chosen);
|
--reaction-text-color: var(--text-color-reaction-chosen);
|
||||||
|
|
||||||
position: relative;
|
|
||||||
z-index: 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -27,7 +24,9 @@
|
|||||||
text-transform: none;
|
text-transform: none;
|
||||||
color: var(--reaction-text-color);
|
color: var(--reaction-text-color);
|
||||||
overflow: visible;
|
overflow: visible;
|
||||||
|
position: relative;
|
||||||
line-height: 1.75rem;
|
line-height: 1.75rem;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
gap: 0.125rem;
|
gap: 0.125rem;
|
||||||
|
|
||||||
|
|||||||
@ -10,12 +10,13 @@ import type {
|
|||||||
ApiSavedReactionTag,
|
ApiSavedReactionTag,
|
||||||
} from '../../../../api/types';
|
} from '../../../../api/types';
|
||||||
import type { ObserveFn } from '../../../../hooks/useIntersectionObserver';
|
import type { ObserveFn } from '../../../../hooks/useIntersectionObserver';
|
||||||
|
import type { ThreadId } from '../../../../types';
|
||||||
import type { Signal } from '../../../../util/signals';
|
import type { Signal } from '../../../../util/signals';
|
||||||
|
|
||||||
import { getReactionKey, isReactionChosen } from '../../../../global/helpers';
|
import { getReactionKey, isReactionChosen } from '../../../../global/helpers';
|
||||||
import { selectPeer } from '../../../../global/selectors';
|
import { selectPeer } from '../../../../global/selectors';
|
||||||
import buildClassName from '../../../../util/buildClassName';
|
import buildClassName from '../../../../util/buildClassName';
|
||||||
import { getMessageKey } from '../../../../util/messageKey';
|
import { getMessageKey } from '../../../../util/keys/messageKey';
|
||||||
|
|
||||||
import useDerivedState from '../../../../hooks/useDerivedState';
|
import useDerivedState from '../../../../hooks/useDerivedState';
|
||||||
import useLastCallback from '../../../../hooks/useLastCallback';
|
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||||
@ -28,6 +29,7 @@ import './Reactions.scss';
|
|||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
message: ApiMessage;
|
message: ApiMessage;
|
||||||
|
threadId?: ThreadId;
|
||||||
isOutside?: boolean;
|
isOutside?: boolean;
|
||||||
maxWidth?: number;
|
maxWidth?: number;
|
||||||
metaChildren?: React.ReactNode;
|
metaChildren?: React.ReactNode;
|
||||||
@ -42,6 +44,7 @@ const MAX_RECENT_AVATARS = 3;
|
|||||||
|
|
||||||
const Reactions: FC<OwnProps> = ({
|
const Reactions: FC<OwnProps> = ({
|
||||||
message,
|
message,
|
||||||
|
threadId,
|
||||||
isOutside,
|
isOutside,
|
||||||
maxWidth,
|
maxWidth,
|
||||||
metaChildren,
|
metaChildren,
|
||||||
@ -53,8 +56,8 @@ const Reactions: FC<OwnProps> = ({
|
|||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
toggleReaction,
|
toggleReaction,
|
||||||
setLocalTextSearchTag,
|
updateMiddleSearch,
|
||||||
searchTextMessagesLocal,
|
performMiddleSearch,
|
||||||
openPremiumModal,
|
openPremiumModal,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
const lang = useOldLang();
|
const lang = useOldLang();
|
||||||
@ -112,8 +115,8 @@ const Reactions: FC<OwnProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
setLocalTextSearchTag({ tag: reaction });
|
updateMiddleSearch({ chatId: message.chatId, threadId, update: { savedTag: reaction } });
|
||||||
searchTextMessagesLocal();
|
performMiddleSearch({ chatId: message.chatId, threadId });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
18
src/components/middle/search/MiddleSearch.async.tsx
Normal file
18
src/components/middle/search/MiddleSearch.async.tsx
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
import React from '../../../lib/teact/teact';
|
||||||
|
|
||||||
|
import type { OwnProps } from './MiddleSearch';
|
||||||
|
|
||||||
|
import { Bundles } from '../../../util/moduleLoader';
|
||||||
|
|
||||||
|
import useModuleLoader from '../../../hooks/useModuleLoader';
|
||||||
|
|
||||||
|
const MiddleSearchAsync: FC<OwnProps> = (props) => {
|
||||||
|
const { isActive } = props;
|
||||||
|
const MiddleSearch = useModuleLoader(Bundles.Extra, 'MiddleSearch', !isActive, true);
|
||||||
|
|
||||||
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||||
|
return MiddleSearch ? <MiddleSearch {...props} /> : undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default MiddleSearchAsync;
|
||||||
300
src/components/middle/search/MiddleSearch.module.scss
Normal file
300
src/components/middle/search/MiddleSearch.module.scss
Normal file
@ -0,0 +1,300 @@
|
|||||||
|
@use "../../../styles/mixins";
|
||||||
|
|
||||||
|
.root {
|
||||||
|
--color-reaction: var(--color-background-secondary);
|
||||||
|
--hover-color-reaction: var(--color-background-secondary-accent);
|
||||||
|
--text-color-reaction: var(--color-text-secondary);
|
||||||
|
--color-reaction-chosen: var(--color-primary);
|
||||||
|
--text-color-reaction-chosen: #FFFFFF;
|
||||||
|
--hover-color-reaction-chosen: var(--color-primary-shade);
|
||||||
|
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: var(--z-local-search);
|
||||||
|
|
||||||
|
@media (min-width: 1276px) {
|
||||||
|
:global(#Main.right-column-open) & {
|
||||||
|
width: calc(100% - var(--right-column-width));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 3.5rem;
|
||||||
|
background-color: var(--color-background);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: max(1.5rem, env(safe-area-inset-left));
|
||||||
|
padding-right: max(0.875rem, env(safe-area-inset-right));
|
||||||
|
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 200ms ease-in-out;
|
||||||
|
|
||||||
|
.active & {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
padding-left: max(0.5rem, env(safe-area-inset-left));
|
||||||
|
padding-right: max(0.5rem, env(safe-area-inset-right));
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(body.is-electron.is-macos) & {
|
||||||
|
padding-left: 4.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Same as in MiddleHeader.scss
|
||||||
|
.avatar {
|
||||||
|
width: 2.5rem !important;
|
||||||
|
height: 2.5rem !important;
|
||||||
|
margin-right: 0.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
border: none;
|
||||||
|
margin-left: 0.25rem;
|
||||||
|
margin-right: 0.75rem;
|
||||||
|
flex: 1;
|
||||||
|
|
||||||
|
transition-property: background-color, box-shadow, border-radius;
|
||||||
|
transition-duration: 200ms;
|
||||||
|
transition-timing-function: ease-in-out;
|
||||||
|
|
||||||
|
.mobile & {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.focused .input {
|
||||||
|
box-shadow: 0 0 0.625rem 0 var(--color-default-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.withDropdown {
|
||||||
|
background-color: var(--color-background);
|
||||||
|
box-shadow: 0 0 0.625rem 0 var(--color-default-shadow);
|
||||||
|
}
|
||||||
|
|
||||||
|
.adaptSearchBorders {
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdown {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
transform: translateY(100%);
|
||||||
|
max-height: min(24rem, 80vh);
|
||||||
|
pointer-events: all;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
background-color: var(--color-background);
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
box-shadow: 0 0 0.625rem 0 var(--color-default-shadow);
|
||||||
|
clip-path: inset(0 -0.625rem -0.625rem -0.625rem); // Hide top shadow
|
||||||
|
|
||||||
|
border-bottom-left-radius: 1.375rem;
|
||||||
|
border-bottom-right-radius: 1.375rem;
|
||||||
|
|
||||||
|
transition-behavior: allow-discrete;
|
||||||
|
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
|
||||||
|
transition-property: display, opacity;
|
||||||
|
transition-duration: 200ms;
|
||||||
|
|
||||||
|
.mobile & {
|
||||||
|
position: absolute;
|
||||||
|
top: 3.375rem; // Subpixel rendering can leave 1px gap otherwise
|
||||||
|
right: 0;
|
||||||
|
bottom: 3.375rem;
|
||||||
|
left: 0;
|
||||||
|
|
||||||
|
max-height: none;
|
||||||
|
padding: 0;
|
||||||
|
|
||||||
|
transform: none;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
@starting-style {
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.dropdownHidden {
|
||||||
|
display: none;
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.results {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-y: scroll;
|
||||||
|
padding: 0.5rem;
|
||||||
|
|
||||||
|
@include mixins.adapt-padding-to-scrollbar(0.5rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.placeholder {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
text-align: center;
|
||||||
|
margin: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.separator {
|
||||||
|
margin-inline: 1rem;
|
||||||
|
border-top: 1px solid var(--color-borders);
|
||||||
|
}
|
||||||
|
|
||||||
|
.savedTags {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.375rem;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
padding-block: 1rem;
|
||||||
|
margin-inline: 1rem;
|
||||||
|
|
||||||
|
border-bottom: 1px solid var(--color-borders);
|
||||||
|
|
||||||
|
overflow-x: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wrap {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchTags {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.125rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.savedSearchTag {
|
||||||
|
margin-inline: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hash {
|
||||||
|
margin-inline-end: -0.5rem;
|
||||||
|
margin-inline-start: 0.5rem;
|
||||||
|
|
||||||
|
display: grid;
|
||||||
|
place-items: center;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchTypes {
|
||||||
|
display: flex;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
padding-block: 1rem;
|
||||||
|
margin-inline: 1rem;
|
||||||
|
|
||||||
|
border-bottom: 1px solid var(--color-borders);
|
||||||
|
|
||||||
|
overflow-x: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.searchType {
|
||||||
|
--accent-color: var(--color-primary);
|
||||||
|
|
||||||
|
flex-grow: 0 !important;
|
||||||
|
flex-shrink: 0;
|
||||||
|
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
background-color: var(--color-item-active);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selectedType {
|
||||||
|
background-color: var(--color-primary);
|
||||||
|
color: var(--color-white) !important;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-primary-shade);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 3.5rem;
|
||||||
|
background-color: var(--color-background);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
padding-left: max(1rem, env(safe-area-inset-left));
|
||||||
|
padding-right: max(0.5rem, env(safe-area-inset-right));
|
||||||
|
|
||||||
|
pointer-events: auto;
|
||||||
|
|
||||||
|
box-shadow: 0 -2px 2px var(--color-light-shadow);
|
||||||
|
|
||||||
|
transform: translateY(100%);
|
||||||
|
transition: transform 200ms ease-in-out;
|
||||||
|
|
||||||
|
.active & {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global {
|
||||||
|
body:not(.keyboard-visible) & {
|
||||||
|
padding-bottom: 0;
|
||||||
|
height: 3.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 600px) {
|
||||||
|
body:not(.keyboard-visible) & {
|
||||||
|
padding-bottom: env(safe-area-inset-bottom);
|
||||||
|
height: calc(3.5rem + env(safe-area-inset-bottom));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.counter {
|
||||||
|
flex: 1;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mobileNavigation {
|
||||||
|
position: absolute;
|
||||||
|
right: 0.5rem;
|
||||||
|
bottom: 4rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigationButton {
|
||||||
|
transition-property: background-color, color, filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
.navigationDisabled {
|
||||||
|
filter: brightness(0.9);
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes jumpIn {
|
||||||
|
from {
|
||||||
|
transform: translateY(100%);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateY(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
818
src/components/middle/search/MiddleSearch.tsx
Normal file
818
src/components/middle/search/MiddleSearch.tsx
Normal file
@ -0,0 +1,818 @@
|
|||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
import React, {
|
||||||
|
memo, useEffect, useLayoutEffect,
|
||||||
|
useMemo,
|
||||||
|
useRef, useState,
|
||||||
|
} from '../../../lib/teact/teact';
|
||||||
|
import { getActions, getGlobal, withGlobal } from '../../../global';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ApiChat, ApiMessage, ApiReaction, ApiReactionKey, ApiSavedReactionTag,
|
||||||
|
} from '../../../api/types';
|
||||||
|
import type {
|
||||||
|
CustomPeer, MiddleSearchParams, MiddleSearchType, ThreadId,
|
||||||
|
} from '../../../types';
|
||||||
|
|
||||||
|
import { ANONYMOUS_USER_ID, REPLIES_USER_ID } from '../../../config';
|
||||||
|
import { requestMeasure, requestMutation, requestNextMutation } from '../../../lib/fasterdom/fasterdom';
|
||||||
|
import { getIsSavedDialog, getReactionKey, isSameReaction } from '../../../global/helpers';
|
||||||
|
import {
|
||||||
|
selectChat,
|
||||||
|
selectChatMessage,
|
||||||
|
selectCurrentMessageList,
|
||||||
|
selectCurrentMiddleSearch,
|
||||||
|
selectForwardedSender,
|
||||||
|
selectIsChatWithSelf,
|
||||||
|
selectIsCurrentUserPremium,
|
||||||
|
selectSender,
|
||||||
|
selectTabState,
|
||||||
|
} from '../../../global/selectors';
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
||||||
|
import { getDayStartAt } from '../../../util/dates/dateFormat';
|
||||||
|
import focusEditableElement from '../../../util/focusEditableElement';
|
||||||
|
import { getSearchResultKey, parseSearchResultKey, type SearchResultKey } from '../../../util/keys/searchResultKey';
|
||||||
|
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||||
|
import { debounce, fastRaf } from '../../../util/schedulers';
|
||||||
|
import { IS_IOS } from '../../../util/windowEnvironment';
|
||||||
|
|
||||||
|
import { useClickOutside } from '../../../hooks/events/useOutsideClick';
|
||||||
|
import useAppLayout from '../../../hooks/useAppLayout';
|
||||||
|
import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps';
|
||||||
|
import useFlag from '../../../hooks/useFlag';
|
||||||
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||||
|
import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
|
||||||
|
import useKeyboardListNavigation from '../../../hooks/useKeyboardListNavigation';
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
|
import useOldLang from '../../../hooks/useOldLang';
|
||||||
|
|
||||||
|
import Avatar from '../../common/Avatar';
|
||||||
|
import Icon from '../../common/icons/Icon';
|
||||||
|
import PickerSelectedItem from '../../common/PickerSelectedItem';
|
||||||
|
import Button from '../../ui/Button';
|
||||||
|
import InfiniteScroll from '../../ui/InfiniteScroll';
|
||||||
|
import SearchInput from '../../ui/SearchInput';
|
||||||
|
import SavedTagButton from '../message/reactions/SavedTagButton';
|
||||||
|
import MiddleSearchResult from './MiddleSearchResult';
|
||||||
|
|
||||||
|
import styles from './MiddleSearch.module.scss';
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
isActive: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
isActive?: boolean;
|
||||||
|
chat?: ApiChat;
|
||||||
|
threadId?: ThreadId;
|
||||||
|
requestedQuery?: string;
|
||||||
|
savedTags?: Record<ApiReactionKey, ApiSavedReactionTag>;
|
||||||
|
savedTag?: ApiReaction;
|
||||||
|
totalCount?: number;
|
||||||
|
lastSearchQuery?: string;
|
||||||
|
foundIds?: SearchResultKey[];
|
||||||
|
isHistoryCalendarOpen?: boolean;
|
||||||
|
isCurrentUserPremium?: boolean;
|
||||||
|
isSavedMessages?: boolean;
|
||||||
|
fetchingQuery?: string;
|
||||||
|
isHashtagQuery?: boolean;
|
||||||
|
searchType?: MiddleSearchType;
|
||||||
|
currentUserId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CHANNELS_PEER: CustomPeer = {
|
||||||
|
isCustomPeer: true,
|
||||||
|
avatarIcon: 'channel-filled',
|
||||||
|
titleKey: 'SearchPublicPosts',
|
||||||
|
};
|
||||||
|
const FOCUSED_SEARCH_TRIGGER_OFFSET = 5;
|
||||||
|
const HIDE_TIMEOUT = 200;
|
||||||
|
const RESULT_ITEM_CLASS_NAME = 'MiddleSearchResult';
|
||||||
|
|
||||||
|
const runDebouncedForSearch = debounce((cb) => cb(), 200, false);
|
||||||
|
|
||||||
|
const MiddleSearch: FC<StateProps> = ({
|
||||||
|
isActive,
|
||||||
|
chat,
|
||||||
|
threadId,
|
||||||
|
requestedQuery,
|
||||||
|
savedTags,
|
||||||
|
savedTag,
|
||||||
|
totalCount,
|
||||||
|
lastSearchQuery,
|
||||||
|
foundIds,
|
||||||
|
isHistoryCalendarOpen,
|
||||||
|
isCurrentUserPremium,
|
||||||
|
isSavedMessages,
|
||||||
|
fetchingQuery,
|
||||||
|
isHashtagQuery,
|
||||||
|
searchType = 'chat',
|
||||||
|
currentUserId,
|
||||||
|
}) => {
|
||||||
|
const {
|
||||||
|
updateMiddleSearch,
|
||||||
|
resetMiddleSearch,
|
||||||
|
performMiddleSearch,
|
||||||
|
focusMessage,
|
||||||
|
closeMiddleSearch,
|
||||||
|
openHistoryCalendar,
|
||||||
|
openPremiumModal,
|
||||||
|
loadSavedReactionTags,
|
||||||
|
} = getActions();
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const inputRef = useRef<HTMLInputElement>(null);
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const { isMobile } = useAppLayout();
|
||||||
|
const oldLang = useOldLang();
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
|
const [query, setQuery] = useState(requestedQuery || '');
|
||||||
|
const [focusedIndex, setFocusedIndex] = useState(0);
|
||||||
|
const canFocusNewer = foundIds && focusedIndex > 0;
|
||||||
|
const canFocusOlder = foundIds && focusedIndex < foundIds.length - 1;
|
||||||
|
|
||||||
|
const [isFullyHidden, setIsFullyHidden] = useState(!isActive);
|
||||||
|
const hiddenTimerRef = useRef<number>();
|
||||||
|
const maybeLongPressActiveRef = useRef(true);
|
||||||
|
|
||||||
|
const [isFocused, markFocused, markBlurred] = useFlag();
|
||||||
|
const [isViewAsList, setIsViewAsList] = useState(true);
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const handleClickOutside = useLastCallback((event: MouseEvent) => {
|
||||||
|
if (maybeLongPressActiveRef.current) return;
|
||||||
|
// Ignore clicks inside modals
|
||||||
|
if ((event.target as HTMLElement).closest('.Modal')) return;
|
||||||
|
markBlurred();
|
||||||
|
});
|
||||||
|
useClickOutside([ref], handleClickOutside);
|
||||||
|
|
||||||
|
const hasResultsContainer = Boolean((query && foundIds) || isHashtagQuery);
|
||||||
|
const isOnlyHash = isHashtagQuery && !query;
|
||||||
|
const areResultsEmpty = Boolean(query && foundIds && !foundIds.length && !isLoading && !isOnlyHash);
|
||||||
|
const hasResultsPlaceholder = areResultsEmpty || isOnlyHash;
|
||||||
|
const isNonFocusedDropdownForced = searchType === 'myChats' || searchType === 'channels';
|
||||||
|
const hasResultsDropdown = isActive && (isViewAsList || !isMobile) && (isFocused || isNonFocusedDropdownForced)
|
||||||
|
&& Boolean(
|
||||||
|
hasResultsContainer || hasResultsPlaceholder || savedTags,
|
||||||
|
);
|
||||||
|
|
||||||
|
const hasQueryData = Boolean((query && !isOnlyHash) || savedTag);
|
||||||
|
const hasNavigationButtons = searchType === 'chat' && Boolean(foundIds?.length);
|
||||||
|
|
||||||
|
const handleClose = useLastCallback(() => {
|
||||||
|
closeMiddleSearch();
|
||||||
|
});
|
||||||
|
|
||||||
|
const focusInput = useLastCallback(() => {
|
||||||
|
requestMeasure(() => {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const blurInput = useLastCallback(() => {
|
||||||
|
inputRef.current?.blur();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Fix for iOS keyboard
|
||||||
|
useEffect(() => {
|
||||||
|
const { visualViewport } = window;
|
||||||
|
if (!visualViewport) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mainEl = document.getElementById('Main') as HTMLDivElement;
|
||||||
|
const handleResize = () => {
|
||||||
|
const { activeElement } = document;
|
||||||
|
if (activeElement && (activeElement === inputRef.current)) {
|
||||||
|
const { pageTop, height } = visualViewport;
|
||||||
|
|
||||||
|
requestMutation(() => {
|
||||||
|
mainEl.style.transform = `translateY(${pageTop}px)`;
|
||||||
|
mainEl.style.height = `${height}px`;
|
||||||
|
document.documentElement.scrollTop = pageTop;
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
requestMutation(() => {
|
||||||
|
mainEl.style.transform = '';
|
||||||
|
mainEl.style.height = '';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
visualViewport.addEventListener('resize', handleResize);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
visualViewport.removeEventListener('resize', handleResize);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// Focus message
|
||||||
|
useEffect(() => {
|
||||||
|
if (foundIds?.length) {
|
||||||
|
if (searchType === 'chat') {
|
||||||
|
const [chatId, messageId] = parseSearchResultKey(foundIds[0]);
|
||||||
|
focusMessage({ chatId, messageId, threadId });
|
||||||
|
}
|
||||||
|
setFocusedIndex(0);
|
||||||
|
} else {
|
||||||
|
setFocusedIndex(-1);
|
||||||
|
}
|
||||||
|
}, [searchType, focusMessage, foundIds, threadId]);
|
||||||
|
|
||||||
|
// Disable native up/down buttons on iOS
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
if (!IS_IOS) return;
|
||||||
|
|
||||||
|
Array.from(document.querySelectorAll<HTMLInputElement>('input')).forEach((input) => {
|
||||||
|
input.disabled = Boolean(isActive && input !== inputRef.current);
|
||||||
|
});
|
||||||
|
}, [isActive]);
|
||||||
|
|
||||||
|
// Blur on exit
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isActive) {
|
||||||
|
inputRef.current!.blur();
|
||||||
|
setIsViewAsList(true);
|
||||||
|
setFocusedIndex(0);
|
||||||
|
setQuery('');
|
||||||
|
hiddenTimerRef.current = window.setTimeout(() => setIsFullyHidden(true), HIDE_TIMEOUT);
|
||||||
|
} else {
|
||||||
|
setIsFullyHidden(false);
|
||||||
|
clearTimeout(hiddenTimerRef.current);
|
||||||
|
}
|
||||||
|
}, [isActive]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!requestedQuery || !chat?.id) return;
|
||||||
|
setQuery(requestedQuery);
|
||||||
|
updateMiddleSearch({ chatId: chat.id, threadId, update: { requestedQuery: undefined } });
|
||||||
|
setIsLoading(true);
|
||||||
|
|
||||||
|
requestNextMutation(() => {
|
||||||
|
const input = inputRef.current;
|
||||||
|
if (!input) return;
|
||||||
|
focusEditableElement(input, true, true);
|
||||||
|
markFocused();
|
||||||
|
});
|
||||||
|
}, [chat?.id, requestedQuery, threadId]);
|
||||||
|
|
||||||
|
useEffectWithPrevDeps(([prevIsActive]) => {
|
||||||
|
if (isActive !== prevIsActive && !query && lastSearchQuery) {
|
||||||
|
setQuery(lastSearchQuery); // Restore query when returning back
|
||||||
|
}
|
||||||
|
}, [isActive, lastSearchQuery, query]);
|
||||||
|
|
||||||
|
useEffectWithPrevDeps(([prevIsCalendarOpen]) => {
|
||||||
|
if (!isActive || prevIsCalendarOpen === isHistoryCalendarOpen) return;
|
||||||
|
if (isHistoryCalendarOpen) {
|
||||||
|
blurInput();
|
||||||
|
markBlurred();
|
||||||
|
} else {
|
||||||
|
focusInput();
|
||||||
|
}
|
||||||
|
}, [isHistoryCalendarOpen, isActive]);
|
||||||
|
|
||||||
|
const handleReset = useLastCallback(() => {
|
||||||
|
if (!query?.length && !savedTag) {
|
||||||
|
handleClose();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setQuery('');
|
||||||
|
setIsLoading(false);
|
||||||
|
resetMiddleSearch();
|
||||||
|
focusInput();
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => (isActive ? captureEscKeyListener(handleReset) : undefined), [isActive, handleClose]);
|
||||||
|
|
||||||
|
const savedTagsArray = useMemo(() => {
|
||||||
|
if (!savedTags) return undefined;
|
||||||
|
return Object.values(savedTags);
|
||||||
|
}, [savedTags]);
|
||||||
|
|
||||||
|
const hasSavedTags = Boolean(savedTagsArray?.length);
|
||||||
|
const areSavedTagsDisabled = hasSavedTags && !isCurrentUserPremium;
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isSavedMessages && isActive) loadSavedReactionTags();
|
||||||
|
}, [isSavedMessages, isActive]);
|
||||||
|
|
||||||
|
const handleSearch = useLastCallback(() => {
|
||||||
|
const chatId = chat?.id;
|
||||||
|
if (!chatId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
runDebouncedForSearch(() => performMiddleSearch({ chatId, threadId, query }));
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleQueryChange = useLastCallback((newQuery: string) => {
|
||||||
|
if (newQuery.startsWith('#') && !isHashtagQuery) {
|
||||||
|
updateMiddleSearch({ chatId: chat!.id, threadId, update: { isHashtag: true } });
|
||||||
|
setQuery(newQuery.slice(1));
|
||||||
|
handleSearch();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setQuery(newQuery);
|
||||||
|
|
||||||
|
if (!newQuery) {
|
||||||
|
setIsLoading(false);
|
||||||
|
resetMiddleSearch();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (query) {
|
||||||
|
handleSearch();
|
||||||
|
}
|
||||||
|
}, [query]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setIsLoading(Boolean(fetchingQuery));
|
||||||
|
}, [fetchingQuery]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!foundIds?.length) return;
|
||||||
|
const isClose = foundIds.length - focusedIndex < FOCUSED_SEARCH_TRIGGER_OFFSET;
|
||||||
|
if (isClose) {
|
||||||
|
handleSearch();
|
||||||
|
}
|
||||||
|
}, [focusedIndex, foundIds?.length]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isActive) return undefined;
|
||||||
|
|
||||||
|
maybeLongPressActiveRef.current = true;
|
||||||
|
|
||||||
|
function focus() {
|
||||||
|
inputRef.current?.focus();
|
||||||
|
markFocused();
|
||||||
|
|
||||||
|
fastRaf(() => {
|
||||||
|
maybeLongPressActiveRef.current = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeListeners() {
|
||||||
|
window.removeEventListener('touchend', focus);
|
||||||
|
window.removeEventListener('mouseup', focus);
|
||||||
|
|
||||||
|
fastRaf(() => {
|
||||||
|
maybeLongPressActiveRef.current = false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('touchend', focus);
|
||||||
|
window.addEventListener('mouseup', focus);
|
||||||
|
|
||||||
|
window.addEventListener('touchstart', removeListeners);
|
||||||
|
window.addEventListener('mousedown', removeListeners);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
removeListeners();
|
||||||
|
window.removeEventListener('touchstart', removeListeners);
|
||||||
|
window.removeEventListener('mousedown', removeListeners);
|
||||||
|
};
|
||||||
|
}, [isActive]);
|
||||||
|
|
||||||
|
useHistoryBack({
|
||||||
|
isActive,
|
||||||
|
onBack: handleClose,
|
||||||
|
});
|
||||||
|
|
||||||
|
const [viewportIds, getMore, viewportOffset = 0] = useInfiniteScroll(handleSearch, foundIds);
|
||||||
|
|
||||||
|
const viewportResults = useMemo(() => {
|
||||||
|
if ((!query && !savedTag) || !viewportIds?.length) {
|
||||||
|
return MEMO_EMPTY_ARRAY;
|
||||||
|
}
|
||||||
|
const global = getGlobal();
|
||||||
|
|
||||||
|
return viewportIds.map((searchResultKey) => {
|
||||||
|
const [chatId, id] = parseSearchResultKey(searchResultKey);
|
||||||
|
const message = selectChatMessage(global, chatId, id);
|
||||||
|
if (!message) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const originalSender = (isSavedMessages || chatId === REPLIES_USER_ID || chatId === ANONYMOUS_USER_ID)
|
||||||
|
? selectForwardedSender(global, message) : undefined;
|
||||||
|
const messageSender = selectSender(global, message);
|
||||||
|
const messageChat = selectChat(global, message.chatId);
|
||||||
|
|
||||||
|
const senderPeer = originalSender || messageSender;
|
||||||
|
|
||||||
|
return {
|
||||||
|
searchResultKey,
|
||||||
|
message,
|
||||||
|
messageChat,
|
||||||
|
senderPeer,
|
||||||
|
};
|
||||||
|
}).filter(Boolean);
|
||||||
|
}, [query, savedTag, viewportIds, isSavedMessages]);
|
||||||
|
|
||||||
|
const handleMessageClick = useLastCallback((message: ApiMessage) => {
|
||||||
|
const searchResultKey = getSearchResultKey(message);
|
||||||
|
const index = foundIds?.indexOf(searchResultKey) || 0;
|
||||||
|
const realIndex = index + viewportOffset;
|
||||||
|
setFocusedIndex(realIndex);
|
||||||
|
|
||||||
|
if (searchType === 'chat') {
|
||||||
|
setIsViewAsList(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
focusMessage({
|
||||||
|
chatId: message.chatId,
|
||||||
|
messageId: message.id,
|
||||||
|
threadId: !isHashtagQuery ? threadId : undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
markBlurred();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleTriggerViewStyle = useLastCallback(() => {
|
||||||
|
setIsViewAsList((prev) => !prev);
|
||||||
|
markFocused();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleKeyDown = useKeyboardListNavigation(containerRef, hasResultsContainer, (index) => {
|
||||||
|
const foundResult = viewportResults?.[index === -1 ? 0 : index];
|
||||||
|
if (foundResult) {
|
||||||
|
handleMessageClick(foundResult.message);
|
||||||
|
setFocusedIndex(index + viewportOffset);
|
||||||
|
}
|
||||||
|
}, `.${RESULT_ITEM_CLASS_NAME}`, true);
|
||||||
|
|
||||||
|
const updateSearchParams = useLastCallback((update: Partial<MiddleSearchParams>) => {
|
||||||
|
updateMiddleSearch({ chatId: chat!.id, threadId, update });
|
||||||
|
|
||||||
|
handleSearch();
|
||||||
|
});
|
||||||
|
|
||||||
|
const activateSearchTag = useLastCallback((tag: ApiReaction) => {
|
||||||
|
if (areSavedTagsDisabled) {
|
||||||
|
openPremiumModal({
|
||||||
|
initialSection: 'saved_tags',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
updateSearchParams({ savedTag: tag });
|
||||||
|
});
|
||||||
|
|
||||||
|
const removeSearchSavedTag = useLastCallback(() => {
|
||||||
|
updateSearchParams({ savedTag: undefined });
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleDeleteTag = useLastCallback(() => {
|
||||||
|
if (isHashtagQuery) {
|
||||||
|
updateSearchParams({ isHashtag: false });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (savedTag) {
|
||||||
|
removeSearchSavedTag();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleChangeSearchType = useLastCallback((type: MiddleSearchType) => {
|
||||||
|
updateSearchParams({ type });
|
||||||
|
setIsViewAsList(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFocusOlder = useLastCallback(() => {
|
||||||
|
if (searchType !== 'chat') return;
|
||||||
|
markBlurred();
|
||||||
|
blurInput();
|
||||||
|
if (foundIds) {
|
||||||
|
const newFocusIndex = focusedIndex + 1;
|
||||||
|
const [chatId, messageId] = parseSearchResultKey(foundIds[newFocusIndex]);
|
||||||
|
focusMessage({ chatId, messageId, threadId });
|
||||||
|
setFocusedIndex(newFocusIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleFocusNewer = useLastCallback(() => {
|
||||||
|
if (searchType !== 'chat') return;
|
||||||
|
markBlurred();
|
||||||
|
blurInput();
|
||||||
|
if (foundIds) {
|
||||||
|
const newFocusIndex = focusedIndex - 1;
|
||||||
|
const [chatId, messageId] = parseSearchResultKey(foundIds[newFocusIndex]);
|
||||||
|
focusMessage({ chatId, messageId, threadId });
|
||||||
|
setFocusedIndex(newFocusIndex);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function renderTypeTag(type: MiddleSearchType, isForTag?: boolean) {
|
||||||
|
const isSelected = !isForTag && searchType === type;
|
||||||
|
switch (type) {
|
||||||
|
case 'chat':
|
||||||
|
return (
|
||||||
|
<PickerSelectedItem
|
||||||
|
className={buildClassName(styles.searchType, isSelected && styles.selectedType)}
|
||||||
|
fluid
|
||||||
|
peerId={chat?.id}
|
||||||
|
title={oldLang('SearchThisChat')}
|
||||||
|
clickArg="chat"
|
||||||
|
onClick={isForTag ? handleDeleteTag : handleChangeSearchType}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'myChats':
|
||||||
|
return (
|
||||||
|
<PickerSelectedItem
|
||||||
|
className={buildClassName(styles.searchType, isSelected && styles.selectedType)}
|
||||||
|
fluid
|
||||||
|
peerId={currentUserId}
|
||||||
|
forceShowSelf
|
||||||
|
title={oldLang('SearchMyMessages')}
|
||||||
|
clickArg="myChats"
|
||||||
|
onClick={isForTag ? handleDeleteTag : handleChangeSearchType}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case 'channels':
|
||||||
|
return (
|
||||||
|
<PickerSelectedItem
|
||||||
|
className={buildClassName(styles.searchType, isSelected && styles.selectedType)}
|
||||||
|
fluid
|
||||||
|
customPeer={CHANNELS_PEER}
|
||||||
|
title={oldLang('SearchPublicPosts')}
|
||||||
|
clickArg="channels"
|
||||||
|
onClick={isForTag ? handleDeleteTag : handleChangeSearchType}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDropdown() {
|
||||||
|
return (
|
||||||
|
<div className={buildClassName(styles.dropdown, !hasResultsDropdown && styles.dropdownHidden)}>
|
||||||
|
{!isMobile && <div className={styles.separator} />}
|
||||||
|
{hasSavedTags && !isHashtagQuery && (
|
||||||
|
<div
|
||||||
|
className={buildClassName(
|
||||||
|
styles.savedTags,
|
||||||
|
!isMobile && styles.wrap,
|
||||||
|
'no-scrollbar',
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{savedTagsArray.map((tag) => {
|
||||||
|
const isChosen = isSameReaction(tag.reaction, savedTag);
|
||||||
|
return (
|
||||||
|
<SavedTagButton
|
||||||
|
containerId="local-search"
|
||||||
|
key={getReactionKey(tag.reaction)}
|
||||||
|
reaction={tag.reaction}
|
||||||
|
tag={tag}
|
||||||
|
withCount
|
||||||
|
isDisabled={areSavedTagsDisabled}
|
||||||
|
isChosen={isChosen}
|
||||||
|
onClick={isChosen ? removeSearchSavedTag : activateSearchTag}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{isHashtagQuery && (
|
||||||
|
<div
|
||||||
|
className={buildClassName(styles.searchTypes, 'no-scrollbar')}
|
||||||
|
>
|
||||||
|
{renderTypeTag('chat')}
|
||||||
|
{renderTypeTag('myChats')}
|
||||||
|
{renderTypeTag('channels')}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{hasResultsContainer && (
|
||||||
|
<InfiniteScroll
|
||||||
|
ref={containerRef}
|
||||||
|
className={buildClassName(styles.results, 'custom-scroll')}
|
||||||
|
items={viewportResults}
|
||||||
|
preloadBackwards={0}
|
||||||
|
onLoadMore={getMore}
|
||||||
|
onKeyDown={handleKeyDown}
|
||||||
|
>
|
||||||
|
{areResultsEmpty && (
|
||||||
|
<span key="nothing" className={styles.placeholder}>
|
||||||
|
{oldLang('NoResultFoundFor', query)}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{isOnlyHash && (
|
||||||
|
<span key="enterhash" className={styles.placeholder}>
|
||||||
|
{oldLang('HashtagSearchPlaceholder')}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{viewportResults?.map(({
|
||||||
|
message, senderPeer, messageChat, searchResultKey,
|
||||||
|
}, i) => (
|
||||||
|
<MiddleSearchResult
|
||||||
|
key={searchResultKey}
|
||||||
|
teactOrderKey={-message.date}
|
||||||
|
className={RESULT_ITEM_CLASS_NAME}
|
||||||
|
query={query}
|
||||||
|
message={message}
|
||||||
|
senderPeer={senderPeer}
|
||||||
|
messageChat={messageChat}
|
||||||
|
shouldShowChat={isHashtagQuery}
|
||||||
|
isActive={focusedIndex - viewportOffset === i}
|
||||||
|
onClick={handleMessageClick}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</InfiniteScroll>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
id="MiddleSearch"
|
||||||
|
className={buildClassName(
|
||||||
|
styles.root,
|
||||||
|
isActive && styles.active,
|
||||||
|
!isActive && isFullyHidden && 'visually-hidden', // `display: none` would prevent focus on iOS
|
||||||
|
isFocused && styles.focused,
|
||||||
|
isMobile && styles.mobile,
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
|
<div className={styles.header}>
|
||||||
|
{!isMobile && (
|
||||||
|
<Avatar
|
||||||
|
className={styles.avatar}
|
||||||
|
peer={chat}
|
||||||
|
size="medium"
|
||||||
|
isSavedMessages={isSavedMessages}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<SearchInput
|
||||||
|
ref={inputRef}
|
||||||
|
value={query}
|
||||||
|
className={buildClassName(
|
||||||
|
styles.input,
|
||||||
|
hasResultsDropdown && styles.withDropdown,
|
||||||
|
hasResultsDropdown && !isMobile && styles.adaptSearchBorders,
|
||||||
|
)}
|
||||||
|
canClose={!isMobile}
|
||||||
|
isLoading={isLoading}
|
||||||
|
resultsItemSelector={`.${styles.results} .${RESULT_ITEM_CLASS_NAME}`}
|
||||||
|
hasUpButton={hasNavigationButtons && !isMobile}
|
||||||
|
hasDownButton={hasNavigationButtons && !isMobile}
|
||||||
|
placeholder={isHashtagQuery ? oldLang('SearchHashtagsHint') : oldLang('Search')}
|
||||||
|
teactExperimentControlled
|
||||||
|
onChange={handleQueryChange}
|
||||||
|
onStartBackspace={handleDeleteTag}
|
||||||
|
onReset={handleReset}
|
||||||
|
withBackIcon={isMobile}
|
||||||
|
onFocus={markFocused}
|
||||||
|
focused={isFocused}
|
||||||
|
onUpClick={canFocusOlder ? handleFocusOlder : undefined}
|
||||||
|
onDownClick={canFocusNewer ? handleFocusNewer : undefined}
|
||||||
|
>
|
||||||
|
<div className={styles.searchTags}>
|
||||||
|
{savedTag && (
|
||||||
|
<SavedTagButton
|
||||||
|
containerId="local-search-tags"
|
||||||
|
className={styles.savedSearchTag}
|
||||||
|
reaction={savedTag}
|
||||||
|
tag={savedTags![getReactionKey(savedTag)]}
|
||||||
|
onClick={removeSearchSavedTag}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{isHashtagQuery && <div className={styles.hash}>#</div>}
|
||||||
|
</div>
|
||||||
|
{!isMobile && renderDropdown()}
|
||||||
|
</SearchInput>
|
||||||
|
{!isMobile && (
|
||||||
|
<div className={styles.icons}>
|
||||||
|
<Button
|
||||||
|
round
|
||||||
|
size="smaller"
|
||||||
|
color="translucent"
|
||||||
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
|
onClick={() => openHistoryCalendar({ selectedAt: getDayStartAt(Date.now()) })}
|
||||||
|
ariaLabel={oldLang('JumpToDate')}
|
||||||
|
>
|
||||||
|
<Icon name="calendar" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{isMobile && renderDropdown()}
|
||||||
|
{isMobile && (
|
||||||
|
<div className={styles.footer}>
|
||||||
|
<Button
|
||||||
|
round
|
||||||
|
size="smaller"
|
||||||
|
color="translucent"
|
||||||
|
// eslint-disable-next-line react/jsx-no-bind
|
||||||
|
onClick={() => openHistoryCalendar({ selectedAt: getDayStartAt(Date.now()) })}
|
||||||
|
ariaLabel={oldLang('JumpToDate')}
|
||||||
|
>
|
||||||
|
<Icon name="calendar" />
|
||||||
|
</Button>
|
||||||
|
<div className={styles.counter}>
|
||||||
|
{hasQueryData && (
|
||||||
|
foundIds?.length ? (
|
||||||
|
oldLang('Of', [focusedIndex + 1, totalCount])
|
||||||
|
) : foundIds && !foundIds.length && (
|
||||||
|
oldLang('NoResult')
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{searchType === 'chat' && Boolean(foundIds?.length) && (
|
||||||
|
<Button
|
||||||
|
className={styles.viewStyle}
|
||||||
|
size="smaller"
|
||||||
|
isText
|
||||||
|
fluid
|
||||||
|
noForcedUpperCase
|
||||||
|
onClick={handleTriggerViewStyle}
|
||||||
|
>
|
||||||
|
{isViewAsList ? oldLang('SearchAsChat') : oldLang('SearchAsList')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{hasNavigationButtons && !hasResultsDropdown && (
|
||||||
|
<div className={styles.mobileNavigation}>
|
||||||
|
<Button
|
||||||
|
className={buildClassName(styles.navigationButton, !canFocusOlder && styles.navigationDisabled)}
|
||||||
|
round
|
||||||
|
size="smaller"
|
||||||
|
color="secondary"
|
||||||
|
onClick={handleFocusOlder}
|
||||||
|
nonInteractive={!canFocusOlder}
|
||||||
|
ariaLabel={lang('AriaSearchOlderResult')}
|
||||||
|
>
|
||||||
|
<Icon name="up" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className={buildClassName(styles.navigationButton, !canFocusNewer && styles.navigationDisabled)}
|
||||||
|
round
|
||||||
|
size="smaller"
|
||||||
|
color="secondary"
|
||||||
|
onClick={handleFocusNewer}
|
||||||
|
nonInteractive={!canFocusNewer}
|
||||||
|
ariaLabel={lang('AriaSearchNewerResult')}
|
||||||
|
>
|
||||||
|
<Icon name="down" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global): StateProps => {
|
||||||
|
const currentMessageList = selectCurrentMessageList(global);
|
||||||
|
if (!currentMessageList) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const { chatId, threadId } = currentMessageList;
|
||||||
|
|
||||||
|
const chat = selectChat(global, chatId);
|
||||||
|
if (!chat) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
const {
|
||||||
|
requestedQuery, savedTag, results, fetchingQuery, isHashtag, type,
|
||||||
|
} = selectCurrentMiddleSearch(global) || {};
|
||||||
|
const { totalCount, foundIds, query: lastSearchQuery } = results || {};
|
||||||
|
|
||||||
|
const currentUserId = global.currentUserId;
|
||||||
|
const isSavedMessages = selectIsChatWithSelf(global, chatId);
|
||||||
|
const isSavedDialog = getIsSavedDialog(chatId, threadId, currentUserId);
|
||||||
|
|
||||||
|
const savedTags = isSavedMessages && !isSavedDialog ? global.savedReactionTags?.byKey : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
chat,
|
||||||
|
requestedQuery,
|
||||||
|
totalCount,
|
||||||
|
threadId,
|
||||||
|
foundIds,
|
||||||
|
isHistoryCalendarOpen: Boolean(selectTabState(global).historyCalendarSelectedAt),
|
||||||
|
savedTags,
|
||||||
|
savedTag,
|
||||||
|
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
||||||
|
isSavedMessages,
|
||||||
|
fetchingQuery,
|
||||||
|
isHashtagQuery: isHashtag,
|
||||||
|
currentUserId,
|
||||||
|
searchType: type,
|
||||||
|
lastSearchQuery,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(MiddleSearch));
|
||||||
57
src/components/middle/search/MiddleSearchResult.module.scss
Normal file
57
src/components/middle/search/MiddleSearchResult.module.scss
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
.root {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.375rem 0.75rem;
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
outline: none;
|
||||||
|
|
||||||
|
color: var(--color-text);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
cursor: var(--custom-cursor, pointer);
|
||||||
|
|
||||||
|
@media (hover: hover) {
|
||||||
|
&:hover,
|
||||||
|
&:focus-visible,
|
||||||
|
&.active {
|
||||||
|
background-color: var(--color-chat-hover);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global {
|
||||||
|
.matching-text-highlight {
|
||||||
|
color: var(--color-text);
|
||||||
|
background: #cae3f7;
|
||||||
|
border-radius: 0.25rem;
|
||||||
|
padding: 0 0.125rem;
|
||||||
|
display: inline-block;
|
||||||
|
|
||||||
|
:global(.theme-dark) & {
|
||||||
|
--color-text: #000;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.info {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-grow: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.topRow {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
margin-inline-start: auto;
|
||||||
|
padding-inline-start: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.subtitle {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
85
src/components/middle/search/MiddleSearchResult.tsx
Normal file
85
src/components/middle/search/MiddleSearchResult.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import React, { memo } from '../../../lib/teact/teact';
|
||||||
|
|
||||||
|
import type { ApiChat, ApiMessage, ApiPeer } from '../../../api/types';
|
||||||
|
|
||||||
|
import { getMessageSenderName } from '../../../global/helpers';
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import renderText from '../../common/helpers/renderText';
|
||||||
|
|
||||||
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
|
import useOldLang from '../../../hooks/useOldLang';
|
||||||
|
|
||||||
|
import Avatar from '../../common/Avatar';
|
||||||
|
import FullNameTitle from '../../common/FullNameTitle';
|
||||||
|
import LastMessageMeta from '../../common/LastMessageMeta';
|
||||||
|
import MessageSummary from '../../common/MessageSummary';
|
||||||
|
|
||||||
|
import styles from './MiddleSearchResult.module.scss';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
isActive?: boolean;
|
||||||
|
message: ApiMessage;
|
||||||
|
senderPeer?: ApiPeer;
|
||||||
|
messageChat?: ApiChat;
|
||||||
|
shouldShowChat?: boolean;
|
||||||
|
query?: string;
|
||||||
|
className?: string;
|
||||||
|
onClick: (message: ApiMessage) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
const TRUNCATE_LENGTH = 200;
|
||||||
|
|
||||||
|
const MiddleSearchResult = ({
|
||||||
|
isActive,
|
||||||
|
message,
|
||||||
|
senderPeer,
|
||||||
|
messageChat,
|
||||||
|
shouldShowChat,
|
||||||
|
query,
|
||||||
|
className,
|
||||||
|
onClick,
|
||||||
|
}: OwnProps) => {
|
||||||
|
const lang = useOldLang();
|
||||||
|
const hiddenForwardTitle = message.forwardInfo?.hiddenUserName;
|
||||||
|
|
||||||
|
const peer = shouldShowChat ? messageChat : senderPeer;
|
||||||
|
|
||||||
|
const senderName = shouldShowChat ? getMessageSenderName(lang, message.chatId, senderPeer) : undefined;
|
||||||
|
|
||||||
|
const handleClick = useLastCallback(() => {
|
||||||
|
onClick(message);
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
role="button"
|
||||||
|
tabIndex={0}
|
||||||
|
className={buildClassName(styles.root, isActive && styles.active, className)}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<Avatar
|
||||||
|
className={styles.avatar}
|
||||||
|
peer={peer}
|
||||||
|
text={hiddenForwardTitle}
|
||||||
|
size="medium"
|
||||||
|
/>
|
||||||
|
<div className={styles.info}>
|
||||||
|
<div className={styles.topRow}>
|
||||||
|
{(peer && <FullNameTitle peer={peer} withEmojiStatus />) || hiddenForwardTitle}
|
||||||
|
<LastMessageMeta className={styles.meta} message={message} />
|
||||||
|
</div>
|
||||||
|
<div className={styles.subtitle} dir="auto">
|
||||||
|
{senderName && (
|
||||||
|
<>
|
||||||
|
<span className="sender-name">{renderText(senderName)}</span>
|
||||||
|
<span className="colon">:</span>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
<MessageSummary message={message} highlight={query} truncateLength={TRUNCATE_LENGTH} />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(MiddleSearchResult);
|
||||||
@ -128,6 +128,7 @@ const CollectibleInfoModal: FC<OwnProps & StateProps> = ({
|
|||||||
className={styles.chip}
|
className={styles.chip}
|
||||||
peerId={modal?.peerId}
|
peerId={modal?.peerId}
|
||||||
forceShowSelf
|
forceShowSelf
|
||||||
|
clickArg={modal?.peerId}
|
||||||
onClick={handleOpenChat}
|
onClick={handleOpenChat}
|
||||||
/>
|
/>
|
||||||
<p className={styles.description}>
|
<p className={styles.description}>
|
||||||
|
|||||||
@ -33,7 +33,6 @@ import Management from './management/Management.async';
|
|||||||
import PollResults from './PollResults.async';
|
import PollResults from './PollResults.async';
|
||||||
import Profile from './Profile';
|
import Profile from './Profile';
|
||||||
import RightHeader from './RightHeader';
|
import RightHeader from './RightHeader';
|
||||||
import RightSearch from './RightSearch.async';
|
|
||||||
import BoostStatistics from './statistics/BoostStatistics';
|
import BoostStatistics from './statistics/BoostStatistics';
|
||||||
import MessageStatistics from './statistics/MessageStatistics.async';
|
import MessageStatistics from './statistics/MessageStatistics.async';
|
||||||
import Statistics from './statistics/Statistics.async';
|
import Statistics from './statistics/Statistics.async';
|
||||||
@ -87,7 +86,6 @@ const RightColumn: FC<OwnProps & StateProps> = ({
|
|||||||
const {
|
const {
|
||||||
toggleChatInfo,
|
toggleChatInfo,
|
||||||
toggleManagement,
|
toggleManagement,
|
||||||
closeLocalTextSearch,
|
|
||||||
setStickerSearchQuery,
|
setStickerSearchQuery,
|
||||||
setGifSearchQuery,
|
setGifSearchQuery,
|
||||||
closePollResults,
|
closePollResults,
|
||||||
@ -117,7 +115,6 @@ const RightColumn: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
const isOpen = contentKey !== undefined;
|
const isOpen = contentKey !== undefined;
|
||||||
const isProfile = contentKey === RightColumnContent.ChatInfo;
|
const isProfile = contentKey === RightColumnContent.ChatInfo;
|
||||||
const isSearch = contentKey === RightColumnContent.Search;
|
|
||||||
const isManagement = contentKey === RightColumnContent.Management;
|
const isManagement = contentKey === RightColumnContent.Management;
|
||||||
const isStatistics = contentKey === RightColumnContent.Statistics;
|
const isStatistics = contentKey === RightColumnContent.Statistics;
|
||||||
const isMessageStatistics = contentKey === RightColumnContent.MessageStatistics;
|
const isMessageStatistics = contentKey === RightColumnContent.MessageStatistics;
|
||||||
@ -200,11 +197,6 @@ const RightColumn: FC<OwnProps & StateProps> = ({
|
|||||||
case RightColumnContent.BoostStatistics:
|
case RightColumnContent.BoostStatistics:
|
||||||
closeBoostStatistics();
|
closeBoostStatistics();
|
||||||
break;
|
break;
|
||||||
case RightColumnContent.Search: {
|
|
||||||
blurSearchInput();
|
|
||||||
closeLocalTextSearch();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
case RightColumnContent.StickerSearch:
|
case RightColumnContent.StickerSearch:
|
||||||
blurSearchInput();
|
blurSearchInput();
|
||||||
setStickerSearchQuery({ query: undefined });
|
setStickerSearchQuery({ query: undefined });
|
||||||
@ -318,16 +310,6 @@ const RightColumn: FC<OwnProps & StateProps> = ({
|
|||||||
onProfileStateChange={setProfileState}
|
onProfileStateChange={setProfileState}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
case RightColumnContent.Search:
|
|
||||||
return (
|
|
||||||
<RightSearch
|
|
||||||
key={`right_search_${chatId!}`}
|
|
||||||
chatId={chatId!}
|
|
||||||
threadId={threadId!}
|
|
||||||
onClose={close}
|
|
||||||
isActive={isOpen && isActive}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case RightColumnContent.Management:
|
case RightColumnContent.Management:
|
||||||
return (
|
return (
|
||||||
<Management
|
<Management
|
||||||
@ -380,7 +362,6 @@ const RightColumn: FC<OwnProps & StateProps> = ({
|
|||||||
threadId={threadId}
|
threadId={threadId}
|
||||||
isColumnOpen={isOpen}
|
isColumnOpen={isOpen}
|
||||||
isProfile={isProfile}
|
isProfile={isProfile}
|
||||||
isSearch={isSearch}
|
|
||||||
isManagement={isManagement}
|
isManagement={isManagement}
|
||||||
isStatistics={isStatistics}
|
isStatistics={isStatistics}
|
||||||
isBoostStatistics={isBoostStatistics}
|
isBoostStatistics={isBoostStatistics}
|
||||||
|
|||||||
@ -16,14 +16,11 @@ import {
|
|||||||
selectChatFullInfo,
|
selectChatFullInfo,
|
||||||
selectCurrentGifSearch,
|
selectCurrentGifSearch,
|
||||||
selectCurrentStickerSearch,
|
selectCurrentStickerSearch,
|
||||||
selectCurrentTextSearch,
|
|
||||||
selectIsChatWithSelf,
|
selectIsChatWithSelf,
|
||||||
selectTabState,
|
selectTabState,
|
||||||
selectUser,
|
selectUser,
|
||||||
} from '../../global/selectors';
|
} from '../../global/selectors';
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
import { getDayStartAt } from '../../util/dates/dateFormat';
|
|
||||||
import { debounce } from '../../util/schedulers';
|
|
||||||
|
|
||||||
import useAppLayout from '../../hooks/useAppLayout';
|
import useAppLayout from '../../hooks/useAppLayout';
|
||||||
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
|
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
|
||||||
@ -46,7 +43,6 @@ type OwnProps = {
|
|||||||
threadId?: ThreadId;
|
threadId?: ThreadId;
|
||||||
isColumnOpen?: boolean;
|
isColumnOpen?: boolean;
|
||||||
isProfile?: boolean;
|
isProfile?: boolean;
|
||||||
isSearch?: boolean;
|
|
||||||
isManagement?: boolean;
|
isManagement?: boolean;
|
||||||
isStatistics?: boolean;
|
isStatistics?: boolean;
|
||||||
isBoostStatistics?: boolean;
|
isBoostStatistics?: boolean;
|
||||||
@ -71,7 +67,6 @@ type StateProps = {
|
|||||||
isChannel?: boolean;
|
isChannel?: boolean;
|
||||||
userId?: string;
|
userId?: string;
|
||||||
isSelf?: boolean;
|
isSelf?: boolean;
|
||||||
messageSearchQuery?: string;
|
|
||||||
stickerSearchQuery?: string;
|
stickerSearchQuery?: string;
|
||||||
gifSearchQuery?: string;
|
gifSearchQuery?: string;
|
||||||
isEditingInvite?: boolean;
|
isEditingInvite?: boolean;
|
||||||
@ -85,7 +80,6 @@ type StateProps = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const COLUMN_ANIMATION_DURATION = 450 + ANIMATION_END_DELAY;
|
const COLUMN_ANIMATION_DURATION = 450 + ANIMATION_END_DELAY;
|
||||||
const runDebouncedForSearch = debounce((cb) => cb(), 200, false);
|
|
||||||
|
|
||||||
enum HeaderContent {
|
enum HeaderContent {
|
||||||
Profile,
|
Profile,
|
||||||
@ -132,7 +126,6 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
|||||||
threadId,
|
threadId,
|
||||||
isColumnOpen,
|
isColumnOpen,
|
||||||
isProfile,
|
isProfile,
|
||||||
isSearch,
|
|
||||||
isManagement,
|
isManagement,
|
||||||
isStatistics,
|
isStatistics,
|
||||||
isMessageStatistics,
|
isMessageStatistics,
|
||||||
@ -151,7 +144,6 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
|||||||
isSelf,
|
isSelf,
|
||||||
canManage,
|
canManage,
|
||||||
isChannel,
|
isChannel,
|
||||||
messageSearchQuery,
|
|
||||||
stickerSearchQuery,
|
stickerSearchQuery,
|
||||||
gifSearchQuery,
|
gifSearchQuery,
|
||||||
isEditingInvite,
|
isEditingInvite,
|
||||||
@ -167,12 +159,9 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
|||||||
canEditBot,
|
canEditBot,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
setLocalTextSearchQuery,
|
|
||||||
setStickerSearchQuery,
|
setStickerSearchQuery,
|
||||||
setGifSearchQuery,
|
setGifSearchQuery,
|
||||||
searchTextMessagesLocal,
|
|
||||||
toggleManagement,
|
toggleManagement,
|
||||||
openHistoryCalendar,
|
|
||||||
openAddContactDialog,
|
openAddContactDialog,
|
||||||
toggleStatistics,
|
toggleStatistics,
|
||||||
setEditingExportedInvite,
|
setEditingExportedInvite,
|
||||||
@ -196,14 +185,6 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
|||||||
closeDeleteDialog();
|
closeDeleteDialog();
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleMessageSearchQueryChange = useLastCallback((query: string) => {
|
|
||||||
setLocalTextSearchQuery({ query });
|
|
||||||
|
|
||||||
if (query.length) {
|
|
||||||
runDebouncedForSearch(searchTextMessagesLocal);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleStickerSearchQueryChange = useLastCallback((query: string) => {
|
const handleStickerSearchQueryChange = useLastCallback((query: string) => {
|
||||||
setStickerSearchQuery({ query });
|
setStickerSearchQuery({ query });
|
||||||
});
|
});
|
||||||
@ -254,8 +235,6 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
|||||||
) : profileState === ProfileState.SavedDialogs ? (
|
) : profileState === ProfileState.SavedDialogs ? (
|
||||||
HeaderContent.SavedDialogs
|
HeaderContent.SavedDialogs
|
||||||
) : -1 // Never reached
|
) : -1 // Never reached
|
||||||
) : isSearch ? (
|
|
||||||
HeaderContent.Search
|
|
||||||
) : isPollResults ? (
|
) : isPollResults ? (
|
||||||
HeaderContent.PollResults
|
HeaderContent.PollResults
|
||||||
) : isStickerSearch ? (
|
) : isStickerSearch ? (
|
||||||
@ -350,26 +329,6 @@ const RightHeader: FC<OwnProps & StateProps> = ({
|
|||||||
switch (renderingContentKey) {
|
switch (renderingContentKey) {
|
||||||
case HeaderContent.PollResults:
|
case HeaderContent.PollResults:
|
||||||
return <h3 className="title">{lang('PollResults')}</h3>;
|
return <h3 className="title">{lang('PollResults')}</h3>;
|
||||||
case HeaderContent.Search:
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<SearchInput
|
|
||||||
parentContainerClassName="RightSearch"
|
|
||||||
value={messageSearchQuery}
|
|
||||||
onChange={handleMessageSearchQueryChange}
|
|
||||||
/>
|
|
||||||
<Button
|
|
||||||
round
|
|
||||||
size="smaller"
|
|
||||||
color="translucent"
|
|
||||||
// eslint-disable-next-line react/jsx-no-bind
|
|
||||||
onClick={() => openHistoryCalendar({ selectedAt: getDayStartAt(Date.now()) })}
|
|
||||||
ariaLabel="Search messages by date"
|
|
||||||
>
|
|
||||||
<i className="icon icon-calendar" />
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
case HeaderContent.AddingMembers:
|
case HeaderContent.AddingMembers:
|
||||||
return <h3 className="title">{lang(isChannel ? 'ChannelAddSubscribers' : 'GroupAddMembers')}</h3>;
|
return <h3 className="title">{lang(isChannel ? 'ChannelAddSubscribers' : 'GroupAddMembers')}</h3>;
|
||||||
case HeaderContent.ManageInitial:
|
case HeaderContent.ManageInitial:
|
||||||
@ -610,7 +569,6 @@ export default withGlobal<OwnProps>(
|
|||||||
chatId, isProfile, isManagement, threadId,
|
chatId, isProfile, isManagement, threadId,
|
||||||
}): StateProps => {
|
}): StateProps => {
|
||||||
const tabState = selectTabState(global);
|
const tabState = selectTabState(global);
|
||||||
const { query: messageSearchQuery } = selectCurrentTextSearch(global) || {};
|
|
||||||
const { query: stickerSearchQuery } = selectCurrentStickerSearch(global) || {};
|
const { query: stickerSearchQuery } = selectCurrentStickerSearch(global) || {};
|
||||||
const { query: gifSearchQuery } = selectCurrentGifSearch(global) || {};
|
const { query: gifSearchQuery } = selectCurrentGifSearch(global) || {};
|
||||||
const chat = chatId ? selectChat(global, chatId) : undefined;
|
const chat = chatId ? selectChat(global, chatId) : undefined;
|
||||||
@ -643,7 +601,6 @@ export default withGlobal<OwnProps>(
|
|||||||
canEditTopic,
|
canEditTopic,
|
||||||
userId: user?.id,
|
userId: user?.id,
|
||||||
isSelf: user?.isSelf,
|
isSelf: user?.isSelf,
|
||||||
messageSearchQuery,
|
|
||||||
stickerSearchQuery,
|
stickerSearchQuery,
|
||||||
gifSearchQuery,
|
gifSearchQuery,
|
||||||
isEditingInvite,
|
isEditingInvite,
|
||||||
|
|||||||
@ -1,19 +0,0 @@
|
|||||||
import type { FC } from '../../lib/teact/teact';
|
|
||||||
import React from '../../lib/teact/teact';
|
|
||||||
|
|
||||||
import type { OwnProps } from './RightSearch';
|
|
||||||
|
|
||||||
import { Bundles } from '../../util/moduleLoader';
|
|
||||||
|
|
||||||
import useModuleLoader from '../../hooks/useModuleLoader';
|
|
||||||
|
|
||||||
import Loading from '../ui/Loading';
|
|
||||||
|
|
||||||
const RightSearchAsync: FC<OwnProps> = (props) => {
|
|
||||||
const RightSearch = useModuleLoader(Bundles.Extra, 'RightSearch');
|
|
||||||
|
|
||||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
|
||||||
return RightSearch ? <RightSearch {...props} /> : <Loading />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default RightSearchAsync;
|
|
||||||
@ -1,29 +0,0 @@
|
|||||||
.RightSearch {
|
|
||||||
height: 100%;
|
|
||||||
padding: 0 0.5rem;
|
|
||||||
overflow-y: auto;
|
|
||||||
|
|
||||||
.helper-text {
|
|
||||||
padding: 1rem;
|
|
||||||
margin-bottom: 0.125rem;
|
|
||||||
font-weight: 500;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
unicode-bidi: plaintext;
|
|
||||||
text-align: initial;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-tags {
|
|
||||||
--color-reaction: var(--color-background-secondary);
|
|
||||||
--hover-color-reaction: var(--color-background-secondary-accent);
|
|
||||||
--text-color-reaction: var(--color-text-secondary);
|
|
||||||
--color-reaction-chosen: var(--color-primary);
|
|
||||||
--text-color-reaction-chosen: #FFFFFF;
|
|
||||||
--hover-color-reaction-chosen: var(--color-primary-shade);
|
|
||||||
|
|
||||||
display: flex;
|
|
||||||
overflow-x: scroll;
|
|
||||||
|
|
||||||
margin-top: 0.25rem;
|
|
||||||
gap: 0.375rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,290 +0,0 @@
|
|||||||
import type { FC } from '../../lib/teact/teact';
|
|
||||||
import React, {
|
|
||||||
memo, useEffect, useMemo, useRef,
|
|
||||||
} from '../../lib/teact/teact';
|
|
||||||
import { getActions, getGlobal, withGlobal } from '../../global';
|
|
||||||
|
|
||||||
import type {
|
|
||||||
ApiMessage, ApiPeer, ApiReaction, ApiReactionKey, ApiSavedReactionTag,
|
|
||||||
} from '../../api/types';
|
|
||||||
import type { ThreadId } from '../../types';
|
|
||||||
|
|
||||||
import { ANONYMOUS_USER_ID, REPLIES_USER_ID } from '../../config';
|
|
||||||
import { getIsSavedDialog, getReactionKey, isSameReaction } from '../../global/helpers';
|
|
||||||
import {
|
|
||||||
selectChatMessages,
|
|
||||||
selectCurrentTextSearch,
|
|
||||||
selectForwardedSender,
|
|
||||||
selectIsChatWithSelf,
|
|
||||||
selectIsCurrentUserPremium,
|
|
||||||
selectSender,
|
|
||||||
} from '../../global/selectors';
|
|
||||||
import { disableDirectTextInput, enableDirectTextInput } from '../../util/directInputManager';
|
|
||||||
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
|
|
||||||
import { debounce } from '../../util/schedulers';
|
|
||||||
import { renderMessageSummary } from '../common/helpers/renderMessageText';
|
|
||||||
|
|
||||||
import useHistoryBack from '../../hooks/useHistoryBack';
|
|
||||||
import useHorizontalScroll from '../../hooks/useHorizontalScroll';
|
|
||||||
import useInfiniteScroll from '../../hooks/useInfiniteScroll';
|
|
||||||
import useKeyboardListNavigation from '../../hooks/useKeyboardListNavigation';
|
|
||||||
import useLastCallback from '../../hooks/useLastCallback';
|
|
||||||
import useOldLang from '../../hooks/useOldLang';
|
|
||||||
|
|
||||||
import Avatar from '../common/Avatar';
|
|
||||||
import FullNameTitle from '../common/FullNameTitle';
|
|
||||||
import LastMessageMeta from '../common/LastMessageMeta';
|
|
||||||
import SavedTagButton from '../middle/message/reactions/SavedTagButton';
|
|
||||||
import InfiniteScroll from '../ui/InfiniteScroll';
|
|
||||||
import ListItem from '../ui/ListItem';
|
|
||||||
|
|
||||||
import './RightSearch.scss';
|
|
||||||
|
|
||||||
export type OwnProps = {
|
|
||||||
chatId: string;
|
|
||||||
threadId: ThreadId;
|
|
||||||
onClose: NoneToVoidFunction;
|
|
||||||
isActive: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
type StateProps = {
|
|
||||||
messagesById?: Record<number, ApiMessage>;
|
|
||||||
query?: string;
|
|
||||||
savedTags?: Record<ApiReactionKey, ApiSavedReactionTag>;
|
|
||||||
searchTag?: ApiReaction;
|
|
||||||
totalCount?: number;
|
|
||||||
foundIds?: number[];
|
|
||||||
isSavedMessages?: boolean;
|
|
||||||
isCurrentUserPremium?: boolean;
|
|
||||||
};
|
|
||||||
|
|
||||||
const runDebouncedForSearch = debounce((cb) => cb(), 200, false);
|
|
||||||
|
|
||||||
const RightSearch: FC<OwnProps & StateProps> = ({
|
|
||||||
chatId,
|
|
||||||
threadId,
|
|
||||||
isActive,
|
|
||||||
messagesById,
|
|
||||||
query,
|
|
||||||
totalCount,
|
|
||||||
foundIds,
|
|
||||||
savedTags,
|
|
||||||
searchTag,
|
|
||||||
isSavedMessages,
|
|
||||||
isCurrentUserPremium,
|
|
||||||
onClose,
|
|
||||||
}) => {
|
|
||||||
const {
|
|
||||||
searchTextMessagesLocal,
|
|
||||||
setLocalTextSearchTag,
|
|
||||||
focusMessage,
|
|
||||||
openPremiumModal,
|
|
||||||
loadSavedReactionTags,
|
|
||||||
} = getActions();
|
|
||||||
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
|
||||||
const tagsRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
const lang = useOldLang();
|
|
||||||
useHistoryBack({
|
|
||||||
isActive,
|
|
||||||
onBack: onClose,
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!isActive) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
disableDirectTextInput();
|
|
||||||
|
|
||||||
return enableDirectTextInput;
|
|
||||||
}, [isActive]);
|
|
||||||
|
|
||||||
const tags = useMemo(() => {
|
|
||||||
if (!savedTags) return undefined;
|
|
||||||
return Object.values(savedTags);
|
|
||||||
}, [savedTags]);
|
|
||||||
|
|
||||||
const hasTags = Boolean(tags?.length);
|
|
||||||
const areTagsDisabled = hasTags && !isCurrentUserPremium;
|
|
||||||
|
|
||||||
useHorizontalScroll(tagsRef, !hasTags);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (isActive) loadSavedReactionTags();
|
|
||||||
}, [hasTags, isActive]);
|
|
||||||
|
|
||||||
const handleSearchTextMessagesLocal = useLastCallback(() => {
|
|
||||||
runDebouncedForSearch(searchTextMessagesLocal);
|
|
||||||
});
|
|
||||||
|
|
||||||
const handleTagClick = useLastCallback((tag: ApiReaction) => {
|
|
||||||
if (areTagsDisabled) {
|
|
||||||
openPremiumModal({
|
|
||||||
initialSection: 'saved_tags',
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isSameReaction(tag, searchTag)) {
|
|
||||||
setLocalTextSearchTag({ tag: undefined });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
setLocalTextSearchTag({ tag });
|
|
||||||
handleSearchTextMessagesLocal();
|
|
||||||
});
|
|
||||||
|
|
||||||
const [viewportIds, getMore] = useInfiniteScroll(handleSearchTextMessagesLocal, foundIds);
|
|
||||||
|
|
||||||
const viewportResults = useMemo(() => {
|
|
||||||
if ((!query && !searchTag) || !viewportIds?.length || !messagesById) {
|
|
||||||
return MEMO_EMPTY_ARRAY;
|
|
||||||
}
|
|
||||||
|
|
||||||
return viewportIds.map((id) => {
|
|
||||||
const message = messagesById[id];
|
|
||||||
if (!message) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const global = getGlobal();
|
|
||||||
|
|
||||||
const originalSender = (isSavedMessages || chatId === REPLIES_USER_ID || chatId === ANONYMOUS_USER_ID)
|
|
||||||
? selectForwardedSender(global, message) : undefined;
|
|
||||||
const messageSender = selectSender(global, message);
|
|
||||||
|
|
||||||
const senderPeer = originalSender || messageSender;
|
|
||||||
|
|
||||||
const hiddenForwardTitle = message.forwardInfo?.hiddenUserName;
|
|
||||||
|
|
||||||
return {
|
|
||||||
message,
|
|
||||||
senderPeer,
|
|
||||||
hiddenForwardTitle,
|
|
||||||
onClick: () => focusMessage({ chatId, threadId, messageId: id }),
|
|
||||||
};
|
|
||||||
}).filter(Boolean);
|
|
||||||
}, [query, searchTag, viewportIds, messagesById, isSavedMessages, chatId, threadId]);
|
|
||||||
|
|
||||||
const handleKeyDown = useKeyboardListNavigation(containerRef, true, (index) => {
|
|
||||||
const foundResult = viewportResults?.[index === -1 ? 0 : index];
|
|
||||||
if (foundResult) {
|
|
||||||
foundResult.onClick();
|
|
||||||
}
|
|
||||||
}, '.ListItem-button', true);
|
|
||||||
|
|
||||||
const renderSearchResult = ({
|
|
||||||
message, senderPeer, hiddenForwardTitle, onClick,
|
|
||||||
}: {
|
|
||||||
message: ApiMessage;
|
|
||||||
senderPeer?: ApiPeer;
|
|
||||||
hiddenForwardTitle?: string;
|
|
||||||
onClick: NoneToVoidFunction;
|
|
||||||
}) => {
|
|
||||||
const text = renderMessageSummary(lang, message, undefined, query);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ListItem
|
|
||||||
key={message.id}
|
|
||||||
teactOrderKey={-message.date}
|
|
||||||
className="chat-item-clickable search-result-message m-0"
|
|
||||||
onClick={onClick}
|
|
||||||
>
|
|
||||||
<Avatar
|
|
||||||
peer={senderPeer}
|
|
||||||
text={hiddenForwardTitle}
|
|
||||||
/>
|
|
||||||
<div className="info">
|
|
||||||
<div className="search-result-message-top">
|
|
||||||
{senderPeer && <FullNameTitle peer={senderPeer} withEmojiStatus />}
|
|
||||||
{!senderPeer && hiddenForwardTitle}
|
|
||||||
<LastMessageMeta message={message} />
|
|
||||||
</div>
|
|
||||||
<div className="subtitle" dir="auto">
|
|
||||||
{text}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ListItem>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const isOnTop = viewportIds?.[0] === foundIds?.[0];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<InfiniteScroll
|
|
||||||
ref={containerRef}
|
|
||||||
className="RightSearch custom-scroll"
|
|
||||||
items={viewportResults}
|
|
||||||
preloadBackwards={0}
|
|
||||||
onLoadMore={getMore}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
>
|
|
||||||
{hasTags && (
|
|
||||||
<div
|
|
||||||
ref={tagsRef}
|
|
||||||
className="search-tags custom-scroll-x no-scrollbar"
|
|
||||||
key="search-tags"
|
|
||||||
>
|
|
||||||
{tags.map((tag) => (
|
|
||||||
<SavedTagButton
|
|
||||||
containerId="local-search"
|
|
||||||
key={getReactionKey(tag.reaction)}
|
|
||||||
reaction={tag.reaction}
|
|
||||||
tag={tag}
|
|
||||||
withCount
|
|
||||||
isDisabled={areTagsDisabled}
|
|
||||||
isChosen={isSameReaction(tag.reaction, searchTag)}
|
|
||||||
onClick={handleTagClick}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{isOnTop && (
|
|
||||||
<p key="helper-text" className="helper-text" dir="auto">
|
|
||||||
{!query ? (
|
|
||||||
lang('lng_dlg_search_for_messages')
|
|
||||||
) : (totalCount === 0 || !viewportResults.length) ? (
|
|
||||||
lang('lng_search_no_results')
|
|
||||||
) : totalCount === 1 ? (
|
|
||||||
'1 message found'
|
|
||||||
) : (
|
|
||||||
`${(viewportResults.length && (totalCount || viewportResults.length))} messages found`
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{viewportResults.map(renderSearchResult)}
|
|
||||||
</InfiniteScroll>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(withGlobal<OwnProps>(
|
|
||||||
(global, { chatId, threadId }): StateProps => {
|
|
||||||
const messagesById = selectChatMessages(global, chatId);
|
|
||||||
if (!messagesById) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
const { query, savedTag, results } = selectCurrentTextSearch(global) || {};
|
|
||||||
const { totalCount, foundIds } = results || {};
|
|
||||||
|
|
||||||
const isSavedMessages = selectIsChatWithSelf(global, chatId);
|
|
||||||
const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
|
|
||||||
|
|
||||||
const savedTags = isSavedMessages && !isSavedDialog ? global.savedReactionTags?.byKey : undefined;
|
|
||||||
|
|
||||||
return {
|
|
||||||
messagesById,
|
|
||||||
query,
|
|
||||||
totalCount,
|
|
||||||
foundIds,
|
|
||||||
isSavedMessages,
|
|
||||||
savedTags,
|
|
||||||
searchTag: savedTag,
|
|
||||||
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
|
||||||
};
|
|
||||||
},
|
|
||||||
)(RightSearch));
|
|
||||||
@ -324,7 +324,10 @@ function Story({
|
|||||||
onMouseLeave: handleLongPressMouseLeave,
|
onMouseLeave: handleLongPressMouseLeave,
|
||||||
onTouchStart: handleLongPressTouchStart,
|
onTouchStart: handleLongPressTouchStart,
|
||||||
onTouchEnd: handleLongPressTouchEnd,
|
onTouchEnd: handleLongPressTouchEnd,
|
||||||
} = useLongPress(handleLongPressStart, handleLongPressEnd);
|
} = useLongPress({
|
||||||
|
onStart: handleLongPressStart,
|
||||||
|
onEnd: handleLongPressEnd,
|
||||||
|
});
|
||||||
|
|
||||||
const isUnsupported = useUnsupportedMedia(
|
const isUnsupported = useUnsupportedMedia(
|
||||||
videoRef,
|
videoRef,
|
||||||
|
|||||||
@ -71,15 +71,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.round {
|
|
||||||
width: 3.5rem;
|
|
||||||
border-radius: 50%;
|
|
||||||
|
|
||||||
.icon {
|
|
||||||
font-size: 1.5rem;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.primary {
|
&.primary {
|
||||||
background-color: var(--color-primary);
|
background-color: var(--color-primary);
|
||||||
color: var(--color-white);
|
color: var(--color-white);
|
||||||
@ -357,6 +348,15 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.round {
|
||||||
|
width: 3.5rem;
|
||||||
|
border-radius: 50%;
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
&.fluid {
|
&.fluid {
|
||||||
padding-left: 1.75rem;
|
padding-left: 1.75rem;
|
||||||
padding-right: 1.75rem;
|
padding-right: 1.75rem;
|
||||||
|
|||||||
@ -100,7 +100,8 @@ const InfiniteScroll: FC<OwnProps> = ({
|
|||||||
|
|
||||||
// Initial preload
|
// Initial preload
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!loadMoreBackwards) {
|
const container = containerRef.current;
|
||||||
|
if (!loadMoreBackwards || !container) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -109,7 +110,7 @@ const InfiniteScroll: FC<OwnProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { scrollHeight, clientHeight } = containerRef.current!;
|
const { scrollHeight, clientHeight } = container;
|
||||||
if (clientHeight && scrollHeight < clientHeight) {
|
if (clientHeight && scrollHeight < clientHeight) {
|
||||||
loadMoreBackwards();
|
loadMoreBackwards();
|
||||||
}
|
}
|
||||||
|
|||||||
@ -378,60 +378,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
&.search-result-message {
|
|
||||||
.title {
|
|
||||||
flex-grow: 1;
|
|
||||||
padding-right: 0.125rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.search-result-message-top {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3 {
|
|
||||||
max-width: 80%;
|
|
||||||
}
|
|
||||||
|
|
||||||
h3,
|
|
||||||
.subtitle {
|
|
||||||
font-size: 1rem;
|
|
||||||
line-height: 1.5rem;
|
|
||||||
overflow: hidden;
|
|
||||||
white-space: nowrap;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
text-align: left;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
|
|
||||||
.matching-text-highlight {
|
|
||||||
color: var(--color-text);
|
|
||||||
background: #cae3f7;
|
|
||||||
border-radius: 0.25rem;
|
|
||||||
padding: 0 0.125rem;
|
|
||||||
display: inline-block;
|
|
||||||
|
|
||||||
.theme-dark & {
|
|
||||||
--color-text: #000;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&[dir="rtl"] {
|
|
||||||
.LastMessageMeta {
|
|
||||||
margin-left: 0;
|
|
||||||
margin-right: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.subtitle {
|
|
||||||
margin-right: 0;
|
|
||||||
display: block;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
&.picker-list-item {
|
&.picker-list-item {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|
||||||
|
|||||||
@ -6,11 +6,15 @@
|
|||||||
border: 2px solid var(--color-chat-hover);
|
border: 2px solid var(--color-chat-hover);
|
||||||
border-radius: 1.375rem;
|
border-radius: 1.375rem;
|
||||||
transition: border-color 0.15s ease;
|
transition: border-color 0.15s ease;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
padding-inline-end: 0.1875rem;
|
||||||
|
|
||||||
&.with-picker-item {
|
&.with-picker-item {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
.icon-search {
|
.icon-container-left {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,34 +48,46 @@
|
|||||||
background-color: transparent !important;
|
background-color: transparent !important;
|
||||||
box-shadow: none !important;
|
box-shadow: none !important;
|
||||||
padding:
|
padding:
|
||||||
calc(0.4375rem - var(--border-width)) calc(2.625rem - var(--border-width))
|
calc(0.4375rem - var(--border-width)) calc(0.625rem - var(--border-width))
|
||||||
calc(0.5rem - var(--border-width)) calc(2.75rem - var(--border-width));
|
calc(0.5rem - var(--border-width)) calc(0.75rem - var(--border-width));
|
||||||
|
|
||||||
&::placeholder {
|
&::placeholder {
|
||||||
color: var(--color-placeholders);
|
color: var(--color-placeholders);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-container {
|
.icon-container-left {
|
||||||
position: absolute;
|
width: 1.5rem;
|
||||||
top: 0;
|
flex-shrink: 0;
|
||||||
left: 0;
|
margin-inline-start: 0.75rem;
|
||||||
pointer-events: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-icon {
|
.icon-container-right {
|
||||||
position: absolute;
|
width: 2.5rem;
|
||||||
top: 50%;
|
flex-shrink: 0;
|
||||||
left: 0.75rem;
|
margin-inline-start: 0.5rem;
|
||||||
transform: translateY(-50%);
|
}
|
||||||
font-size: 1.375rem;
|
|
||||||
|
.icon-container-slide {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.search-icon, .back-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.back-icon {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
.Loading {
|
.Loading {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 50%;
|
||||||
left: 0.1875rem;
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
height: 2.5rem;
|
height: 2.5rem;
|
||||||
width: 2.5rem;
|
width: 2.5rem;
|
||||||
|
|
||||||
@ -81,9 +97,6 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.Button {
|
.Button {
|
||||||
position: absolute;
|
|
||||||
top: 0.125rem;
|
|
||||||
right: 0.125rem;
|
|
||||||
font-size: 1rem;
|
font-size: 1rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -91,7 +104,7 @@
|
|||||||
input {
|
input {
|
||||||
height: 2.5rem;
|
height: 2.5rem;
|
||||||
border-radius: 1.25rem;
|
border-radius: 1.25rem;
|
||||||
padding-left: calc(2.75rem - var(--border-width));
|
padding-left: calc(0.75rem - var(--border-width));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -99,20 +112,5 @@
|
|||||||
input {
|
input {
|
||||||
direction: rtl;
|
direction: rtl;
|
||||||
}
|
}
|
||||||
|
|
||||||
.search-icon {
|
|
||||||
left: auto;
|
|
||||||
right: 0.75rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Loading {
|
|
||||||
right: 0.1875rem;
|
|
||||||
left: auto;
|
|
||||||
}
|
|
||||||
|
|
||||||
.Button {
|
|
||||||
left: 0.125rem;
|
|
||||||
right: auto;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,18 @@
|
|||||||
import type { RefObject } from 'react';
|
import type { RefObject } from 'react';
|
||||||
import type { FC } from '../../lib/teact/teact';
|
import type { FC } from '../../lib/teact/teact';
|
||||||
import React, {
|
import React, {
|
||||||
memo, useCallback, useEffect, useRef,
|
memo, useEffect, useRef,
|
||||||
} from '../../lib/teact/teact';
|
} from '../../lib/teact/teact';
|
||||||
|
|
||||||
import buildClassName from '../../util/buildClassName';
|
import buildClassName from '../../util/buildClassName';
|
||||||
|
|
||||||
import useFlag from '../../hooks/useFlag';
|
import useFlag from '../../hooks/useFlag';
|
||||||
import useInputFocusOnOpen from '../../hooks/useInputFocusOnOpen';
|
import useInputFocusOnOpen from '../../hooks/useInputFocusOnOpen';
|
||||||
|
import useLang from '../../hooks/useLang';
|
||||||
|
import useLastCallback from '../../hooks/useLastCallback';
|
||||||
import useOldLang from '../../hooks/useOldLang';
|
import useOldLang from '../../hooks/useOldLang';
|
||||||
|
|
||||||
|
import Icon from '../common/icons/Icon';
|
||||||
import Button from './Button';
|
import Button from './Button';
|
||||||
import Loading from './Loading';
|
import Loading from './Loading';
|
||||||
import Transition from './Transition';
|
import Transition from './Transition';
|
||||||
@ -19,7 +22,7 @@ import './SearchInput.scss';
|
|||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
ref?: RefObject<HTMLInputElement>;
|
ref?: RefObject<HTMLInputElement>;
|
||||||
children?: React.ReactNode;
|
children?: React.ReactNode;
|
||||||
parentContainerClassName?: string;
|
resultsItemSelector?: string;
|
||||||
className?: string;
|
className?: string;
|
||||||
inputId?: string;
|
inputId?: string;
|
||||||
value?: string;
|
value?: string;
|
||||||
@ -32,17 +35,25 @@ type OwnProps = {
|
|||||||
autoComplete?: string;
|
autoComplete?: string;
|
||||||
canClose?: boolean;
|
canClose?: boolean;
|
||||||
autoFocusSearch?: boolean;
|
autoFocusSearch?: boolean;
|
||||||
|
hasUpButton?: boolean;
|
||||||
|
hasDownButton?: boolean;
|
||||||
|
teactExperimentControlled?: boolean;
|
||||||
|
withBackIcon?: boolean;
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
|
onStartBackspace?: NoneToVoidFunction;
|
||||||
onReset?: NoneToVoidFunction;
|
onReset?: NoneToVoidFunction;
|
||||||
onFocus?: NoneToVoidFunction;
|
onFocus?: NoneToVoidFunction;
|
||||||
onBlur?: NoneToVoidFunction;
|
onBlur?: NoneToVoidFunction;
|
||||||
|
onClick?: NoneToVoidFunction;
|
||||||
|
onUpClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||||
|
onDownClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
|
||||||
onSpinnerClick?: NoneToVoidFunction;
|
onSpinnerClick?: NoneToVoidFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
const SearchInput: FC<OwnProps> = ({
|
const SearchInput: FC<OwnProps> = ({
|
||||||
ref,
|
ref,
|
||||||
children,
|
children,
|
||||||
parentContainerClassName,
|
resultsItemSelector,
|
||||||
value,
|
value,
|
||||||
inputId,
|
inputId,
|
||||||
className,
|
className,
|
||||||
@ -55,10 +66,18 @@ const SearchInput: FC<OwnProps> = ({
|
|||||||
autoComplete,
|
autoComplete,
|
||||||
canClose,
|
canClose,
|
||||||
autoFocusSearch,
|
autoFocusSearch,
|
||||||
|
hasUpButton,
|
||||||
|
hasDownButton,
|
||||||
|
teactExperimentControlled,
|
||||||
|
withBackIcon,
|
||||||
onChange,
|
onChange,
|
||||||
|
onStartBackspace,
|
||||||
onReset,
|
onReset,
|
||||||
onFocus,
|
onFocus,
|
||||||
onBlur,
|
onBlur,
|
||||||
|
onClick,
|
||||||
|
onUpClick,
|
||||||
|
onDownClick,
|
||||||
onSpinnerClick,
|
onSpinnerClick,
|
||||||
}) => {
|
}) => {
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
@ -83,48 +102,70 @@ const SearchInput: FC<OwnProps> = ({
|
|||||||
}
|
}
|
||||||
}, [focused, placeholder]); // Trick for setting focus when selecting a contact to search for
|
}, [focused, placeholder]); // Trick for setting focus when selecting a contact to search for
|
||||||
|
|
||||||
const lang = useOldLang();
|
const oldLang = useOldLang();
|
||||||
|
const lang = useLang();
|
||||||
|
|
||||||
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
|
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
|
||||||
const { currentTarget } = event;
|
const { currentTarget } = event;
|
||||||
onChange(currentTarget.value);
|
onChange(currentTarget.value);
|
||||||
|
|
||||||
|
if (!isInputFocused) {
|
||||||
|
handleFocus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleFocus() {
|
function handleFocus() {
|
||||||
markInputFocused();
|
markInputFocused();
|
||||||
if (onFocus) {
|
onFocus?.();
|
||||||
onFocus();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleBlur() {
|
function handleBlur() {
|
||||||
unmarkInputFocused();
|
unmarkInputFocused();
|
||||||
if (onBlur) {
|
onBlur?.();
|
||||||
onBlur();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
const handleKeyDown = useLastCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (!resultsItemSelector) return;
|
||||||
if (e.key === 'ArrowDown' || e.key === 'Enter') {
|
if (e.key === 'ArrowDown' || e.key === 'Enter') {
|
||||||
const element = document.querySelector(`.${parentContainerClassName} .ListItem-button`) as HTMLElement;
|
const element = document.querySelector(resultsItemSelector) as HTMLElement;
|
||||||
if (element) {
|
if (element) {
|
||||||
element.focus();
|
element.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}, [parentContainerClassName]);
|
|
||||||
|
if (e.key === 'Backspace' && e.currentTarget.selectionStart === 0 && e.currentTarget.selectionEnd === 0) {
|
||||||
|
onStartBackspace?.();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={buildClassName('SearchInput', className, isInputFocused && 'has-focus')}
|
className={buildClassName('SearchInput', className, isInputFocused && 'has-focus')}
|
||||||
dir={lang.isRtl ? 'rtl' : undefined}
|
onClick={onClick}
|
||||||
|
dir={oldLang.isRtl ? 'rtl' : undefined}
|
||||||
>
|
>
|
||||||
{children}
|
<Transition
|
||||||
|
name="fade"
|
||||||
|
shouldCleanup
|
||||||
|
activeKey={Number(!isLoading && !withBackIcon)}
|
||||||
|
className="icon-container-left"
|
||||||
|
slideClassName="icon-container-slide"
|
||||||
|
>
|
||||||
|
{isLoading && !withBackIcon ? (
|
||||||
|
<Loading color={spinnerColor} backgroundColor={spinnerBackgroundColor} onClick={onSpinnerClick} />
|
||||||
|
) : withBackIcon ? (
|
||||||
|
<Icon name="arrow-left" className="back-icon" onClick={onReset} />
|
||||||
|
) : (
|
||||||
|
<Icon name="search" className="search-icon" />
|
||||||
|
)}
|
||||||
|
</Transition>
|
||||||
|
<div>{children}</div>
|
||||||
<input
|
<input
|
||||||
ref={inputRef}
|
ref={inputRef}
|
||||||
id={inputId}
|
id={inputId}
|
||||||
type="text"
|
type="text"
|
||||||
dir="auto"
|
dir="auto"
|
||||||
placeholder={placeholder || lang('Search')}
|
placeholder={placeholder || oldLang('Search')}
|
||||||
className="form-control"
|
className="form-control"
|
||||||
value={value}
|
value={value}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@ -133,29 +174,54 @@ const SearchInput: FC<OwnProps> = ({
|
|||||||
onFocus={handleFocus}
|
onFocus={handleFocus}
|
||||||
onBlur={handleBlur}
|
onBlur={handleBlur}
|
||||||
onKeyDown={handleKeyDown}
|
onKeyDown={handleKeyDown}
|
||||||
|
teactExperimentControlled={teactExperimentControlled}
|
||||||
/>
|
/>
|
||||||
<Transition
|
{hasUpButton && (
|
||||||
name="fade"
|
|
||||||
shouldCleanup
|
|
||||||
activeKey={Number(isLoading)}
|
|
||||||
className="icon-container"
|
|
||||||
>
|
|
||||||
{isLoading ? (
|
|
||||||
<Loading color={spinnerColor} backgroundColor={spinnerBackgroundColor} onClick={onSpinnerClick} />
|
|
||||||
) : (
|
|
||||||
<i className="icon icon-search search-icon" />
|
|
||||||
)}
|
|
||||||
</Transition>
|
|
||||||
{!isLoading && (value || canClose) && onReset && (
|
|
||||||
<Button
|
<Button
|
||||||
round
|
round
|
||||||
size="tiny"
|
size="tiny"
|
||||||
color="translucent"
|
color="translucent"
|
||||||
onClick={onReset}
|
onClick={onUpClick}
|
||||||
|
disabled={!onUpClick}
|
||||||
|
ariaLabel={lang('AriaSearchOlderResult')}
|
||||||
>
|
>
|
||||||
<span className="icon icon-close" />
|
<Icon name="up" />
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
{hasDownButton && (
|
||||||
|
<Button
|
||||||
|
round
|
||||||
|
size="tiny"
|
||||||
|
color="translucent"
|
||||||
|
onClick={onDownClick}
|
||||||
|
disabled={!onDownClick}
|
||||||
|
ariaLabel={lang('AriaSearchNewerResult')}
|
||||||
|
>
|
||||||
|
<Icon name="down" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Transition
|
||||||
|
name="fade"
|
||||||
|
shouldCleanup
|
||||||
|
activeKey={Number(isLoading)}
|
||||||
|
className="icon-container-right"
|
||||||
|
slideClassName="icon-container-slide"
|
||||||
|
>
|
||||||
|
{withBackIcon && isLoading ? (
|
||||||
|
<Loading color={spinnerColor} backgroundColor={spinnerBackgroundColor} onClick={onSpinnerClick} />
|
||||||
|
) : (
|
||||||
|
(value || canClose) && onReset && (
|
||||||
|
<Button
|
||||||
|
round
|
||||||
|
size="tiny"
|
||||||
|
color="translucent"
|
||||||
|
onClick={onReset}
|
||||||
|
>
|
||||||
|
<Icon name="close" />
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</Transition>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import './api/chats';
|
|||||||
import './api/messages';
|
import './api/messages';
|
||||||
import './api/symbols';
|
import './api/symbols';
|
||||||
import './api/globalSearch';
|
import './api/globalSearch';
|
||||||
import './api/localSearch';
|
import './api/middleSearch';
|
||||||
import './api/management';
|
import './api/management';
|
||||||
import './api/sync';
|
import './api/sync';
|
||||||
import './api/accounts';
|
import './api/accounts';
|
||||||
@ -19,7 +19,7 @@ import './ui/initial';
|
|||||||
import './ui/chats';
|
import './ui/chats';
|
||||||
import './ui/messages';
|
import './ui/messages';
|
||||||
import './ui/globalSearch';
|
import './ui/globalSearch';
|
||||||
import './ui/localSearch';
|
import './ui/middleSearch';
|
||||||
import './ui/stickerSearch';
|
import './ui/stickerSearch';
|
||||||
import './ui/users';
|
import './ui/users';
|
||||||
import './ui/settings';
|
import './ui/settings';
|
||||||
|
|||||||
@ -38,7 +38,7 @@ import { isDeepLink } from '../../../util/deepLinkParser';
|
|||||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||||
import { getOrderedIds } from '../../../util/folderManager';
|
import { getOrderedIds } from '../../../util/folderManager';
|
||||||
import { buildCollectionByKey, omit, pick } from '../../../util/iteratees';
|
import { buildCollectionByKey, omit, pick } from '../../../util/iteratees';
|
||||||
import { isLocalMessageId } from '../../../util/messageKey';
|
import { isLocalMessageId } from '../../../util/keys/messageKey';
|
||||||
import * as langProvider from '../../../util/oldLangProvider';
|
import * as langProvider from '../../../util/oldLangProvider';
|
||||||
import { debounce, pause, throttle } from '../../../util/schedulers';
|
import { debounce, pause, throttle } from '../../../util/schedulers';
|
||||||
import { extractCurrentThemeParams } from '../../../util/themeStyle';
|
import { extractCurrentThemeParams } from '../../../util/themeStyle';
|
||||||
@ -69,6 +69,7 @@ import {
|
|||||||
removeChatFromChatLists,
|
removeChatFromChatLists,
|
||||||
replaceChatFullInfo,
|
replaceChatFullInfo,
|
||||||
replaceChatListIds,
|
replaceChatListIds,
|
||||||
|
replaceChatListLoadingParameters,
|
||||||
replaceChats,
|
replaceChats,
|
||||||
replaceThreadParam,
|
replaceThreadParam,
|
||||||
replaceUsers,
|
replaceUsers,
|
||||||
@ -98,8 +99,8 @@ import {
|
|||||||
selectChatByUsername,
|
selectChatByUsername,
|
||||||
selectChatFolder,
|
selectChatFolder,
|
||||||
selectChatFullInfo,
|
selectChatFullInfo,
|
||||||
selectChatLastMessage,
|
|
||||||
selectChatLastMessageId,
|
selectChatLastMessageId,
|
||||||
|
selectChatListLoadingParameters,
|
||||||
selectChatListType,
|
selectChatListType,
|
||||||
selectChatMessages,
|
selectChatMessages,
|
||||||
selectCurrentChat,
|
selectCurrentChat,
|
||||||
@ -108,6 +109,7 @@ import {
|
|||||||
selectIsChatPinned,
|
selectIsChatPinned,
|
||||||
selectIsChatWithSelf,
|
selectIsChatWithSelf,
|
||||||
selectLastServiceNotification,
|
selectLastServiceNotification,
|
||||||
|
selectPeer,
|
||||||
selectSimilarChannelIds,
|
selectSimilarChannelIds,
|
||||||
selectStickerSet,
|
selectStickerSet,
|
||||||
selectSupportChat,
|
selectSupportChat,
|
||||||
@ -512,10 +514,6 @@ addActionHandler('loadAllChats', async (global, actions, payload): Promise<void>
|
|||||||
let { shouldReplace } = payload;
|
let { shouldReplace } = payload;
|
||||||
let i = 0;
|
let i = 0;
|
||||||
|
|
||||||
const getOrderDate = (chat: ApiChat) => {
|
|
||||||
return selectChatLastMessage(global, chat.id, listType === 'saved' ? 'saved' : 'all')?.date || chat.creationDate;
|
|
||||||
};
|
|
||||||
|
|
||||||
while (shouldReplace || !global.chats.isFullyLoaded[listType]) {
|
while (shouldReplace || !global.chats.isFullyLoaded[listType]) {
|
||||||
if (i++ >= INFINITE_LOOP_MARKER) {
|
if (i++ >= INFINITE_LOOP_MARKER) {
|
||||||
if (DEBUG) {
|
if (DEBUG) {
|
||||||
@ -532,24 +530,8 @@ addActionHandler('loadAllChats', async (global, actions, payload): Promise<void>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const listIds = !shouldReplace && global.chats.listIds[listType];
|
|
||||||
const oldestChat = listIds
|
|
||||||
? listIds
|
|
||||||
/* eslint-disable @typescript-eslint/no-loop-func */
|
|
||||||
.map((id) => global.chats.byId[id])
|
|
||||||
.filter((chat) => (
|
|
||||||
Boolean(chat && getOrderDate(chat))
|
|
||||||
&& chat.id !== SERVICE_NOTIFICATIONS_USER_ID
|
|
||||||
&& !selectIsChatPinned(global, chat.id)
|
|
||||||
))
|
|
||||||
/* eslint-enable @typescript-eslint/no-loop-func */
|
|
||||||
.sort((chat1, chat2) => getOrderDate(chat1)! - getOrderDate(chat2)!)[0]
|
|
||||||
: undefined;
|
|
||||||
|
|
||||||
await loadChats(
|
await loadChats(
|
||||||
listType,
|
listType,
|
||||||
oldestChat?.id,
|
|
||||||
oldestChat ? getOrderDate(oldestChat) : undefined,
|
|
||||||
shouldReplace,
|
shouldReplace,
|
||||||
true,
|
true,
|
||||||
);
|
);
|
||||||
@ -2709,21 +2691,30 @@ addActionHandler('requestCollectibleInfo', async (global, actions, payload): Pro
|
|||||||
|
|
||||||
async function loadChats(
|
async function loadChats(
|
||||||
listType: ChatListType,
|
listType: ChatListType,
|
||||||
offsetId?: string,
|
|
||||||
offsetDate?: number,
|
|
||||||
shouldReplace = false,
|
shouldReplace = false,
|
||||||
isFullDraftSync?: boolean,
|
isFullDraftSync?: boolean,
|
||||||
) {
|
) {
|
||||||
// eslint-disable-next-line eslint-multitab-tt/no-immediate-global
|
// eslint-disable-next-line eslint-multitab-tt/no-immediate-global
|
||||||
let global = getGlobal();
|
let global = getGlobal();
|
||||||
let lastLocalServiceMessageId = selectLastServiceNotification(global)?.id;
|
let lastLocalServiceMessageId = selectLastServiceNotification(global)?.id;
|
||||||
|
|
||||||
|
const params = selectChatListLoadingParameters(global, listType);
|
||||||
|
const offsetPeer = !shouldReplace && params.nextOffsetPeerId
|
||||||
|
? selectPeer(global, params.nextOffsetPeerId) : undefined;
|
||||||
|
const offsetDate = !shouldReplace ? params.nextOffsetDate : undefined;
|
||||||
|
const offsetId = !shouldReplace ? params.nextOffsetId : undefined;
|
||||||
|
|
||||||
const result = listType === 'saved' ? await callApi('fetchSavedChats', {
|
const result = listType === 'saved' ? await callApi('fetchSavedChats', {
|
||||||
limit: CHAT_LIST_LOAD_SLICE,
|
limit: CHAT_LIST_LOAD_SLICE,
|
||||||
offsetDate,
|
offsetDate,
|
||||||
|
offsetId,
|
||||||
|
offsetPeer,
|
||||||
withPinned: shouldReplace,
|
withPinned: shouldReplace,
|
||||||
}) : await callApi('fetchChats', {
|
}) : await callApi('fetchChats', {
|
||||||
limit: CHAT_LIST_LOAD_SLICE,
|
limit: CHAT_LIST_LOAD_SLICE,
|
||||||
offsetDate,
|
offsetDate,
|
||||||
|
offsetId,
|
||||||
|
offsetPeer,
|
||||||
archived: listType === 'archived',
|
archived: listType === 'archived',
|
||||||
withPinned: shouldReplace,
|
withPinned: shouldReplace,
|
||||||
lastLocalServiceMessageId,
|
lastLocalServiceMessageId,
|
||||||
@ -2735,10 +2726,6 @@ async function loadChats(
|
|||||||
|
|
||||||
const { chatIds } = result;
|
const { chatIds } = result;
|
||||||
|
|
||||||
if (chatIds.length > 0 && chatIds[0] === offsetId) {
|
|
||||||
chatIds.shift();
|
|
||||||
}
|
|
||||||
|
|
||||||
global = getGlobal();
|
global = getGlobal();
|
||||||
lastLocalServiceMessageId = selectLastServiceNotification(global)?.id;
|
lastLocalServiceMessageId = selectLastServiceNotification(global)?.id;
|
||||||
|
|
||||||
@ -2806,6 +2793,10 @@ async function loadChats(
|
|||||||
global = addMessages(global, result.messages);
|
global = addMessages(global, result.messages);
|
||||||
global = updateChatsLastMessageId(global, result.lastMessageByChatId, listType);
|
global = updateChatsLastMessageId(global, result.lastMessageByChatId, listType);
|
||||||
|
|
||||||
|
global = replaceChatListLoadingParameters(
|
||||||
|
global, listType, result.nextOffsetId, result.nextOffsetPeerId, result.nextOffsetDate,
|
||||||
|
);
|
||||||
|
|
||||||
const idsToUpdateDraft = isFullDraftSync ? result.chatIds : Object.keys(result.draftsById);
|
const idsToUpdateDraft = isFullDraftSync ? result.chatIds : Object.keys(result.draftsById);
|
||||||
idsToUpdateDraft.forEach((chatId) => {
|
idsToUpdateDraft.forEach((chatId) => {
|
||||||
const draft = result.draftsById[chatId];
|
const draft = result.draftsById[chatId];
|
||||||
|
|||||||
@ -72,7 +72,8 @@ addActionHandler('setGlobalSearchDate', (global, actions, payload): ActionReturn
|
|||||||
const maxDate = date ? timestampPlusDay(date) : date;
|
const maxDate = date ? timestampPlusDay(date) : date;
|
||||||
|
|
||||||
global = updateGlobalSearch(global, {
|
global = updateGlobalSearch(global, {
|
||||||
date,
|
minDate: date,
|
||||||
|
maxDate,
|
||||||
query: '',
|
query: '',
|
||||||
resultsByType: {
|
resultsByType: {
|
||||||
...selectTabState(global, tabId).globalSearch.resultsByType,
|
...selectTabState(global, tabId).globalSearch.resultsByType,
|
||||||
@ -85,29 +86,46 @@ addActionHandler('setGlobalSearchDate', (global, actions, payload): ActionReturn
|
|||||||
}, tabId);
|
}, tabId);
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
|
|
||||||
const { chatId } = selectTabState(global, tabId).globalSearch;
|
actions.searchMessagesGlobal({ type: 'text', tabId });
|
||||||
const chat = chatId ? selectChat(global, chatId) : undefined;
|
|
||||||
searchMessagesGlobal(global, '', 'text', undefined, chat, maxDate, date, tabId);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('searchMessagesGlobal', (global, actions, payload): ActionReturnType => {
|
addActionHandler('searchMessagesGlobal', (global, actions, payload): ActionReturnType => {
|
||||||
const { type, tabId = getCurrentTabId() } = payload;
|
const { type, tabId = getCurrentTabId() } = payload;
|
||||||
const {
|
const {
|
||||||
query, resultsByType, chatId, date,
|
query, resultsByType, chatId,
|
||||||
} = selectTabState(global, tabId).globalSearch;
|
} = selectTabState(global, tabId).globalSearch;
|
||||||
const maxDate = date ? timestampPlusDay(date) : date;
|
const offsetId = (resultsByType?.[type])?.nextOffsetId;
|
||||||
const nextOffsetId = (resultsByType?.[type as ApiGlobalMessageSearchType])?.nextOffsetId;
|
const offsetRate = (resultsByType?.[type])?.nextOffsetRate;
|
||||||
|
const offsetPeerId = (resultsByType?.[type])?.nextOffsetPeerId;
|
||||||
|
|
||||||
const chat = chatId ? selectChat(global, chatId) : undefined;
|
const chat = chatId ? selectChat(global, chatId) : undefined;
|
||||||
|
const offsetPeer = offsetPeerId ? selectChat(global, offsetPeerId) : undefined;
|
||||||
|
|
||||||
searchMessagesGlobal(global, query, type, nextOffsetId, chat, maxDate, date, tabId);
|
searchMessagesGlobal(global, {
|
||||||
|
query,
|
||||||
|
type,
|
||||||
|
offsetRate,
|
||||||
|
offsetId,
|
||||||
|
offsetPeer,
|
||||||
|
chat,
|
||||||
|
tabId,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
async function searchMessagesGlobal<T extends GlobalState>(
|
async function searchMessagesGlobal<T extends GlobalState>(global: T, params: {
|
||||||
global: T,
|
query?: string;
|
||||||
query = '', type: ApiGlobalMessageSearchType, offsetRate?: number, chat?: ApiChat, maxDate?: number, minDate?: number,
|
type: ApiGlobalMessageSearchType;
|
||||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
offsetRate?: number;
|
||||||
) {
|
offsetId?: number;
|
||||||
|
offsetPeer?: ApiChat;
|
||||||
|
chat?: ApiChat;
|
||||||
|
maxDate?: number;
|
||||||
|
minDate?: number;
|
||||||
|
tabId: TabArgs<T>[0];
|
||||||
|
}) {
|
||||||
|
const {
|
||||||
|
query = '', type, offsetRate, offsetId, offsetPeer, chat, maxDate, minDate, tabId = getCurrentTabId(),
|
||||||
|
} = params;
|
||||||
let result: {
|
let result: {
|
||||||
messages: ApiMessage[];
|
messages: ApiMessage[];
|
||||||
users: ApiUser[];
|
users: ApiUser[];
|
||||||
@ -115,18 +133,20 @@ async function searchMessagesGlobal<T extends GlobalState>(
|
|||||||
topics?: ApiTopic[];
|
topics?: ApiTopic[];
|
||||||
totalTopicsCount?: number;
|
totalTopicsCount?: number;
|
||||||
totalCount: number;
|
totalCount: number;
|
||||||
nextRate: number | undefined;
|
nextOffsetRate?: number;
|
||||||
|
nextOffsetId?: number;
|
||||||
|
nextOffsetPeerId?: string;
|
||||||
} | undefined;
|
} | undefined;
|
||||||
|
|
||||||
let messageLink: ApiMessage | undefined;
|
let messageLink: ApiMessage | undefined;
|
||||||
|
|
||||||
if (chat) {
|
if (chat) {
|
||||||
const localResultRequest = callApi('searchMessagesLocal', {
|
const inChatResultRequest = callApi('searchMessagesInChat', {
|
||||||
chat,
|
chat,
|
||||||
query,
|
query,
|
||||||
type,
|
type,
|
||||||
limit: GLOBAL_SEARCH_SLICE,
|
limit: GLOBAL_SEARCH_SLICE,
|
||||||
offsetId: offsetRate,
|
offsetId,
|
||||||
minDate,
|
minDate,
|
||||||
maxDate,
|
maxDate,
|
||||||
});
|
});
|
||||||
@ -136,12 +156,12 @@ async function searchMessagesGlobal<T extends GlobalState>(
|
|||||||
limit: GLOBAL_TOPIC_SEARCH_SLICE,
|
limit: GLOBAL_TOPIC_SEARCH_SLICE,
|
||||||
}) : undefined;
|
}) : undefined;
|
||||||
|
|
||||||
const [localResult, topics] = await Promise.all([localResultRequest, topicsRequest]);
|
const [inChatResult, topics] = await Promise.all([inChatResultRequest, topicsRequest]);
|
||||||
|
|
||||||
if (localResult) {
|
if (inChatResult) {
|
||||||
const {
|
const {
|
||||||
messages, users, totalCount, nextOffsetId,
|
messages, users, totalCount, nextOffsetId,
|
||||||
} = localResult;
|
} = inChatResult;
|
||||||
|
|
||||||
const { topics: localTopics, count } = topics || {};
|
const { topics: localTopics, count } = topics || {};
|
||||||
|
|
||||||
@ -152,13 +172,15 @@ async function searchMessagesGlobal<T extends GlobalState>(
|
|||||||
users,
|
users,
|
||||||
chats: [],
|
chats: [],
|
||||||
totalCount,
|
totalCount,
|
||||||
nextRate: nextOffsetId,
|
nextOffsetId,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
result = await callApi('searchMessagesGlobal', {
|
result = await callApi('searchMessagesGlobal', {
|
||||||
query,
|
query,
|
||||||
offsetRate,
|
offsetRate,
|
||||||
|
offsetId,
|
||||||
|
offsetPeer,
|
||||||
limit: GLOBAL_SEARCH_SLICE,
|
limit: GLOBAL_SEARCH_SLICE,
|
||||||
type,
|
type,
|
||||||
maxDate,
|
maxDate,
|
||||||
@ -187,7 +209,7 @@ async function searchMessagesGlobal<T extends GlobalState>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
messages, users, chats, totalCount, nextRate,
|
messages, users, chats, totalCount, nextOffsetRate, nextOffsetId, nextOffsetPeerId,
|
||||||
} = result;
|
} = result;
|
||||||
|
|
||||||
if (chats.length) {
|
if (chats.length) {
|
||||||
@ -207,7 +229,9 @@ async function searchMessagesGlobal<T extends GlobalState>(
|
|||||||
messages,
|
messages,
|
||||||
totalCount,
|
totalCount,
|
||||||
type,
|
type,
|
||||||
nextRate,
|
nextOffsetRate,
|
||||||
|
nextOffsetId,
|
||||||
|
nextOffsetPeerId,
|
||||||
tabId,
|
tabId,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -16,7 +16,7 @@ import type {
|
|||||||
ApiUser,
|
ApiUser,
|
||||||
ApiVideo,
|
ApiVideo,
|
||||||
} from '../../../api/types';
|
} from '../../../api/types';
|
||||||
import type { MessageKey } from '../../../util/messageKey';
|
import type { MessageKey } from '../../../util/keys/messageKey';
|
||||||
import type { RequiredGlobalActions } from '../../index';
|
import type { RequiredGlobalActions } from '../../index';
|
||||||
import type {
|
import type {
|
||||||
ActionReturnType, ApiDraft, GlobalState, TabArgs,
|
ActionReturnType, ApiDraft, GlobalState, TabArgs,
|
||||||
@ -46,7 +46,7 @@ import {
|
|||||||
split,
|
split,
|
||||||
unique,
|
unique,
|
||||||
} from '../../../util/iteratees';
|
} from '../../../util/iteratees';
|
||||||
import { getMessageKey, isLocalMessageId } from '../../../util/messageKey';
|
import { getMessageKey, isLocalMessageId } from '../../../util/keys/messageKey';
|
||||||
import { oldTranslate } from '../../../util/oldLangProvider';
|
import { oldTranslate } from '../../../util/oldLangProvider';
|
||||||
import { debounce, onTickEnd, rafPromise } from '../../../util/schedulers';
|
import { debounce, onTickEnd, rafPromise } from '../../../util/schedulers';
|
||||||
import { IS_IOS } from '../../../util/windowEnvironment';
|
import { IS_IOS } from '../../../util/windowEnvironment';
|
||||||
|
|||||||
@ -1,8 +1,8 @@
|
|||||||
import type { ApiChat } from '../../../api/types';
|
|
||||||
import type {
|
import type {
|
||||||
ChatMediaSearchParams, ChatMediaSearchSegment, LoadingState, SharedMediaType, ThreadId,
|
ChatMediaSearchParams, ChatMediaSearchSegment, LoadingState, SharedMediaType, ThreadId,
|
||||||
} from '../../../types';
|
} from '../../../types';
|
||||||
import type { ActionReturnType, GlobalState, TabArgs } from '../../types';
|
import type { ActionReturnType, GlobalState, TabArgs } from '../../types';
|
||||||
|
import { type ApiChat, MAIN_THREAD_ID } from '../../../api/types';
|
||||||
import { LoadMoreDirection } from '../../../types';
|
import { LoadMoreDirection } from '../../../types';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@ -10,6 +10,7 @@ import {
|
|||||||
} from '../../../config';
|
} from '../../../config';
|
||||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||||
import { buildCollectionByKey, isInsideSortedArrayRange } from '../../../util/iteratees';
|
import { buildCollectionByKey, isInsideSortedArrayRange } from '../../../util/iteratees';
|
||||||
|
import { getSearchResultKey } from '../../../util/keys/searchResultKey';
|
||||||
import { callApi } from '../../../api/gramjs';
|
import { callApi } from '../../../api/gramjs';
|
||||||
import { getChatMediaMessageIds, getIsSavedDialog, isSameReaction } from '../../helpers';
|
import { getChatMediaMessageIds, getIsSavedDialog, isSameReaction } from '../../helpers';
|
||||||
import {
|
import {
|
||||||
@ -18,27 +19,30 @@ import {
|
|||||||
import {
|
import {
|
||||||
addChatMessagesById,
|
addChatMessagesById,
|
||||||
addChats,
|
addChats,
|
||||||
|
addMessages,
|
||||||
addUsers,
|
addUsers,
|
||||||
initializeChatMediaSearchResults,
|
initializeChatMediaSearchResults,
|
||||||
mergeWithChatMediaSearchSegment,
|
mergeWithChatMediaSearchSegment,
|
||||||
setChatMediaSearchLoading,
|
setChatMediaSearchLoading,
|
||||||
updateChatMediaSearchResults,
|
updateChatMediaSearchResults,
|
||||||
updateLocalTextSearchResults,
|
updateMiddleSearch,
|
||||||
|
updateMiddleSearchResults,
|
||||||
updateSharedMediaSearchResults,
|
updateSharedMediaSearchResults,
|
||||||
} from '../../reducers';
|
} from '../../reducers';
|
||||||
import {
|
import {
|
||||||
selectChat,
|
selectChat,
|
||||||
selectCurrentChatMediaSearch,
|
selectCurrentChatMediaSearch,
|
||||||
selectCurrentMessageList,
|
selectCurrentMessageList,
|
||||||
|
selectCurrentMiddleSearch,
|
||||||
selectCurrentSharedMediaSearch,
|
selectCurrentSharedMediaSearch,
|
||||||
selectCurrentTextSearch,
|
|
||||||
} from '../../selectors';
|
} from '../../selectors';
|
||||||
|
|
||||||
const MEDIA_PRELOAD_OFFSET = 9;
|
const MEDIA_PRELOAD_OFFSET = 9;
|
||||||
|
|
||||||
addActionHandler('searchTextMessagesLocal', async (global, actions, payload): Promise<void> => {
|
addActionHandler('performMiddleSearch', async (global, actions, payload): Promise<void> => {
|
||||||
const { tabId = getCurrentTabId() } = payload || {};
|
const {
|
||||||
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
|
query, chatId, threadId = MAIN_THREAD_ID, tabId = getCurrentTabId(),
|
||||||
|
} = payload || {};
|
||||||
|
|
||||||
if (!chatId) return;
|
if (!chatId) return;
|
||||||
|
|
||||||
@ -47,45 +51,91 @@ addActionHandler('searchTextMessagesLocal', async (global, actions, payload): Pr
|
|||||||
const realChatId = isSavedDialog ? String(threadId) : chatId;
|
const realChatId = isSavedDialog ? String(threadId) : chatId;
|
||||||
|
|
||||||
const chat = realChatId ? selectChat(global, realChatId) : undefined;
|
const chat = realChatId ? selectChat(global, realChatId) : undefined;
|
||||||
let currentSearch = selectCurrentTextSearch(global, tabId);
|
let currentSearch = selectCurrentMiddleSearch(global, tabId);
|
||||||
if (!chat || !threadId || !currentSearch) {
|
if (!chat) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { query, results, savedTag } = currentSearch;
|
if (!currentSearch) {
|
||||||
|
global = updateMiddleSearch(global, realChatId, threadId, {}, tabId);
|
||||||
|
setGlobal(global);
|
||||||
|
global = getGlobal();
|
||||||
|
}
|
||||||
|
currentSearch = selectCurrentMiddleSearch(global, tabId)!;
|
||||||
|
|
||||||
|
const {
|
||||||
|
results, savedTag, type, isHashtag,
|
||||||
|
} = currentSearch;
|
||||||
const offsetId = results?.nextOffsetId;
|
const offsetId = results?.nextOffsetId;
|
||||||
|
const offsetRate = results?.nextOffsetRate;
|
||||||
|
const offsetPeerId = results?.nextOffsetPeerId;
|
||||||
|
const offsetPeer = offsetPeerId ? selectChat(global, offsetPeerId) : undefined;
|
||||||
|
|
||||||
if (!query && !savedTag) {
|
const shouldHaveQuery = isHashtag || !savedTag;
|
||||||
|
if (shouldHaveQuery && !query) {
|
||||||
|
global = updateMiddleSearch(global, realChatId, threadId, {
|
||||||
|
fetchingQuery: undefined,
|
||||||
|
}, tabId);
|
||||||
|
setGlobal(global);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await callApi('searchMessagesLocal', {
|
global = updateMiddleSearch(global, realChatId, threadId, {
|
||||||
chat,
|
fetchingQuery: query,
|
||||||
type: 'text',
|
}, tabId);
|
||||||
query,
|
setGlobal(global);
|
||||||
threadId,
|
|
||||||
limit: MESSAGE_SEARCH_SLICE,
|
let result;
|
||||||
offsetId,
|
if (type === 'chat') {
|
||||||
isSavedDialog,
|
result = await callApi('searchMessagesInChat', {
|
||||||
savedTag,
|
chat,
|
||||||
});
|
type: 'text',
|
||||||
|
query: isHashtag ? `#${query}` : query,
|
||||||
|
threadId,
|
||||||
|
limit: MESSAGE_SEARCH_SLICE,
|
||||||
|
offsetId,
|
||||||
|
isSavedDialog,
|
||||||
|
savedTag,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'myChats') {
|
||||||
|
result = await callApi('searchMessagesGlobal', {
|
||||||
|
type: 'text',
|
||||||
|
query: isHashtag ? `#${query}` : query!,
|
||||||
|
limit: MESSAGE_SEARCH_SLICE,
|
||||||
|
offsetId,
|
||||||
|
offsetRate,
|
||||||
|
offsetPeer,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === 'channels') {
|
||||||
|
result = await callApi('searchHashtagPosts', {
|
||||||
|
hashtag: query!,
|
||||||
|
limit: MESSAGE_SEARCH_SLICE,
|
||||||
|
offsetId,
|
||||||
|
offsetPeer,
|
||||||
|
offsetRate,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
chats, users, messages, totalCount, nextOffsetId,
|
chats, users, messages, totalCount, nextOffsetId, nextOffsetRate, nextOffsetPeerId,
|
||||||
} = result;
|
} = result;
|
||||||
|
|
||||||
const byId = buildCollectionByKey(messages, 'id');
|
const newFoundIds = messages.map(getSearchResultKey);
|
||||||
const newFoundIds = Object.keys(byId).map(Number);
|
|
||||||
|
|
||||||
global = getGlobal();
|
global = getGlobal();
|
||||||
|
|
||||||
currentSearch = selectCurrentTextSearch(global, tabId);
|
currentSearch = selectCurrentMiddleSearch(global, tabId);
|
||||||
const hasTagChanged = !isSameReaction(savedTag, currentSearch?.savedTag);
|
const hasTagChanged = currentSearch?.savedTag && !isSameReaction(savedTag, currentSearch.savedTag);
|
||||||
if (!currentSearch || query !== currentSearch.query || hasTagChanged) {
|
const hasSearchChanged = currentSearch?.fetchingQuery && currentSearch.fetchingQuery !== query;
|
||||||
|
if (!currentSearch || hasSearchChanged || hasTagChanged) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -93,11 +143,42 @@ addActionHandler('searchTextMessagesLocal', async (global, actions, payload): Pr
|
|||||||
|
|
||||||
global = addChats(global, buildCollectionByKey(chats, 'id'));
|
global = addChats(global, buildCollectionByKey(chats, 'id'));
|
||||||
global = addUsers(global, buildCollectionByKey(users, 'id'));
|
global = addUsers(global, buildCollectionByKey(users, 'id'));
|
||||||
global = addChatMessagesById(global, resultChatId, byId);
|
global = addMessages(global, messages);
|
||||||
global = updateLocalTextSearchResults(global, resultChatId, threadId, newFoundIds, totalCount, nextOffsetId, tabId);
|
global = updateMiddleSearch(global, resultChatId, threadId, {
|
||||||
|
fetchingQuery: undefined,
|
||||||
|
}, tabId);
|
||||||
|
global = updateMiddleSearchResults(global, resultChatId, threadId, {
|
||||||
|
foundIds: newFoundIds,
|
||||||
|
totalCount,
|
||||||
|
nextOffsetId,
|
||||||
|
nextOffsetRate,
|
||||||
|
nextOffsetPeerId,
|
||||||
|
query: query || '',
|
||||||
|
}, tabId);
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('searchHashtag', (global, actions, payload): ActionReturnType => {
|
||||||
|
const { hashtag, tabId = getCurrentTabId() } = payload;
|
||||||
|
|
||||||
|
const messageList = selectCurrentMessageList(global, tabId);
|
||||||
|
if (!messageList) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const cleanQuery = hashtag.replace(/^#/, '');
|
||||||
|
|
||||||
|
actions.updateMiddleSearch({
|
||||||
|
chatId: messageList.chatId,
|
||||||
|
threadId: messageList.threadId,
|
||||||
|
update: {
|
||||||
|
isHashtag: true,
|
||||||
|
requestedQuery: cleanQuery,
|
||||||
|
},
|
||||||
|
tabId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
addActionHandler('searchSharedMediaMessages', (global, actions, payload): ActionReturnType => {
|
addActionHandler('searchSharedMediaMessages', (global, actions, payload): ActionReturnType => {
|
||||||
const { tabId = getCurrentTabId() } = payload || {};
|
const { tabId = getCurrentTabId() } = payload || {};
|
||||||
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
|
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
|
||||||
@ -203,7 +284,7 @@ async function searchSharedMedia<T extends GlobalState>(
|
|||||||
) {
|
) {
|
||||||
const resultChatId = isSavedDialog ? global.currentUserId! : chat.id;
|
const resultChatId = isSavedDialog ? global.currentUserId! : chat.id;
|
||||||
|
|
||||||
const result = await callApi('searchMessagesLocal', {
|
const result = await callApi('searchMessagesInChat', {
|
||||||
chat,
|
chat,
|
||||||
type,
|
type,
|
||||||
limit: SHARED_MEDIA_SLICE * 2,
|
limit: SHARED_MEDIA_SLICE * 2,
|
||||||
@ -367,7 +448,7 @@ async function searchChatMedia<T extends GlobalState>(
|
|||||||
global = setChatMediaSearchLoading(global, resultChatId, threadId, true, tabId);
|
global = setChatMediaSearchLoading(global, resultChatId, threadId, true, tabId);
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
|
|
||||||
const result = await callApi('searchMessagesLocal', {
|
const result = await callApi('searchMessagesInChat', {
|
||||||
chat,
|
chat,
|
||||||
type: 'media',
|
type: 'media',
|
||||||
limit,
|
limit,
|
||||||
@ -7,8 +7,8 @@ import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
|||||||
import {
|
import {
|
||||||
buildCollectionByCallback, buildCollectionByKey, omit, unique,
|
buildCollectionByCallback, buildCollectionByKey, omit, unique,
|
||||||
} from '../../../util/iteratees';
|
} from '../../../util/iteratees';
|
||||||
|
import { getMessageKey } from '../../../util/keys/messageKey';
|
||||||
import * as mediaLoader from '../../../util/mediaLoader';
|
import * as mediaLoader from '../../../util/mediaLoader';
|
||||||
import { getMessageKey } from '../../../util/messageKey';
|
|
||||||
import requestActionTimeout from '../../../util/requestActionTimeout';
|
import requestActionTimeout from '../../../util/requestActionTimeout';
|
||||||
import { callApi } from '../../../api/gramjs';
|
import { callApi } from '../../../api/gramjs';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ import { MAIN_THREAD_ID } from '../../../api/types';
|
|||||||
|
|
||||||
import { ARCHIVED_FOLDER_ID, MAX_ACTIVE_PINNED_CHATS } from '../../../config';
|
import { ARCHIVED_FOLDER_ID, MAX_ACTIVE_PINNED_CHATS } from '../../../config';
|
||||||
import { buildCollectionByKey, omit } from '../../../util/iteratees';
|
import { buildCollectionByKey, omit } from '../../../util/iteratees';
|
||||||
import { isLocalMessageId } from '../../../util/messageKey';
|
import { isLocalMessageId } from '../../../util/keys/messageKey';
|
||||||
import { closeMessageNotifications, notifyAboutMessage } from '../../../util/notifications';
|
import { closeMessageNotifications, notifyAboutMessage } from '../../../util/notifications';
|
||||||
import { buildLocalMessage } from '../../../api/gramjs/apiBuilders/messages';
|
import { buildLocalMessage } from '../../../api/gramjs/apiBuilders/messages';
|
||||||
import { checkIfHasUnreadReactions, isChatChannel } from '../../helpers';
|
import { checkIfHasUnreadReactions, isChatChannel } from '../../helpers';
|
||||||
|
|||||||
@ -15,7 +15,7 @@ import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
|||||||
import {
|
import {
|
||||||
buildCollectionByKey, omit, pickTruthy, unique,
|
buildCollectionByKey, omit, pickTruthy, unique,
|
||||||
} from '../../../util/iteratees';
|
} from '../../../util/iteratees';
|
||||||
import { getMessageKey, isLocalMessageId } from '../../../util/messageKey';
|
import { getMessageKey, isLocalMessageId } from '../../../util/keys/messageKey';
|
||||||
import { notifyAboutMessage } from '../../../util/notifications';
|
import { notifyAboutMessage } from '../../../util/notifications';
|
||||||
import { onTickEnd } from '../../../util/schedulers';
|
import { onTickEnd } from '../../../util/schedulers';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@ -6,13 +6,13 @@ import { createMessageHashUrl } from '../../../util/routing';
|
|||||||
import { IS_ELECTRON } from '../../../util/windowEnvironment';
|
import { IS_ELECTRON } from '../../../util/windowEnvironment';
|
||||||
import { addActionHandler, setGlobal } from '../../index';
|
import { addActionHandler, setGlobal } from '../../index';
|
||||||
import {
|
import {
|
||||||
|
closeMiddleSearch,
|
||||||
exitMessageSelectMode, replaceTabThreadParam, updateCurrentMessageList, updateRequestedChatTranslation,
|
exitMessageSelectMode, replaceTabThreadParam, updateCurrentMessageList, updateRequestedChatTranslation,
|
||||||
} from '../../reducers';
|
} from '../../reducers';
|
||||||
import { updateTabState } from '../../reducers/tabs';
|
import { updateTabState } from '../../reducers/tabs';
|
||||||
import {
|
import {
|
||||||
selectChat, selectCurrentMessageList, selectTabState,
|
selectChat, selectCurrentMessageList, selectTabState,
|
||||||
} from '../../selectors';
|
} from '../../selectors';
|
||||||
import { closeLocalTextSearch } from './localSearch';
|
|
||||||
|
|
||||||
addActionHandler('processOpenChatOrThread', (global, actions, payload): ActionReturnType => {
|
addActionHandler('processOpenChatOrThread', (global, actions, payload): ActionReturnType => {
|
||||||
const {
|
const {
|
||||||
@ -50,10 +50,11 @@ addActionHandler('processOpenChatOrThread', (global, actions, payload): ActionRe
|
|||||||
activeReactions: {},
|
activeReactions: {},
|
||||||
shouldPreventComposerAnimation: true,
|
shouldPreventComposerAnimation: true,
|
||||||
}, tabId);
|
}, tabId);
|
||||||
|
|
||||||
|
global = closeMiddleSearch(global, chatId, threadId, tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
global = exitMessageSelectMode(global, tabId);
|
global = exitMessageSelectMode(global, tabId);
|
||||||
global = closeLocalTextSearch(global, tabId);
|
|
||||||
|
|
||||||
global = updateTabState(global, {
|
global = updateTabState(global, {
|
||||||
isStatisticsShown: false,
|
isStatisticsShown: false,
|
||||||
|
|||||||
@ -1,93 +0,0 @@
|
|||||||
import type { ActionReturnType, GlobalState, TabArgs } from '../../types';
|
|
||||||
|
|
||||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
|
||||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
|
||||||
import { buildChatThreadKey, isSameReaction } from '../../helpers';
|
|
||||||
import { addActionHandler } from '../../index';
|
|
||||||
import {
|
|
||||||
replaceLocalTextSearchResults,
|
|
||||||
updateLocalTextSearch,
|
|
||||||
updateLocalTextSearchTag,
|
|
||||||
updateSharedMediaSearchType,
|
|
||||||
} from '../../reducers';
|
|
||||||
import { selectCurrentMessageList, selectTabState } from '../../selectors';
|
|
||||||
|
|
||||||
addActionHandler('openLocalTextSearch', (global, actions, payload): ActionReturnType => {
|
|
||||||
const { tabId = getCurrentTabId() } = payload || {};
|
|
||||||
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
|
|
||||||
if (!chatId || !threadId) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return updateLocalTextSearch(global, chatId, threadId, '', tabId);
|
|
||||||
});
|
|
||||||
|
|
||||||
addActionHandler('closeLocalTextSearch', (global, actions, payload): ActionReturnType => {
|
|
||||||
const { tabId = getCurrentTabId() } = payload || {};
|
|
||||||
return closeLocalTextSearch(global, tabId);
|
|
||||||
});
|
|
||||||
|
|
||||||
addActionHandler('setLocalTextSearchQuery', (global, actions, payload): ActionReturnType => {
|
|
||||||
const { query, tabId = getCurrentTabId() } = payload!;
|
|
||||||
|
|
||||||
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
|
|
||||||
if (!chatId || !threadId) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chatThreadKey = buildChatThreadKey(chatId, threadId);
|
|
||||||
const { query: currentQuery } = selectTabState(global, tabId).localTextSearch.byChatThreadKey[chatThreadKey] || {};
|
|
||||||
|
|
||||||
if (query !== currentQuery) {
|
|
||||||
global = replaceLocalTextSearchResults(global, chatId, threadId, MEMO_EMPTY_ARRAY, undefined, undefined, tabId);
|
|
||||||
}
|
|
||||||
|
|
||||||
global = updateLocalTextSearch(global, chatId, threadId, query, tabId);
|
|
||||||
|
|
||||||
return global;
|
|
||||||
});
|
|
||||||
|
|
||||||
addActionHandler('setLocalTextSearchTag', (global, actions, payload): ActionReturnType => {
|
|
||||||
const { tag, tabId = getCurrentTabId() } = payload!;
|
|
||||||
|
|
||||||
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
|
|
||||||
if (!chatId || !threadId) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
const chatThreadKey = buildChatThreadKey(chatId, threadId);
|
|
||||||
const { savedTag } = selectTabState(global, tabId).localTextSearch.byChatThreadKey[chatThreadKey] || {};
|
|
||||||
|
|
||||||
if (!isSameReaction(tag, savedTag)) {
|
|
||||||
global = replaceLocalTextSearchResults(global, chatId, threadId, MEMO_EMPTY_ARRAY, undefined, undefined, tabId);
|
|
||||||
}
|
|
||||||
|
|
||||||
global = updateLocalTextSearchTag(global, chatId, threadId, tag, tabId);
|
|
||||||
|
|
||||||
return global;
|
|
||||||
});
|
|
||||||
|
|
||||||
addActionHandler('setSharedMediaSearchType', (global, actions, payload): ActionReturnType => {
|
|
||||||
const { mediaType, tabId = getCurrentTabId() } = payload;
|
|
||||||
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
|
|
||||||
if (!chatId || !threadId) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return updateSharedMediaSearchType(global, chatId, threadId, mediaType, tabId);
|
|
||||||
});
|
|
||||||
|
|
||||||
export function closeLocalTextSearch<T extends GlobalState>(
|
|
||||||
global: T,
|
|
||||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
|
||||||
): T {
|
|
||||||
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
|
|
||||||
if (!chatId || !threadId) {
|
|
||||||
return global;
|
|
||||||
}
|
|
||||||
|
|
||||||
global = updateLocalTextSearchTag(global, chatId, threadId, undefined, tabId);
|
|
||||||
global = updateLocalTextSearch(global, chatId, threadId, undefined, tabId);
|
|
||||||
global = replaceLocalTextSearchResults(global, chatId, threadId, undefined, undefined, undefined, tabId);
|
|
||||||
return global;
|
|
||||||
}
|
|
||||||
76
src/global/actions/ui/middleSearch.ts
Normal file
76
src/global/actions/ui/middleSearch.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import type { ActionReturnType } from '../../types';
|
||||||
|
import { MAIN_THREAD_ID } from '../../../api/types';
|
||||||
|
|
||||||
|
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||||
|
import { addActionHandler } from '../../index';
|
||||||
|
import {
|
||||||
|
closeMiddleSearch,
|
||||||
|
resetMiddleSearch,
|
||||||
|
updateMiddleSearch,
|
||||||
|
updateSharedMediaSearchType,
|
||||||
|
} from '../../reducers';
|
||||||
|
import { selectCurrentMessageList } from '../../selectors';
|
||||||
|
|
||||||
|
addActionHandler('openMiddleSearch', (global, actions, payload): ActionReturnType => {
|
||||||
|
const { tabId = getCurrentTabId() } = payload || {};
|
||||||
|
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
|
||||||
|
if (!chatId || !threadId) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return updateMiddleSearch(global, chatId, threadId, {}, tabId);
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('closeMiddleSearch', (global, actions, payload): ActionReturnType => {
|
||||||
|
const { tabId = getCurrentTabId() } = payload || {};
|
||||||
|
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
|
||||||
|
if (!chatId || !threadId) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return closeMiddleSearch(global, chatId, threadId, tabId);
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('updateMiddleSearch', (global, actions, payload): ActionReturnType => {
|
||||||
|
const {
|
||||||
|
update, tabId = getCurrentTabId(),
|
||||||
|
} = payload;
|
||||||
|
|
||||||
|
let chatId;
|
||||||
|
let threadId;
|
||||||
|
if (payload.chatId) {
|
||||||
|
chatId = payload.chatId;
|
||||||
|
threadId = payload.threadId || MAIN_THREAD_ID;
|
||||||
|
} else {
|
||||||
|
const currentMessageList = selectCurrentMessageList(global, tabId);
|
||||||
|
if (!currentMessageList) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
chatId = currentMessageList.chatId;
|
||||||
|
threadId = currentMessageList.threadId;
|
||||||
|
}
|
||||||
|
|
||||||
|
global = updateMiddleSearch(global, chatId, threadId, update, tabId);
|
||||||
|
|
||||||
|
return global;
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('resetMiddleSearch', (global, actions, payload): ActionReturnType => {
|
||||||
|
const { tabId = getCurrentTabId() } = payload || {};
|
||||||
|
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
|
||||||
|
if (!chatId || !threadId) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return resetMiddleSearch(global, chatId, threadId, tabId);
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('setSharedMediaSearchType', (global, actions, payload): ActionReturnType => {
|
||||||
|
const { mediaType, tabId = getCurrentTabId() } = payload;
|
||||||
|
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
|
||||||
|
if (!chatId || !threadId) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return updateSharedMediaSearchType(global, chatId, threadId, mediaType, tabId);
|
||||||
|
});
|
||||||
@ -252,6 +252,10 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
|
|||||||
if (!cached.quickReplies) {
|
if (!cached.quickReplies) {
|
||||||
cached.quickReplies = initialState.quickReplies;
|
cached.quickReplies = initialState.quickReplies;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!cached.chats.loadingParameters) {
|
||||||
|
cached.chats.loadingParameters = initialState.chats.loadingParameters;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateCache(force?: boolean) {
|
function updateCache(force?: boolean) {
|
||||||
|
|||||||
@ -27,6 +27,14 @@ export function isUserId(entityId: string) {
|
|||||||
return !entityId.startsWith('-');
|
return !entityId.startsWith('-');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isPeerChat(entity: ApiPeer): entity is ApiChat {
|
||||||
|
return 'title' in entity;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPeerUser(entity: ApiPeer): entity is ApiUser {
|
||||||
|
return !isPeerChat(entity);
|
||||||
|
}
|
||||||
|
|
||||||
export function isChannelId(entityId: string) {
|
export function isChannelId(entityId: string) {
|
||||||
return entityId.length === CHANNEL_ID_LENGTH && entityId.startsWith('-1');
|
return entityId.length === CHANNEL_ID_LENGTH && entityId.startsWith('-1');
|
||||||
}
|
}
|
||||||
@ -365,14 +373,12 @@ export function getMessageSenderName(lang: LangFn, chatId: string, sender?: ApiP
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isUserId(sender.id)) {
|
if (isPeerChat(sender)) {
|
||||||
if (chatId === sender.id) return undefined;
|
if (chatId === sender.id) return undefined;
|
||||||
|
|
||||||
return (sender as ApiChat).title;
|
return sender.title;
|
||||||
}
|
}
|
||||||
|
|
||||||
sender = sender as ApiUser;
|
|
||||||
|
|
||||||
if (sender.isSelf) {
|
if (sender.isSelf) {
|
||||||
return lang('FromYou');
|
return lang('FromYou');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,7 @@ export * from './users';
|
|||||||
export * from './chats';
|
export * from './chats';
|
||||||
export * from './messages';
|
export * from './messages';
|
||||||
export * from './messageMedia';
|
export * from './messageMedia';
|
||||||
export * from './localSearch';
|
export * from './middleSearch';
|
||||||
export * from './reactions';
|
export * from './reactions';
|
||||||
export * from './bots';
|
export * from './bots';
|
||||||
export * from './media';
|
export * from './media';
|
||||||
|
|||||||
@ -1,11 +1,9 @@
|
|||||||
import type {
|
import type {
|
||||||
ApiAttachment,
|
ApiAttachment,
|
||||||
ApiChat,
|
|
||||||
ApiMessage,
|
ApiMessage,
|
||||||
ApiMessageEntityTextUrl,
|
ApiMessageEntityTextUrl,
|
||||||
ApiPeer,
|
ApiPeer,
|
||||||
ApiStory,
|
ApiStory,
|
||||||
ApiUser,
|
|
||||||
} from '../../api/types';
|
} from '../../api/types';
|
||||||
import type { MediaContent } from '../../api/types/messages';
|
import type { MediaContent } from '../../api/types/messages';
|
||||||
import type { LangFn } from '../../hooks/useOldLang';
|
import type { LangFn } from '../../hooks/useOldLang';
|
||||||
@ -24,10 +22,15 @@ import {
|
|||||||
VIDEO_STICKER_MIME_TYPE,
|
VIDEO_STICKER_MIME_TYPE,
|
||||||
} from '../../config';
|
} from '../../config';
|
||||||
import { areSortedArraysIntersecting, unique } from '../../util/iteratees';
|
import { areSortedArraysIntersecting, unique } from '../../util/iteratees';
|
||||||
import { isLocalMessageId } from '../../util/messageKey';
|
import { isLocalMessageId } from '../../util/keys/messageKey';
|
||||||
import { getServerTime } from '../../util/serverTime';
|
import { getServerTime } from '../../util/serverTime';
|
||||||
import { getGlobal } from '../index';
|
import { getGlobal } from '../index';
|
||||||
import { getChatTitle, getCleanPeerId, isUserId } from './chats';
|
import {
|
||||||
|
getChatTitle,
|
||||||
|
getCleanPeerId,
|
||||||
|
isPeerUser,
|
||||||
|
isUserId,
|
||||||
|
} from './chats';
|
||||||
import { getMainUsername, getUserFullName } from './users';
|
import { getMainUsername, getUserFullName } from './users';
|
||||||
|
|
||||||
const RE_LINK = new RegExp(RE_LINK_TEMPLATE, 'i');
|
const RE_LINK = new RegExp(RE_LINK_TEMPLATE, 'i');
|
||||||
@ -182,7 +185,7 @@ export function isAnonymousOwnMessage(message: ApiMessage) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function getSenderTitle(lang: LangFn, sender: ApiPeer) {
|
export function getSenderTitle(lang: LangFn, sender: ApiPeer) {
|
||||||
return isUserId(sender.id) ? getUserFullName(sender as ApiUser) : getChatTitle(lang, sender as ApiChat);
|
return isPeerUser(sender) ? getUserFullName(sender) : getChatTitle(lang, sender);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getSendingState(message: ApiMessage) {
|
export function getSendingState(message: ApiMessage) {
|
||||||
|
|||||||
@ -6,12 +6,12 @@ import { isCacheApiSupported } from '../util/cacheApi';
|
|||||||
import { getCurrentTabId, reestablishMasterToSelf } from '../util/establishMultitabRole';
|
import { getCurrentTabId, reestablishMasterToSelf } from '../util/establishMultitabRole';
|
||||||
import { initGlobal } from '../util/init';
|
import { initGlobal } from '../util/init';
|
||||||
import { cloneDeep } from '../util/iteratees';
|
import { cloneDeep } from '../util/iteratees';
|
||||||
import { isLocalMessageId } from '../util/messageKey';
|
import { isLocalMessageId } from '../util/keys/messageKey';
|
||||||
import { Bundles, loadBundle } from '../util/moduleLoader';
|
import { Bundles, loadBundle } from '../util/moduleLoader';
|
||||||
import { parseLocationHash } from '../util/routing';
|
import { parseLocationHash } from '../util/routing';
|
||||||
import { updatePeerColors } from '../util/theme';
|
import { updatePeerColors } from '../util/theme';
|
||||||
import { IS_MULTITAB_SUPPORTED } from '../util/windowEnvironment';
|
import { IS_MULTITAB_SUPPORTED } from '../util/windowEnvironment';
|
||||||
import { initializeChatMediaSearchResults } from './reducers/localSearch';
|
import { initializeChatMediaSearchResults } from './reducers/middleSearch';
|
||||||
import { updateTabState } from './reducers/tabs';
|
import { updateTabState } from './reducers/tabs';
|
||||||
import { initCache } from './cache';
|
import { initCache } from './cache';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@ -108,6 +108,11 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
|
|||||||
byId: {},
|
byId: {},
|
||||||
fullInfoById: {},
|
fullInfoById: {},
|
||||||
similarChannelsById: {},
|
similarChannelsById: {},
|
||||||
|
loadingParameters: {
|
||||||
|
active: {},
|
||||||
|
archived: {},
|
||||||
|
saved: {},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
messages: {
|
messages: {
|
||||||
@ -317,7 +322,7 @@ export const INITIAL_TAB_STATE: TabState = {
|
|||||||
|
|
||||||
userSearch: {},
|
userSearch: {},
|
||||||
|
|
||||||
localTextSearch: {
|
middleSearch: {
|
||||||
byChatThreadKey: {},
|
byChatThreadKey: {},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@ -30,6 +30,25 @@ export function replaceChatListIds<T extends GlobalState>(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function replaceChatListLoadingParameters<T extends GlobalState>(
|
||||||
|
global: T, type: ChatListType, nextOffsetId?: number, nextOffsetPeerId?: string, nextOffsetDate?: number,
|
||||||
|
): T {
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
chats: {
|
||||||
|
...global.chats,
|
||||||
|
loadingParameters: {
|
||||||
|
...global.chats.loadingParameters,
|
||||||
|
[type]: {
|
||||||
|
nextOffsetId,
|
||||||
|
nextOffsetPeerId,
|
||||||
|
nextOffsetDate,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
export function updateChatLastMessageId<T extends GlobalState>(
|
export function updateChatLastMessageId<T extends GlobalState>(
|
||||||
global: T, chatId: string, lastMessageId: number, listType?: ChatListType,
|
global: T, chatId: string, lastMessageId: number, listType?: ChatListType,
|
||||||
): T {
|
): T {
|
||||||
|
|||||||
@ -4,11 +4,10 @@ import type { GlobalState, TabArgs, TabState } from '../types';
|
|||||||
|
|
||||||
import { getCurrentTabId } from '../../util/establishMultitabRole';
|
import { getCurrentTabId } from '../../util/establishMultitabRole';
|
||||||
import { areSortedArraysEqual } from '../../util/iteratees';
|
import { areSortedArraysEqual } from '../../util/iteratees';
|
||||||
|
import { getSearchResultKey } from '../../util/keys/searchResultKey';
|
||||||
import { selectTabState } from '../selectors';
|
import { selectTabState } from '../selectors';
|
||||||
import { updateTabState } from './tabs';
|
import { updateTabState } from './tabs';
|
||||||
|
|
||||||
const getComplexKey = (message: ApiMessage) => `${message.chatId}_${message.id}`;
|
|
||||||
|
|
||||||
export function updateGlobalSearch<T extends GlobalState>(
|
export function updateGlobalSearch<T extends GlobalState>(
|
||||||
global: T,
|
global: T,
|
||||||
searchStatePartial: Partial<TabState['globalSearch']>,
|
searchStatePartial: Partial<TabState['globalSearch']>,
|
||||||
@ -35,12 +34,14 @@ export function updateGlobalSearchResults<T extends GlobalState>(
|
|||||||
newFoundMessages: ApiMessage[],
|
newFoundMessages: ApiMessage[],
|
||||||
totalCount: number,
|
totalCount: number,
|
||||||
type: ApiGlobalMessageSearchType,
|
type: ApiGlobalMessageSearchType,
|
||||||
nextRate?: number,
|
nextOffsetRate?: number,
|
||||||
|
nextOffsetId?: number,
|
||||||
|
nextOffsetPeerId?: string,
|
||||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||||
): T {
|
): T {
|
||||||
const { resultsByType } = selectTabState(global, tabId).globalSearch || {};
|
const { resultsByType } = selectTabState(global, tabId).globalSearch || {};
|
||||||
const newFoundMessagesById = newFoundMessages.reduce((result, message) => {
|
const newFoundMessagesById = newFoundMessages.reduce((result, message) => {
|
||||||
result[getComplexKey(message)] = message;
|
result[getSearchResultKey(message)] = message;
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}, {} as Record<string, ApiMessage>);
|
}, {} as Record<string, ApiMessage>);
|
||||||
@ -48,7 +49,7 @@ export function updateGlobalSearchResults<T extends GlobalState>(
|
|||||||
|
|
||||||
if (foundIdsForType !== undefined
|
if (foundIdsForType !== undefined
|
||||||
&& Object.keys(newFoundMessagesById).every(
|
&& Object.keys(newFoundMessagesById).every(
|
||||||
(newId) => foundIdsForType.includes(getComplexKey(newFoundMessagesById[newId])),
|
(newId) => foundIdsForType.includes(getSearchResultKey(newFoundMessagesById[newId])),
|
||||||
)
|
)
|
||||||
) {
|
) {
|
||||||
return updateGlobalSearchFetchingStatus(global, { messages: false }, tabId);
|
return updateGlobalSearchFetchingStatus(global, { messages: false }, tabId);
|
||||||
@ -56,7 +57,7 @@ export function updateGlobalSearchResults<T extends GlobalState>(
|
|||||||
|
|
||||||
const prevFoundIds = foundIdsForType || [];
|
const prevFoundIds = foundIdsForType || [];
|
||||||
const newFoundIds = newFoundMessages
|
const newFoundIds = newFoundMessages
|
||||||
.map((message) => getComplexKey(message))
|
.map((message) => getSearchResultKey(message))
|
||||||
.filter((id) => !prevFoundIds.includes(id));
|
.filter((id) => !prevFoundIds.includes(id));
|
||||||
const foundIds = Array.prototype.concat(prevFoundIds, newFoundIds);
|
const foundIds = Array.prototype.concat(prevFoundIds, newFoundIds);
|
||||||
const foundOrPrevFoundIds = areSortedArraysEqual(prevFoundIds, foundIds) ? prevFoundIds : foundIds;
|
const foundOrPrevFoundIds = areSortedArraysEqual(prevFoundIds, foundIds) ? prevFoundIds : foundIds;
|
||||||
@ -68,7 +69,9 @@ export function updateGlobalSearchResults<T extends GlobalState>(
|
|||||||
...(selectTabState(global, tabId).globalSearch || {}).resultsByType,
|
...(selectTabState(global, tabId).globalSearch || {}).resultsByType,
|
||||||
[type]: {
|
[type]: {
|
||||||
totalCount,
|
totalCount,
|
||||||
nextOffsetId: nextRate,
|
nextOffsetId,
|
||||||
|
nextOffsetRate,
|
||||||
|
nextOffsetPeerId,
|
||||||
foundIds: foundOrPrevFoundIds,
|
foundIds: foundOrPrevFoundIds,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@ -3,7 +3,7 @@ export * from './messages';
|
|||||||
export * from './symbols';
|
export * from './symbols';
|
||||||
export * from './users';
|
export * from './users';
|
||||||
export * from './globalSearch';
|
export * from './globalSearch';
|
||||||
export * from './localSearch';
|
export * from './middleSearch';
|
||||||
export * from './management';
|
export * from './management';
|
||||||
export * from './settings';
|
export * from './settings';
|
||||||
export * from './twoFaSettings';
|
export * from './twoFaSettings';
|
||||||
|
|||||||
@ -14,7 +14,7 @@ import { getCurrentTabId } from '../../util/establishMultitabRole';
|
|||||||
import {
|
import {
|
||||||
areSortedArraysEqual, excludeSortedArray, omit, pick, pickTruthy, unique,
|
areSortedArraysEqual, excludeSortedArray, omit, pick, pickTruthy, unique,
|
||||||
} from '../../util/iteratees';
|
} from '../../util/iteratees';
|
||||||
import { isLocalMessageId, type MessageKey } from '../../util/messageKey';
|
import { isLocalMessageId, type MessageKey } from '../../util/keys/messageKey';
|
||||||
import {
|
import {
|
||||||
hasMessageTtl, isMediaLoadableInViewer,
|
hasMessageTtl, isMediaLoadableInViewer,
|
||||||
mergeIdRanges, orderHistoryIds, orderPinnedIds,
|
mergeIdRanges, orderHistoryIds, orderPinnedIds,
|
||||||
@ -38,7 +38,7 @@ import {
|
|||||||
selectThreadInfo,
|
selectThreadInfo,
|
||||||
selectViewportIds,
|
selectViewportIds,
|
||||||
} from '../selectors';
|
} from '../selectors';
|
||||||
import { removeIdFromSearchResults } from './localSearch';
|
import { removeIdFromSearchResults } from './middleSearch';
|
||||||
import { updateTabState } from './tabs';
|
import { updateTabState } from './tabs';
|
||||||
import { clearMessageTranslation } from './translations';
|
import { clearMessageTranslation } from './translations';
|
||||||
|
|
||||||
|
|||||||
@ -1,27 +1,24 @@
|
|||||||
import type { ApiMessage, ApiMessageSearchType, ApiReaction } from '../../api/types';
|
import type { ApiMessage, ApiMessageSearchType } from '../../api/types';
|
||||||
import type {
|
import type {
|
||||||
ChatMediaSearchParams, ChatMediaSearchSegment, LoadingState,
|
ChatMediaSearchParams,
|
||||||
SharedMediaType, ThreadId,
|
ChatMediaSearchSegment,
|
||||||
|
LoadingState,
|
||||||
|
MiddleSearchParams,
|
||||||
|
MiddleSearchResults,
|
||||||
|
SharedMediaType,
|
||||||
|
ThreadId,
|
||||||
} from '../../types';
|
} from '../../types';
|
||||||
import type { GlobalState, TabArgs } from '../types';
|
import type { GlobalState, TabArgs } from '../types';
|
||||||
|
|
||||||
import { getCurrentTabId } from '../../util/establishMultitabRole';
|
import { getCurrentTabId } from '../../util/establishMultitabRole';
|
||||||
import { areSortedArraysEqual, areSortedArraysIntersecting, unique } from '../../util/iteratees';
|
import {
|
||||||
|
areSortedArraysEqual, areSortedArraysIntersecting, omit, unique,
|
||||||
|
} from '../../util/iteratees';
|
||||||
import { buildChatThreadKey, isMediaLoadableInViewer } from '../helpers';
|
import { buildChatThreadKey, isMediaLoadableInViewer } from '../helpers';
|
||||||
import { selectTabState } from '../selectors';
|
import { selectTabState } from '../selectors';
|
||||||
import { selectChatMediaSearch } from '../selectors/localSearch';
|
import { selectChatMediaSearch } from '../selectors/middleSearch';
|
||||||
import { updateTabState } from './tabs';
|
import { updateTabState } from './tabs';
|
||||||
|
|
||||||
interface TextSearchParams {
|
|
||||||
query?: string;
|
|
||||||
savedTag?: ApiReaction;
|
|
||||||
results?: {
|
|
||||||
totalCount?: number;
|
|
||||||
nextOffsetId?: number;
|
|
||||||
foundIds?: number[];
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface SharedMediaSearchParams {
|
interface SharedMediaSearchParams {
|
||||||
currentType?: SharedMediaType;
|
currentType?: SharedMediaType;
|
||||||
resultsByType?: Partial<Record<SharedMediaType, {
|
resultsByType?: Partial<Record<SharedMediaType, {
|
||||||
@ -31,93 +28,128 @@ interface SharedMediaSearchParams {
|
|||||||
}>>;
|
}>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function replaceLocalTextSearch<T extends GlobalState>(
|
function replaceMiddleSearch<T extends GlobalState>(
|
||||||
global: T,
|
global: T,
|
||||||
chatThreadKey: string,
|
chatThreadKey: string,
|
||||||
searchParams: TextSearchParams,
|
searchParams?: MiddleSearchParams,
|
||||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||||
): T {
|
): T {
|
||||||
|
const current = selectTabState(global, tabId).middleSearch.byChatThreadKey;
|
||||||
|
if (!searchParams) {
|
||||||
|
return updateTabState(global, {
|
||||||
|
middleSearch: {
|
||||||
|
byChatThreadKey: omit(current, [chatThreadKey]),
|
||||||
|
},
|
||||||
|
}, tabId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const { type = 'chat', ...rest } = searchParams;
|
||||||
return updateTabState(global, {
|
return updateTabState(global, {
|
||||||
localTextSearch: {
|
middleSearch: {
|
||||||
byChatThreadKey: {
|
byChatThreadKey: {
|
||||||
...selectTabState(global, tabId).localTextSearch.byChatThreadKey,
|
...selectTabState(global, tabId).middleSearch.byChatThreadKey,
|
||||||
[chatThreadKey]: searchParams,
|
[chatThreadKey]: {
|
||||||
|
type,
|
||||||
|
...rest,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}, tabId);
|
}, tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateLocalTextSearch<T extends GlobalState>(
|
export function updateMiddleSearch<T extends GlobalState>(
|
||||||
global: T,
|
global: T,
|
||||||
chatId: string,
|
chatId: string,
|
||||||
threadId: ThreadId,
|
threadId: ThreadId,
|
||||||
query?: string,
|
update: Partial<MiddleSearchParams>,
|
||||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||||
): T {
|
): T {
|
||||||
const chatThreadKey = buildChatThreadKey(chatId, threadId);
|
const chatThreadKey = buildChatThreadKey(chatId, threadId);
|
||||||
|
const currentSearch = selectTabState(global, tabId).middleSearch.byChatThreadKey[chatThreadKey];
|
||||||
|
|
||||||
return replaceLocalTextSearch(global, chatThreadKey, {
|
const updated = {
|
||||||
...selectTabState(global, tabId).localTextSearch.byChatThreadKey[chatThreadKey],
|
type: 'chat',
|
||||||
query,
|
|
||||||
}, tabId);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function updateLocalTextSearchTag<T extends GlobalState>(
|
|
||||||
global: T,
|
|
||||||
chatId: string,
|
|
||||||
threadId: ThreadId,
|
|
||||||
tag?: ApiReaction,
|
|
||||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
|
||||||
): T {
|
|
||||||
const chatThreadKey = buildChatThreadKey(chatId, threadId);
|
|
||||||
|
|
||||||
const currentSearch = selectTabState(global, tabId).localTextSearch.byChatThreadKey[chatThreadKey];
|
|
||||||
const query = currentSearch?.query || '';
|
|
||||||
|
|
||||||
return replaceLocalTextSearch(global, chatThreadKey, {
|
|
||||||
...currentSearch,
|
...currentSearch,
|
||||||
query,
|
...update,
|
||||||
savedTag: tag,
|
} satisfies MiddleSearchParams;
|
||||||
}, tabId);
|
|
||||||
|
if (!updated.isHashtag) {
|
||||||
|
updated.type = 'chat';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentSearch && (currentSearch.type !== updated.type || currentSearch.savedTag !== updated.savedTag)) {
|
||||||
|
updated.results = undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return replaceMiddleSearch(global, chatThreadKey, updated, tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function replaceLocalTextSearchResults<T extends GlobalState>(
|
export function resetMiddleSearch<T extends GlobalState>(
|
||||||
global: T,
|
global: T,
|
||||||
chatId: string,
|
chatId: string,
|
||||||
threadId: ThreadId,
|
threadId: ThreadId,
|
||||||
foundIds?: number[],
|
|
||||||
totalCount?: number,
|
|
||||||
nextOffsetId?: number,
|
|
||||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||||
): T {
|
): T {
|
||||||
const chatThreadKey = buildChatThreadKey(chatId, threadId);
|
return replaceMiddleSearch(global, buildChatThreadKey(chatId, threadId), {
|
||||||
|
type: 'chat',
|
||||||
return replaceLocalTextSearch(global, chatThreadKey, {
|
|
||||||
...selectTabState(global, tabId).localTextSearch.byChatThreadKey[chatThreadKey],
|
|
||||||
results: {
|
|
||||||
foundIds,
|
|
||||||
totalCount,
|
|
||||||
nextOffsetId,
|
|
||||||
},
|
|
||||||
}, tabId);
|
}, tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateLocalTextSearchResults<T extends GlobalState>(
|
function replaceMiddleSearchResults<T extends GlobalState>(
|
||||||
global: T,
|
global: T,
|
||||||
chatId: string,
|
chatId: string,
|
||||||
threadId: ThreadId,
|
threadId: ThreadId,
|
||||||
newFoundIds: number[],
|
results: MiddleSearchResults,
|
||||||
totalCount?: number,
|
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||||
nextOffsetId?: number,
|
): T {
|
||||||
|
return updateMiddleSearch(global, chatId, threadId, {
|
||||||
|
results,
|
||||||
|
fetchingQuery: undefined,
|
||||||
|
}, tabId);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateMiddleSearchResults<T extends GlobalState>(
|
||||||
|
global: T,
|
||||||
|
chatId: string,
|
||||||
|
threadId: ThreadId,
|
||||||
|
update: MiddleSearchResults,
|
||||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||||
): T {
|
): T {
|
||||||
const chatThreadKey = buildChatThreadKey(chatId, threadId);
|
const chatThreadKey = buildChatThreadKey(chatId, threadId);
|
||||||
const { results } = selectTabState(global, tabId).localTextSearch.byChatThreadKey[chatThreadKey] || {};
|
const { results } = selectTabState(global, tabId).middleSearch.byChatThreadKey[chatThreadKey] || {};
|
||||||
|
const prevQuery = (results?.query) || '';
|
||||||
|
if (update.query !== prevQuery) {
|
||||||
|
return replaceMiddleSearchResults(global, chatId, threadId, update, tabId);
|
||||||
|
}
|
||||||
|
|
||||||
const prevFoundIds = (results?.foundIds) || [];
|
const prevFoundIds = (results?.foundIds) || [];
|
||||||
const foundIds = orderFoundIdsByDescending(unique(Array.prototype.concat(prevFoundIds, newFoundIds)));
|
const {
|
||||||
|
query, foundIds: newFoundIds, totalCount, nextOffsetId, nextOffsetPeerId, nextOffsetRate,
|
||||||
|
} = update;
|
||||||
|
const foundIds = unique(Array.prototype.concat(prevFoundIds, newFoundIds));
|
||||||
const foundOrPrevFoundIds = areSortedArraysEqual(prevFoundIds, foundIds) ? prevFoundIds : foundIds;
|
const foundOrPrevFoundIds = areSortedArraysEqual(prevFoundIds, foundIds) ? prevFoundIds : foundIds;
|
||||||
|
|
||||||
return replaceLocalTextSearchResults(global, chatId, threadId, foundOrPrevFoundIds, totalCount, nextOffsetId, tabId);
|
return replaceMiddleSearchResults(
|
||||||
|
global, chatId, threadId, {
|
||||||
|
query,
|
||||||
|
foundIds: foundOrPrevFoundIds,
|
||||||
|
totalCount,
|
||||||
|
nextOffsetId,
|
||||||
|
nextOffsetRate,
|
||||||
|
nextOffsetPeerId,
|
||||||
|
}, tabId,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function closeMiddleSearch<T extends GlobalState>(
|
||||||
|
global: T,
|
||||||
|
chatId: string,
|
||||||
|
threadId: ThreadId,
|
||||||
|
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||||
|
): T {
|
||||||
|
const chatThreadKey = buildChatThreadKey(chatId, threadId);
|
||||||
|
|
||||||
|
return replaceMiddleSearch(global, chatThreadKey, undefined, tabId);
|
||||||
}
|
}
|
||||||
|
|
||||||
function replaceSharedMediaSearch<T extends GlobalState>(
|
function replaceSharedMediaSearch<T extends GlobalState>(
|
||||||
@ -41,6 +41,12 @@ export function selectPeerFullInfo<T extends GlobalState>(global: T, peerId: str
|
|||||||
return selectChatFullInfo(global, peerId);
|
return selectChatFullInfo(global, peerId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function selectChatListLoadingParameters<T extends GlobalState>(
|
||||||
|
global: T, listType: ChatListType,
|
||||||
|
) {
|
||||||
|
return global.chats.loadingParameters[listType];
|
||||||
|
}
|
||||||
|
|
||||||
export function selectChatUser<T extends GlobalState>(global: T, chat: ApiChat) {
|
export function selectChatUser<T extends GlobalState>(global: T, chat: ApiChat) {
|
||||||
const userId = getPrivateChatUserId(chat);
|
const userId = getPrivateChatUserId(chat);
|
||||||
if (!userId) {
|
if (!userId) {
|
||||||
|
|||||||
@ -3,7 +3,7 @@ export * from './users';
|
|||||||
export * from './chats';
|
export * from './chats';
|
||||||
export * from './messages';
|
export * from './messages';
|
||||||
export * from './globalSearch';
|
export * from './globalSearch';
|
||||||
export * from './localSearch';
|
export * from './middleSearch';
|
||||||
export * from './management';
|
export * from './management';
|
||||||
export * from './symbols';
|
export * from './symbols';
|
||||||
export * from './payments';
|
export * from './payments';
|
||||||
|
|||||||
@ -22,8 +22,8 @@ import {
|
|||||||
} from '../../config';
|
} from '../../config';
|
||||||
import { getCurrentTabId } from '../../util/establishMultitabRole';
|
import { getCurrentTabId } from '../../util/establishMultitabRole';
|
||||||
import { findLast } from '../../util/iteratees';
|
import { findLast } from '../../util/iteratees';
|
||||||
|
import { getMessageKey, isLocalMessageId } from '../../util/keys/messageKey';
|
||||||
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
|
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
|
||||||
import { getMessageKey, isLocalMessageId } from '../../util/messageKey';
|
|
||||||
import { getServerTime } from '../../util/serverTime';
|
import { getServerTime } from '../../util/serverTime';
|
||||||
import { IS_TRANSLATION_SUPPORTED } from '../../util/windowEnvironment';
|
import { IS_TRANSLATION_SUPPORTED } from '../../util/windowEnvironment';
|
||||||
import {
|
import {
|
||||||
|
|||||||
@ -2,11 +2,11 @@ import type { ThreadId } from '../../types';
|
|||||||
import type { GlobalState, TabArgs } from '../types';
|
import type { GlobalState, TabArgs } from '../types';
|
||||||
|
|
||||||
import { getCurrentTabId } from '../../util/establishMultitabRole';
|
import { getCurrentTabId } from '../../util/establishMultitabRole';
|
||||||
import { buildChatThreadKey } from '../helpers/localSearch';
|
import { buildChatThreadKey } from '../helpers/middleSearch';
|
||||||
import { selectCurrentMessageList } from './messages';
|
import { selectCurrentMessageList } from './messages';
|
||||||
import { selectTabState } from './tabs';
|
import { selectTabState } from './tabs';
|
||||||
|
|
||||||
export function selectCurrentTextSearch<T extends GlobalState>(
|
export function selectCurrentMiddleSearch<T extends GlobalState>(
|
||||||
global: T,
|
global: T,
|
||||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||||
) {
|
) {
|
||||||
@ -16,12 +16,8 @@ export function selectCurrentTextSearch<T extends GlobalState>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const chatThreadKey = buildChatThreadKey(chatId, threadId);
|
const chatThreadKey = buildChatThreadKey(chatId, threadId);
|
||||||
const currentSearch = selectTabState(global, tabId).localTextSearch.byChatThreadKey[chatThreadKey];
|
|
||||||
if (!currentSearch || currentSearch.query === undefined) {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
return currentSearch;
|
return selectTabState(global, tabId).middleSearch.byChatThreadKey[chatThreadKey];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function selectCurrentSharedMediaSearch<T extends GlobalState>(
|
export function selectCurrentSharedMediaSearch<T extends GlobalState>(
|
||||||
@ -5,7 +5,6 @@ import { NewChatMembersProgress, RightColumnContent } from '../../types';
|
|||||||
|
|
||||||
import { getCurrentTabId } from '../../util/establishMultitabRole';
|
import { getCurrentTabId } from '../../util/establishMultitabRole';
|
||||||
import { getMessageVideo, getMessageWebPageVideo } from '../helpers/messageMedia';
|
import { getMessageVideo, getMessageWebPageVideo } from '../helpers/messageMedia';
|
||||||
import { selectCurrentTextSearch } from './localSearch';
|
|
||||||
import { selectCurrentManagement } from './management';
|
import { selectCurrentManagement } from './management';
|
||||||
import { selectIsStatisticsShown } from './statistics';
|
import { selectIsStatisticsShown } from './statistics';
|
||||||
import { selectTabState } from './tabs';
|
import { selectTabState } from './tabs';
|
||||||
@ -38,8 +37,6 @@ export function selectRightColumnContentKey<T extends GlobalState>(
|
|||||||
RightColumnContent.CreateTopic
|
RightColumnContent.CreateTopic
|
||||||
) : tabState.pollResults.messageId ? (
|
) : tabState.pollResults.messageId ? (
|
||||||
RightColumnContent.PollResults
|
RightColumnContent.PollResults
|
||||||
) : !isMobile && selectCurrentTextSearch(global, tabId) ? (
|
|
||||||
RightColumnContent.Search
|
|
||||||
) : selectCurrentManagement(global, tabId) ? (
|
) : selectCurrentManagement(global, tabId) ? (
|
||||||
RightColumnContent.Management
|
RightColumnContent.Management
|
||||||
) : tabState.isStatisticsShown && tabState.statistics.currentMessageId ? (
|
) : tabState.isStatisticsShown && tabState.statistics.currentMessageId ? (
|
||||||
|
|||||||
@ -112,6 +112,7 @@ import type {
|
|||||||
ManagementState,
|
ManagementState,
|
||||||
MediaViewerMedia,
|
MediaViewerMedia,
|
||||||
MediaViewerOrigin,
|
MediaViewerOrigin,
|
||||||
|
MiddleSearchParams,
|
||||||
NewChatMembersProgress,
|
NewChatMembersProgress,
|
||||||
NotifyException,
|
NotifyException,
|
||||||
PaymentStep,
|
PaymentStep,
|
||||||
@ -127,6 +128,7 @@ import type {
|
|||||||
ThemeKey,
|
ThemeKey,
|
||||||
ThreadId,
|
ThreadId,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
import type { SearchResultKey } from '../util/keys/searchResultKey';
|
||||||
import type { DownloadableMedia } from './helpers';
|
import type { DownloadableMedia } from './helpers';
|
||||||
|
|
||||||
export type MessageListType =
|
export type MessageListType =
|
||||||
@ -365,7 +367,8 @@ export type TabState = {
|
|||||||
|
|
||||||
globalSearch: {
|
globalSearch: {
|
||||||
query?: string;
|
query?: string;
|
||||||
date?: number;
|
minDate?: number;
|
||||||
|
maxDate?: number;
|
||||||
currentContent?: GlobalSearchContent;
|
currentContent?: GlobalSearchContent;
|
||||||
chatId?: string;
|
chatId?: string;
|
||||||
foundTopicIds?: number[];
|
foundTopicIds?: number[];
|
||||||
@ -382,8 +385,10 @@ export type TabState = {
|
|||||||
};
|
};
|
||||||
resultsByType?: Partial<Record<ApiGlobalMessageSearchType, {
|
resultsByType?: Partial<Record<ApiGlobalMessageSearchType, {
|
||||||
totalCount?: number;
|
totalCount?: number;
|
||||||
nextOffsetId: number;
|
nextOffsetId?: number;
|
||||||
foundIds: string[];
|
nextOffsetPeerId?: string;
|
||||||
|
nextOffsetRate?: number;
|
||||||
|
foundIds: SearchResultKey[];
|
||||||
}>>;
|
}>>;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -397,16 +402,8 @@ export type TabState = {
|
|||||||
activeEmojiInteractions?: ActiveEmojiInteraction[];
|
activeEmojiInteractions?: ActiveEmojiInteraction[];
|
||||||
activeReactions: Record<string, ApiReaction[]>;
|
activeReactions: Record<string, ApiReaction[]>;
|
||||||
|
|
||||||
localTextSearch: {
|
middleSearch: {
|
||||||
byChatThreadKey: Record<string, {
|
byChatThreadKey: Record<string, MiddleSearchParams | undefined>;
|
||||||
query?: string;
|
|
||||||
savedTag?: ApiReaction;
|
|
||||||
results?: {
|
|
||||||
totalCount?: number;
|
|
||||||
nextOffsetId?: number;
|
|
||||||
foundIds?: number[];
|
|
||||||
};
|
|
||||||
}>;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
sharedMediaSearch: {
|
sharedMediaSearch: {
|
||||||
@ -930,6 +927,11 @@ export type GlobalState = {
|
|||||||
all?: Record<string, number>;
|
all?: Record<string, number>;
|
||||||
saved?: Record<string, number>;
|
saved?: Record<string, number>;
|
||||||
};
|
};
|
||||||
|
loadingParameters: Record<ChatListType, {
|
||||||
|
nextOffsetId?: number;
|
||||||
|
nextOffsetPeerId?: string;
|
||||||
|
nextOffsetDate?: number;
|
||||||
|
}>;
|
||||||
forDiscussionIds?: string[];
|
forDiscussionIds?: string[];
|
||||||
// Obtained from GetFullChat / GetFullChannel
|
// Obtained from GetFullChat / GetFullChannel
|
||||||
fullInfoById: Record<string, ApiChatFullInfo>;
|
fullInfoById: Record<string, ApiChatFullInfo>;
|
||||||
@ -1342,19 +1344,26 @@ export interface ActionPayloads {
|
|||||||
userIds: string[];
|
userIds: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
// message search
|
// Message search
|
||||||
openLocalTextSearch: WithTabId | undefined;
|
openMiddleSearch: WithTabId | undefined;
|
||||||
closeLocalTextSearch: WithTabId | undefined;
|
closeMiddleSearch: WithTabId | undefined;
|
||||||
setLocalTextSearchQuery: {
|
updateMiddleSearch: {
|
||||||
|
chatId: string;
|
||||||
|
threadId?: ThreadId;
|
||||||
|
update: Partial<Omit<MiddleSearchParams, 'results'>>;
|
||||||
|
} & WithTabId;
|
||||||
|
resetMiddleSearch: WithTabId | undefined;
|
||||||
|
performMiddleSearch: {
|
||||||
|
chatId: string;
|
||||||
|
threadId?: ThreadId;
|
||||||
query?: string;
|
query?: string;
|
||||||
} & WithTabId;
|
} & WithTabId;
|
||||||
setLocalTextSearchTag: {
|
searchHashtag: {
|
||||||
tag: ApiReaction | undefined;
|
hashtag: string;
|
||||||
} & WithTabId;
|
} & WithTabId;
|
||||||
setSharedMediaSearchType: {
|
setSharedMediaSearchType: {
|
||||||
mediaType: SharedMediaType;
|
mediaType: SharedMediaType;
|
||||||
} & WithTabId;
|
} & WithTabId;
|
||||||
searchTextMessagesLocal: WithTabId | undefined;
|
|
||||||
searchSharedMediaMessages: WithTabId | undefined;
|
searchSharedMediaMessages: WithTabId | undefined;
|
||||||
searchChatMediaMessages: {
|
searchChatMediaMessages: {
|
||||||
currentMediaMessageId: number;
|
currentMediaMessageId: number;
|
||||||
|
|||||||
22
src/hooks/events/useOutsideClick.ts
Normal file
22
src/hooks/events/useOutsideClick.ts
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
import { useEffect } from '../../lib/teact/teact';
|
||||||
|
|
||||||
|
import useLastCallback from '../useLastCallback';
|
||||||
|
|
||||||
|
export function useClickOutside(
|
||||||
|
refs: React.RefObject<HTMLElement>[], callback: (event: MouseEvent) => void,
|
||||||
|
) {
|
||||||
|
const handleClickOutside = useLastCallback((event: MouseEvent) => {
|
||||||
|
const clickedOutside = refs.every((ref) => {
|
||||||
|
return ref.current && !ref.current.contains(event.target as Node);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (clickedOutside) callback(event);
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.addEventListener('click', handleClickOutside);
|
||||||
|
return () => {
|
||||||
|
document.removeEventListener('click', handleClickOutside);
|
||||||
|
};
|
||||||
|
}, [handleClickOutside]);
|
||||||
|
}
|
||||||
@ -17,19 +17,20 @@ const useInfiniteScroll = <ListId extends string | number>(
|
|||||||
listIds?: ListId[],
|
listIds?: ListId[],
|
||||||
isDisabled = false,
|
isDisabled = false,
|
||||||
listSlice = DEFAULT_LIST_SLICE,
|
listSlice = DEFAULT_LIST_SLICE,
|
||||||
): [ListId[]?, GetMore?] => {
|
): [ListId[]?, GetMore?, number?] => {
|
||||||
const requestParamsRef = useRef<{
|
const requestParamsRef = useRef<{
|
||||||
direction?: LoadMoreDirection;
|
direction?: LoadMoreDirection;
|
||||||
offsetId?: ListId;
|
offsetId?: ListId;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const currentStateRef = useRef<{ viewportIds: ListId[]; isOnTop: boolean } | undefined>();
|
const currentStateRef = useRef<{ viewportIds: ListId[]; isOnTop: boolean; offset: number } | undefined>();
|
||||||
if (!currentStateRef.current && listIds && !isDisabled) {
|
if (!currentStateRef.current && listIds && !isDisabled) {
|
||||||
const {
|
const {
|
||||||
newViewportIds,
|
newViewportIds,
|
||||||
newIsOnTop,
|
newIsOnTop,
|
||||||
|
fromOffset,
|
||||||
} = getViewportSlice(listIds, LoadMoreDirection.Forwards, listSlice, listIds[0]);
|
} = getViewportSlice(listIds, LoadMoreDirection.Forwards, listSlice, listIds[0]);
|
||||||
currentStateRef.current = { viewportIds: newViewportIds, isOnTop: newIsOnTop };
|
currentStateRef.current = { viewportIds: newViewportIds, isOnTop: newIsOnTop, offset: fromOffset };
|
||||||
}
|
}
|
||||||
|
|
||||||
const forceUpdate = useForceUpdate();
|
const forceUpdate = useForceUpdate();
|
||||||
@ -45,12 +46,12 @@ const useInfiniteScroll = <ListId extends string | number>(
|
|||||||
const currentMiddleId = viewportIds && !isOnTop ? viewportIds[Math.round(viewportIds.length / 2)] : undefined;
|
const currentMiddleId = viewportIds && !isOnTop ? viewportIds[Math.round(viewportIds.length / 2)] : undefined;
|
||||||
const defaultOffsetId = currentMiddleId && listIds.includes(currentMiddleId) ? currentMiddleId : listIds[0];
|
const defaultOffsetId = currentMiddleId && listIds.includes(currentMiddleId) ? currentMiddleId : listIds[0];
|
||||||
const { offsetId = defaultOffsetId, direction = LoadMoreDirection.Forwards } = requestParamsRef.current || {};
|
const { offsetId = defaultOffsetId, direction = LoadMoreDirection.Forwards } = requestParamsRef.current || {};
|
||||||
const { newViewportIds, newIsOnTop } = getViewportSlice(listIds, direction, listSlice, offsetId);
|
const { newViewportIds, newIsOnTop, fromOffset } = getViewportSlice(listIds, direction, listSlice, offsetId);
|
||||||
|
|
||||||
requestParamsRef.current = {};
|
requestParamsRef.current = {};
|
||||||
|
|
||||||
if (!viewportIds || !areSortedArraysEqual(viewportIds, newViewportIds)) {
|
if (!viewportIds || !areSortedArraysEqual(viewportIds, newViewportIds)) {
|
||||||
currentStateRef.current = { viewportIds: newViewportIds, isOnTop: newIsOnTop };
|
currentStateRef.current = { viewportIds: newViewportIds, isOnTop: newIsOnTop, offset: fromOffset };
|
||||||
}
|
}
|
||||||
} else if (!listIds) {
|
} else if (!listIds) {
|
||||||
currentStateRef.current = undefined;
|
currentStateRef.current = undefined;
|
||||||
@ -75,11 +76,11 @@ const useInfiniteScroll = <ListId extends string | number>(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
newViewportIds, areSomeLocal, areAllLocal, newIsOnTop,
|
newViewportIds, areSomeLocal, areAllLocal, newIsOnTop, fromOffset,
|
||||||
} = getViewportSlice(listIds, direction, listSlice, offsetId);
|
} = getViewportSlice(listIds, direction, listSlice, offsetId);
|
||||||
|
|
||||||
if (areSomeLocal && !(viewportIds && areSortedArraysEqual(viewportIds, newViewportIds))) {
|
if (areSomeLocal && !(viewportIds && areSortedArraysEqual(viewportIds, newViewportIds))) {
|
||||||
currentStateRef.current = { viewportIds: newViewportIds, isOnTop: newIsOnTop };
|
currentStateRef.current = { viewportIds: newViewportIds, isOnTop: newIsOnTop, offset: fromOffset };
|
||||||
forceUpdate();
|
forceUpdate();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -92,7 +93,7 @@ const useInfiniteScroll = <ListId extends string | number>(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return isDisabled ? [listIds] : [currentStateRef.current?.viewportIds, getMore];
|
return isDisabled ? [listIds] : [currentStateRef.current?.viewportIds, getMore, currentStateRef.current?.offset];
|
||||||
};
|
};
|
||||||
|
|
||||||
function getViewportSlice<ListId extends string | number>(
|
function getViewportSlice<ListId extends string | number>(
|
||||||
@ -127,6 +128,7 @@ function getViewportSlice<ListId extends string | number>(
|
|||||||
areSomeLocal,
|
areSomeLocal,
|
||||||
areAllLocal,
|
areAllLocal,
|
||||||
newIsOnTop: newViewportIds[0] === sourceIds[0],
|
newIsOnTop: newViewportIds[0] === sourceIds[0],
|
||||||
|
fromOffset: from,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,10 +2,14 @@ import { useCallback, useEffect, useRef } from '../lib/teact/teact';
|
|||||||
|
|
||||||
const DEFAULT_THRESHOLD = 250;
|
const DEFAULT_THRESHOLD = 250;
|
||||||
|
|
||||||
function useLongPress(
|
function useLongPress({
|
||||||
onStart: NoneToVoidFunction,
|
onClick, onStart, onEnd, threshold = DEFAULT_THRESHOLD,
|
||||||
onEnd: NoneToVoidFunction,
|
}: {
|
||||||
) {
|
onStart?: NoneToVoidFunction;
|
||||||
|
onClick?: (event: React.MouseEvent | React.TouchEvent) => void;
|
||||||
|
onEnd?: NoneToVoidFunction;
|
||||||
|
threshold?: number;
|
||||||
|
}) {
|
||||||
const isLongPressActive = useRef(false);
|
const isLongPressActive = useRef(false);
|
||||||
const isPressed = useRef(false);
|
const isPressed = useRef(false);
|
||||||
const timerId = useRef<number | undefined>(undefined);
|
const timerId = useRef<number | undefined>(undefined);
|
||||||
@ -18,20 +22,24 @@ function useLongPress(
|
|||||||
|
|
||||||
isPressed.current = true;
|
isPressed.current = true;
|
||||||
timerId.current = window.setTimeout(() => {
|
timerId.current = window.setTimeout(() => {
|
||||||
onStart();
|
onStart?.();
|
||||||
isLongPressActive.current = true;
|
isLongPressActive.current = true;
|
||||||
}, DEFAULT_THRESHOLD);
|
}, threshold);
|
||||||
}, [onStart]);
|
}, [onStart, threshold]);
|
||||||
|
|
||||||
|
const cancel = useCallback((e: React.MouseEvent | React.TouchEvent) => {
|
||||||
|
if (!isPressed.current) return;
|
||||||
|
|
||||||
const cancel = useCallback(() => {
|
|
||||||
if (isLongPressActive.current) {
|
if (isLongPressActive.current) {
|
||||||
onEnd();
|
onEnd?.();
|
||||||
|
} else {
|
||||||
|
onClick?.(e);
|
||||||
}
|
}
|
||||||
|
|
||||||
isLongPressActive.current = false;
|
isLongPressActive.current = false;
|
||||||
isPressed.current = false;
|
isPressed.current = false;
|
||||||
window.clearTimeout(timerId.current);
|
window.clearTimeout(timerId.current);
|
||||||
}, [onEnd]);
|
}, [onEnd, onClick]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
return () => {
|
return () => {
|
||||||
|
|||||||
@ -1612,6 +1612,7 @@ channels.clickSponsoredMessage#18afbc93 channel:InputChannel random_id:bytes = B
|
|||||||
channels.toggleViewForumAsMessages#9738bb15 channel:InputChannel enabled:Bool = Updates;
|
channels.toggleViewForumAsMessages#9738bb15 channel:InputChannel enabled:Bool = Updates;
|
||||||
channels.getChannelRecommendations#25a71742 flags:# channel:flags.0?InputChannel = messages.Chats;
|
channels.getChannelRecommendations#25a71742 flags:# channel:flags.0?InputChannel = messages.Chats;
|
||||||
channels.reportSponsoredMessage#af8ff6b9 channel:InputChannel random_id:bytes option:bytes = channels.SponsoredMessageReportResult;
|
channels.reportSponsoredMessage#af8ff6b9 channel:InputChannel random_id:bytes option:bytes = channels.SponsoredMessageReportResult;
|
||||||
|
channels.searchPosts#d19f987b hashtag:string offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages;
|
||||||
bots.setBotInfo#10cf3123 flags:# bot:flags.2?InputUser lang_code:string name:flags.3?string about:flags.0?string description:flags.1?string = Bool;
|
bots.setBotInfo#10cf3123 flags:# bot:flags.2?InputUser lang_code:string name:flags.3?string about:flags.0?string description:flags.1?string = Bool;
|
||||||
bots.canSendMessage#1359f4e6 bot:InputUser = Bool;
|
bots.canSendMessage#1359f4e6 bot:InputUser = Bool;
|
||||||
bots.allowSendMessage#f132e3ef bot:InputUser = Updates;
|
bots.allowSendMessage#f132e3ef bot:InputUser = Updates;
|
||||||
|
|||||||
@ -224,6 +224,7 @@
|
|||||||
"channels.getSponsoredMessages",
|
"channels.getSponsoredMessages",
|
||||||
"channels.getChannelRecommendations",
|
"channels.getChannelRecommendations",
|
||||||
"channels.reportSponsoredMessage",
|
"channels.reportSponsoredMessage",
|
||||||
|
"channels.searchPosts",
|
||||||
"bots.canSendMessage",
|
"bots.canSendMessage",
|
||||||
"bots.allowSendMessage",
|
"bots.allowSendMessage",
|
||||||
"bots.invokeWebViewCustomMethod",
|
"bots.invokeWebViewCustomMethod",
|
||||||
|
|||||||
@ -915,7 +915,7 @@ function DEBUG_checkKeyUniqueness(children: VirtualElementChildren) {
|
|||||||
|
|
||||||
if (keys.length !== unique(keys).length) {
|
if (keys.length !== unique(keys).length) {
|
||||||
// eslint-disable-next-line no-console
|
// eslint-disable-next-line no-console
|
||||||
console.warn('[Teact] Duplicated keys:', keys.filter((e, i, a) => a.indexOf(e) !== i));
|
console.warn('[Teact] Duplicated keys:', keys.filter((e, i, a) => a.indexOf(e) !== i), children);
|
||||||
throw new Error('[Teact] Children keys are not unique');
|
throw new Error('[Teact] Children keys are not unique');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -256,7 +256,7 @@ $color-message-story-mention-to: #74bcff;
|
|||||||
--z-forum-panel: 13;
|
--z-forum-panel: 13;
|
||||||
--z-message-context-menu: 13;
|
--z-message-context-menu: 13;
|
||||||
--z-scroll-down-button: 12;
|
--z-scroll-down-button: 12;
|
||||||
--z-mobile-search: 12;
|
--z-local-search: 12;
|
||||||
--z-left-header: 11;
|
--z-left-header: 11;
|
||||||
--z-middle-header: 11;
|
--z-middle-header: 11;
|
||||||
--z-middle-footer: 11;
|
--z-middle-footer: 11;
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import type {
|
|||||||
ApiUser,
|
ApiUser,
|
||||||
ApiVideo,
|
ApiVideo,
|
||||||
} from '../api/types';
|
} from '../api/types';
|
||||||
|
import type { SearchResultKey } from '../util/keys/searchResultKey';
|
||||||
import type { IconName } from './icons';
|
import type { IconName } from './icons';
|
||||||
|
|
||||||
export type TextPart = TeactNode;
|
export type TextPart = TeactNode;
|
||||||
@ -299,7 +300,6 @@ export enum GlobalSearchContent {
|
|||||||
|
|
||||||
export enum RightColumnContent {
|
export enum RightColumnContent {
|
||||||
ChatInfo,
|
ChatInfo,
|
||||||
Search,
|
|
||||||
Management,
|
Management,
|
||||||
Statistics,
|
Statistics,
|
||||||
BoostStatistics,
|
BoostStatistics,
|
||||||
@ -399,6 +399,24 @@ export type ProfileTabType =
|
|||||||
| 'similarChannels'
|
| 'similarChannels'
|
||||||
| 'dialogs';
|
| 'dialogs';
|
||||||
export type SharedMediaType = 'media' | 'documents' | 'links' | 'audio' | 'voice';
|
export type SharedMediaType = 'media' | 'documents' | 'links' | 'audio' | 'voice';
|
||||||
|
export type MiddleSearchType = 'chat' | 'myChats' | 'channels';
|
||||||
|
export type MiddleSearchParams = {
|
||||||
|
requestedQuery?: string;
|
||||||
|
savedTag?: ApiReaction;
|
||||||
|
isHashtag?: boolean;
|
||||||
|
fetchingQuery?: string;
|
||||||
|
type: MiddleSearchType;
|
||||||
|
results?: MiddleSearchResults;
|
||||||
|
};
|
||||||
|
export type MiddleSearchResults = {
|
||||||
|
query: string;
|
||||||
|
totalCount?: number;
|
||||||
|
nextOffsetId?: number;
|
||||||
|
nextOffsetPeerId?: string;
|
||||||
|
nextOffsetRate?: number;
|
||||||
|
foundIds?: SearchResultKey[];
|
||||||
|
};
|
||||||
|
|
||||||
export type ApiPrivacyKey = 'phoneNumber' | 'addByPhone' | 'lastSeen' | 'profilePhoto' | 'voiceMessages' |
|
export type ApiPrivacyKey = 'phoneNumber' | 'addByPhone' | 'lastSeen' | 'profilePhoto' | 'voiceMessages' |
|
||||||
'forwards' | 'chatInvite' | 'phoneCall' | 'phoneP2P' | 'bio' | 'birthday';
|
'forwards' | 'chatInvite' | 'phoneCall' | 'phoneP2P' | 'bio' | 'birthday';
|
||||||
export type PrivacyVisibility = 'everybody' | 'contacts' | 'closeFriends' | 'nonContacts' | 'nobody';
|
export type PrivacyVisibility = 'everybody' | 'contacts' | 'closeFriends' | 'nonContacts' | 'nobody';
|
||||||
|
|||||||
6
src/types/language.d.ts
vendored
6
src/types/language.d.ts
vendored
@ -1185,7 +1185,6 @@ export interface LangPair {
|
|||||||
'AttachSticker': undefined;
|
'AttachSticker': undefined;
|
||||||
'AttachMusic': undefined;
|
'AttachMusic': undefined;
|
||||||
'AttachContact': undefined;
|
'AttachContact': undefined;
|
||||||
'PaymentInvoice': undefined;
|
|
||||||
'MessageLocation': undefined;
|
'MessageLocation': undefined;
|
||||||
'MessageLiveLocation': undefined;
|
'MessageLiveLocation': undefined;
|
||||||
'ServiceNotifications': undefined;
|
'ServiceNotifications': undefined;
|
||||||
@ -1511,8 +1510,11 @@ export interface LangPair {
|
|||||||
'MenuBetaChangelog': undefined;
|
'MenuBetaChangelog': undefined;
|
||||||
'MenuSwitchToK': undefined;
|
'MenuSwitchToK': undefined;
|
||||||
'MenuInstallApp': undefined;
|
'MenuInstallApp': undefined;
|
||||||
'RemoveEffect' : undefined;
|
'RemoveEffect': undefined;
|
||||||
'ReplyInPrivateMessage': undefined;
|
'ReplyInPrivateMessage': undefined;
|
||||||
|
'AriaSearchOlderResult': undefined;
|
||||||
|
'AriaSearchNewerResult': undefined;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export type LangKey = keyof LangPair;
|
export type LangKey = keyof LangPair;
|
||||||
|
|||||||
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