Search: New design (#4718)

This commit is contained in:
zubiden 2024-08-29 15:52:14 +02:00 committed by Alexander Zinchuk
parent 9bf26bbb1c
commit 5f5536b6a0
103 changed files with 2271 additions and 1455 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View 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);
}
}

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View 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);
});

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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: {},
}, },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 ? (

View File

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

View 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]);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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