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;
messages: ApiMessage[];
lastMessageByChatId: Record<string, number>;
nextOffsetId?: number;
nextOffsetPeerId?: string;
nextOffsetDate?: number;
};
let onUpdate: OnApiUpdate;
@ -111,18 +114,24 @@ export function init(_onUpdate: OnApiUpdate) {
export async function fetchChats({
limit,
offsetDate,
offsetPeer,
offsetId,
archived,
withPinned,
lastLocalServiceMessageId,
}: {
limit: number;
offsetDate?: number;
offsetPeer?: ApiPeer;
offsetId?: number;
archived?: boolean;
withPinned?: boolean;
lastLocalServiceMessageId?: number;
}): Promise<ChatListData | undefined> {
const peer = (offsetPeer && buildInputPeer(offsetPeer.id, offsetPeer.accessHash)) || new GramJs.InputPeerEmpty();
const result = await invokeRequest(new GramJs.messages.GetDialogs({
offsetPeer: new GramJs.InputPeerEmpty(),
offsetPeer: peer,
offsetId,
limit,
offsetDate,
...(withPinned && { excludePinned: true }),
@ -217,6 +226,13 @@ export async function fetchChats({
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 {
chatIds,
chats,
@ -227,20 +243,29 @@ export async function fetchChats({
totalChatCount,
lastMessageByChatId,
messages,
nextOffsetId,
nextOffsetPeerId,
nextOffsetDate,
};
}
export async function fetchSavedChats({
limit,
offsetDate,
offsetPeer,
offsetId,
withPinned,
}: {
limit: number;
offsetDate?: number;
offsetPeer?: ApiPeer;
offsetId?: number;
withPinned?: boolean;
}): Promise<ChatListData | undefined> {
const peer = (offsetPeer && buildInputPeer(offsetPeer.id, offsetPeer.accessHash)) || new GramJs.InputPeerEmpty();
const result = await invokeRequest(new GramJs.messages.GetSavedDialogs({
offsetPeer: new GramJs.InputPeerEmpty(),
offsetPeer: peer,
offsetId,
limit,
offsetDate,
...(withPinned && { excludePinned: true }),
@ -305,6 +330,13 @@ export async function fetchSavedChats({
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 {
chatIds,
chats,
@ -315,6 +347,9 @@ export async function fetchSavedChats({
lastMessageByChatId,
messages,
draftsById: {},
nextOffsetId,
nextOffsetPeerId,
nextOffsetDate,
};
}

View File

@ -27,7 +27,7 @@ export {
export {
fetchMessages, fetchMessage, sendMessage, pinMessage, unpinAllMessages, deleteMessages, deleteHistory,
markMessageListRead, markMessagesRead, searchMessagesLocal, searchMessagesGlobal,
markMessageListRead, markMessagesRead, searchMessagesInChat, searchMessagesGlobal, searchHashtagPosts,
fetchWebPagePreview, editMessage, forwardMessages, loadPollOptionResults, sendPollVote, findFirstMessageIdAfterDate,
fetchPinnedMessages, fetchScheduledHistory, sendScheduledMessages, rescheduleMessage, deleteScheduledMessages,
reportMessages, sendMessageAction, fetchSeenBy, fetchSponsoredMessages, viewSponsoredMessage, fetchSendAs,

View File

@ -22,6 +22,7 @@ import type {
ApiSticker,
ApiStory,
ApiStorySkipped,
ApiUser,
ApiVideo,
MediaContent,
OnApiUpdate,
@ -43,7 +44,7 @@ import {
import { getEmojiOnlyCountForMessage } from '../../../global/helpers/getEmojiOnlyCountForMessage';
import { fetchFile } from '../../../util/files';
import { compact, split } from '../../../util/iteratees';
import { getMessageKey } from '../../../util/messageKey';
import { getMessageKey } from '../../../util/keys/messageKey';
import { getServerTimeOffset } from '../../../util/serverTime';
import { interpolateArray } from '../../../util/waveform';
import { buildApiChatFromPreview, buildApiSendAsPeerId } from '../apiBuilders/chats';
@ -83,6 +84,7 @@ import {
addEntitiesToLocalDb,
addMessageToLocalDb,
deserializeBytes,
resolveMessageApiChatId,
} from '../helpers';
import { processAffectedHistory, updateChannelState } from '../updates/updateManager';
import { dispatchThreadInfoUpdates } from '../updates/updater';
@ -101,6 +103,16 @@ type TranslateTextParams = ({
toLanguageCode: string;
};
type SearchResults = {
messages: ApiMessage[];
users: ApiUser[];
chats: ApiChat[];
totalCount: number;
nextOffsetRate?: number;
nextOffsetPeerId?: string;
nextOffsetId?: number;
};
let onUpdate: OnApiUpdate;
export function init(_onUpdate: OnApiUpdate) {
@ -1135,8 +1147,8 @@ export async function fetchDiscussionMessage({
};
}
export async function searchMessagesLocal({
chat, isSavedDialog, savedTag, type, query, threadId, minDate, maxDate, ...pagination
export async function searchMessagesInChat({
chat, isSavedDialog, savedTag, type, query = '', threadId, minDate, maxDate, ...pagination
}: {
chat: ApiChat;
isSavedDialog?: boolean;
@ -1149,7 +1161,7 @@ export async function searchMessagesLocal({
limit: number;
minDate?: number;
maxDate?: number;
}) {
}): Promise<SearchResults | undefined> {
let filter;
switch (type) {
case 'media':
@ -1184,7 +1196,7 @@ export async function searchMessagesLocal({
savedReaction: savedTag && [buildInputReaction(savedTag)],
topMsgId: threadId !== MAIN_THREAD_ID && !isSavedDialog ? Number(threadId) : undefined,
filter,
q: query || '',
q: query,
minDate,
maxDate,
...pagination,
@ -1228,15 +1240,17 @@ export async function searchMessagesLocal({
}
export async function searchMessagesGlobal({
query, offsetRate = 0, limit, type = 'text', minDate, maxDate,
query, offsetRate = 0, offsetPeer, offsetId, limit, type = 'text', minDate, maxDate,
}: {
query: string;
offsetRate?: number;
offsetPeer?: ApiPeer;
offsetId?: number;
limit: number;
type?: ApiGlobalMessageSearchType;
minDate?: number;
maxDate?: number;
}) {
}): Promise<SearchResults | undefined> {
let filter;
switch (type) {
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({
q: query,
offsetRate,
offsetPeer: new GramJs.InputPeerEmpty(),
offsetPeer: peer,
offsetId,
broadcastsOnly: type === 'channels' || undefined,
limit,
filter,
@ -1284,11 +1301,7 @@ export async function searchMessagesGlobal({
return undefined;
}
updateLocalDb({
chats: result.chats,
users: result.users,
messages: result.messages,
} as GramJs.messages.Messages);
updateLocalDb(result);
const chats = result.chats.map((c) => buildApiChatFromPreview(c)).filter(Boolean);
const users = result.users.map(buildApiUser).filter(Boolean);
@ -1296,21 +1309,77 @@ export async function searchMessagesGlobal({
dispatchThreadInfoUpdates(result.messages);
let totalCount = messages.length;
let nextRate: number | undefined;
if (result instanceof GramJs.messages.MessagesSlice || result instanceof GramJs.messages.ChannelMessages) {
totalCount = result.count;
if (messages.length) {
nextRate = messages[messages.length - 1].id;
}
} 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,
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 localDb from '../localDb';
import { invokeRequest } from './client';
import { searchMessagesLocal } from './messages';
import { searchMessagesInChat } from './messages';
let onUpdate: OnApiUpdate;
@ -139,7 +139,7 @@ export async function fetchTopUsers() {
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);
return {
@ -295,7 +295,7 @@ export async function fetchProfilePhotos({
if (chat?.isRestricted) return undefined;
const result = await searchMessagesLocal({
const result = await searchMessagesInChat({
chat: chat!,
type: 'profilePhoto',
limit,

View File

@ -1271,3 +1271,5 @@
"MenuInstallApp" = "Install App";
"RemoveEffect" = "Remove effect";
"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 CustomEmojiSetsModal } from '../components/common/CustomEmojiSetsModal';
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 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 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 GifSearch } from '../components/right/GifSearch';
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 type {
ApiChat, ApiPeer, ApiPhoto, ApiUser,
ApiWebDocument,
ApiPeer, ApiPhoto, ApiWebDocument,
} from '../../api/types';
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
import type { CustomPeer, StoryViewerOrigin } from '../../types';
@ -22,6 +21,8 @@ import {
isAnonymousForwardsChat,
isChatWithRepliesBot,
isDeletedUser,
isPeerChat,
isPeerUser,
isUserId,
} from '../../global/helpers';
import buildClassName, { createClassNameBuilder } from '../../util/buildClassName';
@ -102,9 +103,8 @@ const Avatar: FC<OwnProps> = ({
const videoLoopCountRef = useRef(0);
const isCustomPeer = peer && 'isCustomPeer' in peer;
const realPeer = peer && !isCustomPeer ? peer : undefined;
const isPeerChat = realPeer && 'title' in realPeer;
const user = peer && !isPeerChat ? peer as ApiUser : undefined;
const chat = peer && isPeerChat ? peer as ApiChat : undefined;
const user = realPeer && isPeerUser(realPeer) ? realPeer : undefined;
const chat = realPeer && isPeerChat(realPeer) ? realPeer : undefined;
const isDeleted = user && isDeletedUser(user);
const isReplies = realPeer && isChatWithRepliesBot(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 type {
ApiChat, ApiPeer, ApiUser,
ApiPeer,
} from '../../api/types';
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
import type { CustomPeer } from '../../types';
import { EMOJI_STATUS_LOOP_LIMIT } from '../../config';
import {
getChatTitle, getUserFullName, isAnonymousForwardsChat, isChatWithRepliesBot, isUserId,
getChatTitle, getUserFullName, isAnonymousForwardsChat, isChatWithRepliesBot, isPeerUser,
} from '../../global/helpers';
import buildClassName from '../../util/buildClassName';
import { copyTextToClipboard } from '../../util/clipboard';
@ -62,9 +62,9 @@ const FullNameTitle: FC<OwnProps> = ({
const { showNotification } = getActions();
const realPeer = 'id' in peer ? peer : undefined;
const customPeer = 'isCustomPeer' in peer ? peer : undefined;
const isUser = realPeer && isUserId(realPeer.id);
const title = realPeer && (isUser ? getUserFullName(realPeer as ApiUser) : getChatTitle(lang, realPeer as ApiChat));
const isPremium = isUser && (peer as ApiUser).isPremium;
const isUser = realPeer && isPeerUser(realPeer);
const title = realPeer && (isUser ? getUserFullName(realPeer) : getChatTitle(lang, realPeer));
const isPremium = isUser && realPeer.isPremium;
const handleTitleClick = useLastCallback((e) => {
if (!title || !canCopyTitle) {

View File

@ -1,8 +1,8 @@
import type { FC } from '../../lib/teact/teact';
import React, { memo } from '../../lib/teact/teact';
import type { ApiMessage, ApiMessageOutgoingStatus } from '../../api/types';
import buildClassName from '../../util/buildClassName';
import { formatPastTimeShort } from '../../util/dates/dateFormat';
import useOldLang from '../../hooks/useOldLang';
@ -12,17 +12,20 @@ import MessageOutgoingStatus from './MessageOutgoingStatus';
import './LastMessageMeta.scss';
type OwnProps = {
className?: string;
message: ApiMessage;
outgoingStatus?: ApiMessageOutgoingStatus;
draftDate?: number;
};
const LastMessageMeta: FC<OwnProps> = ({ message, outgoingStatus, draftDate }) => {
const LastMessageMeta = ({
className, message, outgoingStatus, draftDate,
}: OwnProps) => {
const lang = useOldLang();
const shouldUseDraft = draftDate && draftDate > message.date;
return (
<div className="LastMessageMeta">
<div className={buildClassName('LastMessageMeta', className)}>
{outgoingStatus && !shouldUseDraft && (
<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 { ObserveFn } from '../../hooks/useIntersectionObserver';
import type { LangFn } from '../../hooks/useOldLang';
import { ApiMessageEntityTypes } from '../../api/types';
import {
@ -18,42 +17,43 @@ import {
import trimText from '../../util/trimText';
import renderText from './helpers/renderText';
import useOldLang from '../../hooks/useOldLang';
import MessageText from './MessageText';
interface OwnProps {
lang: LangFn;
message: ApiMessage;
translatedText?: ApiFormattedText;
noEmoji?: boolean;
highlight?: string;
truncateLength?: number;
observeIntersectionForLoading?: ObserveFn;
observeIntersectionForPlaying?: ObserveFn;
withTranslucentThumbs?: boolean;
inChatList?: boolean;
emojiSize?: number;
observeIntersectionForLoading?: ObserveFn;
observeIntersectionForPlaying?: ObserveFn;
}
function MessageSummary({
lang,
message,
translatedText,
noEmoji = false,
highlight,
truncateLength = TRUNCATED_SUMMARY_LENGTH,
observeIntersectionForLoading,
observeIntersectionForPlaying,
withTranslucentThumbs = false,
inChatList = false,
emojiSize,
observeIntersectionForLoading,
observeIntersectionForPlaying,
}: OwnProps) {
const lang = useOldLang();
const { text, entities } = extractMessageText(message, inChatList) || {};
const hasSpoilers = entities?.some((e) => e.type === ApiMessageEntityTypes.Spoiler);
const hasCustomEmoji = entities?.some((e) => e.type === ApiMessageEntityTypes.CustomEmoji);
const hasPoll = Boolean(getMessagePoll(message));
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);
return (

View File

@ -4,8 +4,8 @@
background: var(--color-chat-hover);
height: 2rem;
min-width: 2rem;
margin-left: 0.5rem;
margin-bottom: 0.5rem;
margin-left: 0.25rem;
margin-right: 0.25rem;
padding-right: 1rem;
border-radius: 1rem;
cursor: var(--custom-cursor, pointer);
@ -55,15 +55,6 @@
max-width: unset;
}
.SearchInput & {
flex: 1 0 auto;
position: relative;
top: 0.25rem;
left: -0.125rem;
color: var(--color-text-secondary);
}
.Avatar,
.item-icon {
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 { withGlobal } from '../../global';
@ -19,19 +19,21 @@ import Icon from './icons/Icon';
import './PickerSelectedItem.scss';
type OwnProps = {
type OwnProps<T = undefined> = {
// eslint-disable-next-line react/no-unused-prop-types
peerId?: string;
// eslint-disable-next-line react/no-unused-prop-types
forceShowSelf?: boolean;
customPeer?: CustomPeer;
icon?: IconName;
title?: string;
isMinimized?: boolean;
canClose?: boolean;
forceShowSelf?: boolean;
clickArg?: any;
className?: string;
fluid?: boolean;
withPeerColors?: boolean;
onClick: (arg: any) => void;
clickArg: T;
onClick: (arg: T) => void;
};
type StateProps = {
@ -40,7 +42,8 @@ type StateProps = {
isSavedMessages?: boolean;
};
const PickerSelectedItem: FC<OwnProps & StateProps> = ({
// eslint-disable-next-line @typescript-eslint/comma-dangle
const PickerSelectedItem = <T,>({
icon,
title,
isMinimized,
@ -54,7 +57,7 @@ const PickerSelectedItem: FC<OwnProps & StateProps> = ({
isSavedMessages,
withPeerColors,
onClick,
}) => {
}: OwnProps<T> & StateProps) => {
const lang = useOldLang();
let iconElement: TeactNode | undefined;
@ -82,7 +85,7 @@ const PickerSelectedItem: FC<OwnProps & StateProps> = ({
? getUserFirstOrLastName(user)
: getChatTitle(lang, chat, isSavedMessages));
titleText = name ? renderText(name) : undefined;
titleText = title || (name ? renderText(name) : undefined);
}
const fullClassName = buildClassName(
@ -133,4 +136,4 @@ export default memo(withGlobal<OwnProps>(
isSavedMessages,
};
},
)(PickerSelectedItem));
)(PickerSelectedItem)) as typeof PickerSelectedItem;

View File

@ -152,7 +152,6 @@ const EmbeddedMessage: FC<OwnProps> = ({
return (
<MessageSummary
lang={lang}
message={message}
noEmoji={Boolean(mediaThumbnail)}
translatedText={translatedText}

View File

@ -254,7 +254,6 @@ function renderMessageContent(
const messageSummary = (
<MessageSummary
lang={lang}
message={message}
truncateLength={MAX_LENGTH}
observeIntersectionForLoading={observeIntersectionForLoading}

View File

@ -12,7 +12,7 @@ import {
getMessageSummaryText,
TRUNCATED_SUMMARY_LENGTH,
} from '../../../global/helpers/messageSummary';
import { getMessageKey } from '../../../util/messageKey';
import { getMessageKey } from '../../../util/keys/messageKey';
import trimText from '../../../util/trimText';
import renderText from './renderText';
import { renderTextWithEntities } from './renderTextWithEntities';

View File

@ -656,8 +656,7 @@ function handleBotCommandClick(e: React.MouseEvent<HTMLAnchorElement>) {
}
function handleHashtagClick(e: React.MouseEvent<HTMLAnchorElement>) {
getActions().setLocalTextSearchQuery({ query: e.currentTarget.innerText });
getActions().searchTextMessagesLocal();
getActions().searchHashtag({ hashtag: e.currentTarget.innerText });
}
function handleCodeClick(e: React.MouseEvent<HTMLElement>) {

View File

@ -562,7 +562,7 @@ export default memo(withGlobal<OwnProps>(
const {
globalSearch: {
query,
date,
minDate,
},
shouldSkipHistoryAnimations,
activeChatFolder,
@ -589,7 +589,7 @@ export default memo(withGlobal<OwnProps>(
return {
searchQuery: query,
searchDate: date,
searchDate: minDate,
isFirstChatFolderActive: activeChatFolder === 0,
shouldSkipHistoryAnimations,
currentUserId,

View File

@ -141,4 +141,10 @@
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
icon="calendar"
title={selectedSearchDate}
fluid
canClose
isMinimized={Boolean(globalSearchChatId)}
className="search-date"
className="left-search-picker-item search-date"
onClick={setGlobalSearchDate}
clickArg={CLEAR_DATE_SEARCH_PARAM}
/>
)}
{globalSearchChatId && (
<PickerSelectedItem
className="left-search-picker-item"
peerId={globalSearchChatId}
onClick={setGlobalSearchChatId}
fluid
canClose
isMinimized
clickArg={CLEAR_CHAT_SEARCH_PARAM}
/>
)}
@ -269,7 +273,7 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
</DropdownMenu>
<SearchInput
inputId="telegram-search-input"
parentContainerClassName="LeftSearch"
resultsItemSelector=".LeftSearch .ListItem-button"
className={buildClassName(
(globalSearchChatId || searchDate) ? 'with-picker-item' : undefined,
shouldHideSearch && 'SearchInput--hidden',
@ -324,7 +328,7 @@ export default memo(withGlobal<OwnProps>(
(global): StateProps => {
const tabState = selectTabState(global);
const {
query: searchQuery, fetchingStatus, chatId, date,
query: searchQuery, fetchingStatus, chatId, minDate,
} = tabState.globalSearch;
const {
connectionState, isSyncing, isFetchingDifference,
@ -335,7 +339,7 @@ export default memo(withGlobal<OwnProps>(
searchQuery,
isLoading: fetchingStatus ? Boolean(fetchingStatus.chats || fetchingStatus.messages) : false,
globalSearchChatId: chatId,
searchDate: date,
searchDate: minDate,
theme: selectTheme(global),
connectionState,
isSyncing,

View File

@ -8,7 +8,6 @@ import type {
} from '../../../../api/types';
import type { ApiDraft } from '../../../../global/types';
import type { ObserveFn } from '../../../../hooks/useIntersectionObserver';
import type { LangFn } from '../../../../hooks/useOldLang';
import { ANIMATION_END_DELAY, CHAT_HEIGHT_PX } from '../../../../config';
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" />)}
{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>
);
}, [
@ -243,11 +242,10 @@ export default function useChatListEntry({
}
function renderSummary(
lang: LangFn, message: ApiMessage, observeIntersection?: ObserveFn, blobUrl?: string, isRoundVideo?: boolean,
message: ApiMessage, observeIntersection?: ObserveFn, blobUrl?: string, isRoundVideo?: boolean,
) {
const messageSummary = (
<MessageSummary
lang={lang}
message={message}
noEmoji={Boolean(blobUrl)}
observeIntersectionForLoading={observeIntersection}

View File

@ -10,6 +10,7 @@ import { SLIDE_TRANSITION_DURATION } from '../../../config';
import { getIsDownloading, getMessageDownloadableMedia } from '../../../global/helpers';
import buildClassName from '../../../util/buildClassName';
import { formatMonthAndYear, toYearMonth } from '../../../util/dates/dateFormat';
import { parseSearchResultKey } from '../../../util/keys/searchResultKey';
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
import { throttle } from '../../../util/schedulers';
import { createMapStateToProps } from './helpers/createMapStateToProps';
@ -66,9 +67,9 @@ const AudioResults: FC<OwnProps & StateProps> = ({
}
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);
}, [globalMessagesByChatId, foundIds]);

View File

@ -6,6 +6,7 @@ import type { ApiChat, ApiMessage } from '../../../api/types';
import { LoadMoreDirection } from '../../../types';
import { selectTabState } from '../../../global/selectors';
import { parseSearchResultKey, type SearchResultKey } from '../../../util/keys/searchResultKey';
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
import { throttle } from '../../../util/schedulers';
import { renderMessageSummary } from '../../common/helpers/renderMessageText';
@ -28,7 +29,7 @@ export type OwnProps = {
type StateProps = {
currentUserId?: string;
foundIds?: string[];
foundIds?: SearchResultKey[];
globalMessagesByChatId?: Record<string, { byId: Record<number, ApiMessage> }>;
chatsById: Record<string, ApiChat>;
fetchingStatus?: { chats?: boolean; messages?: boolean };
@ -85,9 +86,9 @@ const ChatMessageResults: FC<OwnProps & StateProps> = ({
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)
.sort((a, b) => b.date - a.date);

View File

@ -16,6 +16,7 @@ import {
import { selectSimilarChannelIds, selectTabState } from '../../../global/selectors';
import { getOrderedIds } from '../../../util/folderManager';
import { unique } from '../../../util/iteratees';
import { parseSearchResultKey, type SearchResultKey } from '../../../util/keys/searchResultKey';
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
import { throttle } from '../../../util/schedulers';
import { renderMessageSummary } from '../../common/helpers/renderMessageText';
@ -49,7 +50,7 @@ type StateProps = {
contactIds?: string[];
accountPeerIds?: string[];
globalPeerIds?: string[];
foundIds?: string[];
foundIds?: SearchResultKey[];
globalMessagesByChatId?: Record<string, { byId: Record<number, ApiMessage> }>;
fetchingStatus?: { chats?: boolean; messages?: boolean };
suggestedChannelIds?: string[];
@ -191,12 +192,12 @@ const ChatResults: FC<OwnProps & StateProps> = ({
return foundIds
.map((id) => {
const [chatId, messageId] = id.split('_');
const [chatId, messageId] = parseSearchResultKey(id);
const chat = chatsById[chatId];
if (!chat) return undefined;
if (isChannelList && !isChatChannel(chat)) return undefined;
return globalMessagesByChatId?.[chatId]?.byId[Number(messageId)];
return globalMessagesByChatId?.[chatId]?.byId[messageId];
})
.filter(Boolean);
}, [searchQuery, searchDate, foundIds, isChannelList, globalMessagesByChatId]);

View File

@ -4,7 +4,6 @@
flex-direction: row;
justify-content: space-between;
margin-left: 0.5rem;
margin-bottom: 0.5rem;
.date-item {
display: flex;

View File

@ -12,6 +12,7 @@ import { SLIDE_TRANSITION_DURATION } from '../../../config';
import { getIsDownloading, getMessageDocument } from '../../../global/helpers';
import buildClassName from '../../../util/buildClassName';
import { formatMonthAndYear, toYearMonth } from '../../../util/dates/dateFormat';
import { parseSearchResultKey } from '../../../util/keys/searchResultKey';
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
import { throttle } from '../../../util/schedulers';
import { createMapStateToProps } from './helpers/createMapStateToProps';
@ -77,8 +78,8 @@ const FileResults: FC<OwnProps & StateProps> = ({
}
return foundIds.map((id) => {
const [chatId, messageId] = id.split('_');
const message = globalMessagesByChatId[chatId]?.byId[Number(messageId)];
const [chatId, messageId] = parseSearchResultKey(id);
const message = globalMessagesByChatId[chatId]?.byId[messageId];
return message && getMessageDocument(message) ? message : undefined;
}).filter(Boolean) as ApiMessage[];

View File

@ -24,9 +24,9 @@
.section-heading {
position: relative;
padding-top: 1.25rem;
padding-top: 0.25rem;
padding-left: 1.25rem;
margin: 0 0 1rem -1.25rem !important;
margin: 0 0 0.5rem -1.25rem !important;
font-weight: 500;
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) {
.ListItem {
margin: 0 -0.125rem 0 -0.5rem;
@ -233,7 +222,7 @@
}
.chat-selection {
padding-top: 0.5rem;
padding-block: 0.5rem;
display: flex;
flex-shrink: 0;
flex-wrap: nowrap;
@ -246,18 +235,7 @@
overflow-y: hidden;
> .PickerSelectedItem {
flex: 0 0 auto;
&:last-child {
margin-right: auto;
}
}
&[dir="rtl"] {
> .PickerSelectedItem:last-child {
margin-left: auto;
margin-right: 0;
}
flex-shrink: 0;
}
}

View File

@ -11,6 +11,7 @@ import { LoadMoreDirection } from '../../../types';
import { SLIDE_TRANSITION_DURATION } from '../../../config';
import buildClassName from '../../../util/buildClassName';
import { formatMonthAndYear, toYearMonth } from '../../../util/dates/dateFormat';
import { parseSearchResultKey } from '../../../util/keys/searchResultKey';
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
import { throttle } from '../../../util/schedulers';
import { createMapStateToProps } from './helpers/createMapStateToProps';
@ -75,9 +76,9 @@ const LinkResults: FC<OwnProps & StateProps> = ({
}
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);
}, [globalMessagesByChatId, foundIds]);

View File

@ -9,6 +9,7 @@ import { LoadMoreDirection, MediaViewerOrigin } from '../../../types';
import { SLIDE_TRANSITION_DURATION } from '../../../config';
import buildClassName from '../../../util/buildClassName';
import { parseSearchResultKey } from '../../../util/keys/searchResultKey';
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
import { throttle } from '../../../util/schedulers';
import { createMapStateToProps } from './helpers/createMapStateToProps';
@ -71,9 +72,9 @@ const MediaResults: FC<OwnProps & StateProps> = ({
}
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);
}, [globalMessagesByChatId, foundIds]);

View File

@ -3,6 +3,7 @@ import type {
} from '../../../../api/types';
import type { GlobalState, TabState } from '../../../../global/types';
import type { ISettings } from '../../../../types';
import type { SearchResultKey } from '../../../../util/keys/searchResultKey';
import { selectChat, selectTabState, selectTheme } from '../../../../global/selectors';
@ -12,7 +13,7 @@ export type StateProps = {
chatsById: Record<string, ApiChat>;
usersById: Record<string, ApiUser>;
globalMessagesByChatId?: Record<string, { byId: Record<number, ApiMessage> }>;
foundIds?: string[];
foundIds?: SearchResultKey[];
searchChatId?: string;
activeDownloads: TabState['activeDownloads'];
isChatProtected?: boolean;

View File

@ -22,7 +22,7 @@ const HistoryCalendar: FC<OwnProps & StateProps> = ({
const { searchMessagesByDate, closeHistoryCalendar } = getActions();
const handleJumpToDate = useCallback((date: Date) => {
searchMessagesByDate({ timestamp: date.valueOf() / 1000 });
searchMessagesByDate({ timestamp: date.getTime() / 1000 });
closeHistoryCalendar();
}, [closeHistoryCalendar, searchMessagesByDate]);

View File

@ -10,6 +10,7 @@ import type {
import { type MediaViewerMedia, MediaViewerOrigin, type ThreadId } from '../../types';
import { ANIMATION_END_DELAY } from '../../config';
import { requestMutation } from '../../lib/fasterdom/fasterdom';
import {
getChatMediaMessageIds, getMessagePaidMedia, isChatAdmin, isUserId,
} from '../../global/helpers';
@ -180,7 +181,9 @@ const MediaViewer = ({
useEffect(() => {
if (isMobile) {
document.body.classList.toggle('is-media-viewer-open', isOpen);
requestMutation(() => {
document.body.classList.toggle('is-media-viewer-open', isOpen);
});
}
}, [isMobile, isOpen]);

View File

@ -5,7 +5,7 @@ import { getActions, withGlobal } from '../../global';
import type { MessageListType } from '../../global/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 buildClassName from '../../util/buildClassName';
@ -153,8 +153,10 @@ export default memo(withGlobal<OwnProps>(
const { chatId, threadId, type: messageListType } = currentMessageList;
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 {
messageListType,

View File

@ -123,7 +123,7 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
const {
joinChannel,
sendBotCommand,
openLocalTextSearch,
openMiddleSearch,
restartBot,
requestMasterAndRequestCall,
requestNextManagementScreen,
@ -196,12 +196,11 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
return;
}
openLocalTextSearch();
openMiddleSearch();
if (isMobile) {
// iOS requires synchronous focus on user event.
const searchInput = document.querySelector<HTMLInputElement>('#MobileSearch input')!;
searchInput.focus();
setFocusInSearchInput();
} else if (noAnimation) {
// The second RAF is necessary because Teact must update the state and render the async component
requestMeasure(() => {
@ -543,6 +542,6 @@ export default memo(withGlobal<OwnProps>(
)(HeaderActions));
function setFocusInSearchInput() {
const searchInput = document.querySelector<HTMLInputElement>('.RightHeader .SearchInput input');
const searchInput = document.querySelector<HTMLInputElement>('#MiddleSearch input');
searchInput?.focus();
}

View File

@ -184,7 +184,6 @@ const HeaderPinnedMessage: FC<OwnProps> = ({
<Transition activeKey={message.id} name="slideVerticalFade" className={styles.messageTextTransition}>
<p dir="auto" className={styles.summary}>
<MessageSummary
lang={lang}
message={message}
noEmoji={Boolean(mediaThumbnail)}
emojiSize={EMOJI_SIZE}

View File

@ -57,7 +57,7 @@ import {
import animateScroll, { isAnimatingScroll, restartCurrentScrollAnimation } from '../../util/animateScroll';
import buildClassName from '../../util/buildClassName';
import { orderBy } from '../../util/iteratees';
import { isLocalMessageId } from '../../util/messageKey';
import { isLocalMessageId } from '../../util/keys/messageKey';
import resetScroll from '../../util/resetScroll';
import { debounce, onTickEnd } from '../../util/schedulers';
import { groupMessages } from './helpers/groupMessages';

View File

@ -45,7 +45,7 @@ import {
selectChatFullInfo,
selectChatMessage,
selectCurrentMessageList,
selectCurrentTextSearch,
selectCurrentMiddleSearch,
selectDraft,
selectIsChatBotNotStarted,
selectIsInSelectMode,
@ -92,9 +92,9 @@ import FloatingActionButtons from './FloatingActionButtons';
import MessageList from './MessageList';
import MessageSelectToolbar from './MessageSelectToolbar.async';
import MiddleHeader from './MiddleHeader';
import MobileSearch from './MobileSearch.async';
import PremiumRequiredPlaceholder from './PremiumRequiredPlaceholder';
import ReactorListModal from './ReactorListModal.async';
import MiddleSearch from './search/MiddleSearch.async';
import './MiddleColumn.scss';
import styles from './MiddleColumn.module.scss';
@ -128,7 +128,7 @@ type StateProps = {
isRightColumnShown?: boolean;
isBackgroundBlurred?: boolean;
leftColumnWidth?: number;
hasCurrentTextSearch?: boolean;
hasActiveMiddleSearch?: boolean;
isSelectModeActive?: boolean;
isSeenByModalOpen: boolean;
isPrivacySettingsNoticeModalOpen: boolean;
@ -188,7 +188,7 @@ function MiddleColumn({
isRightColumnShown,
isBackgroundBlurred,
leftColumnWidth,
hasCurrentTextSearch,
hasActiveMiddleSearch,
isSelectModeActive,
isSeenByModalOpen,
isPrivacySettingsNoticeModalOpen,
@ -221,7 +221,6 @@ function MiddleColumn({
unpinAllMessages,
loadUser,
loadChatSettings,
closeLocalTextSearch,
exitMessageSelectMode,
joinChannel,
sendBotCommand,
@ -238,7 +237,8 @@ function MiddleColumn({
const lang = useOldLang();
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 [isUnpinModalOpen, setIsUnpinModalOpen] = useState(false);
@ -250,7 +250,6 @@ function MiddleColumn({
getForceNextPinnedInHeader,
} = usePinnedMessage(chatId, threadId, pinnedIds, topMessageId);
const isMobileSearchActive = isMobile && hasCurrentTextSearch;
const closeAnimationDuration = isMobile ? LAYER_ANIMATION_DURATION_MS : undefined;
const hasTools = hasPinned && (
windowWidth < MOBILE_SCREEN_MAX_WIDTH
@ -480,11 +479,6 @@ function MiddleColumn({
onBack: exitMessageSelectMode,
});
useHistoryBack({
isActive: isMobileSearchActive,
onBack: closeLocalTextSearch,
});
const isMessagingDisabled = Boolean(
!isPinnedMessageList && !isSavedDialog && !renderingCanPost && !renderingCanRestartBot && !renderingCanStartBot
&& !renderingCanSubscribe && composerRestrictionMessage,
@ -700,7 +694,7 @@ function MiddleColumn({
withExtraShift={withExtraShift}
/>
</div>
{isMobile && <MobileSearch isActive={Boolean(isMobileSearchActive)} />}
<MiddleSearch isActive={Boolean(hasActiveMiddleSearch)} />
</>
)}
{chatId && (
@ -749,7 +743,7 @@ export default memo(withGlobal<OwnProps>(
isLeftColumnShown,
isRightColumnShown: selectIsRightColumnShown(global, isMobile),
isBackgroundBlurred,
hasCurrentTextSearch: Boolean(selectCurrentTextSearch(global)),
hasActiveMiddleSearch: Boolean(selectCurrentMiddleSearch(global)),
isSelectModeActive: selectIsInSelectMode(global),
isSeenByModalOpen: Boolean(seenByModal),
isPrivacySettingsNoticeModalOpen: Boolean(privacySettingsNoticeModal),

View File

@ -35,6 +35,7 @@ import {
selectChat,
selectChatMessage,
selectChatMessages,
selectCurrentMiddleSearch,
selectForwardedSender,
selectIsChatBotNotStarted,
selectIsChatWithBot,
@ -50,7 +51,7 @@ import {
} from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
import cycleRestrict from '../../util/cycleRestrict';
import { getMessageKey } from '../../util/messageKey';
import { getMessageKey } from '../../util/keys/messageKey';
import useAppLayout from '../../hooks/useAppLayout';
import useConnectionStatus from '../../hooks/useConnectionStatus';
@ -58,8 +59,8 @@ import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
import useDerivedState from '../../hooks/useDerivedState';
import useElectronDrag from '../../hooks/useElectronDrag';
import useEnsureMessage from '../../hooks/useEnsureMessage';
import { useFastClick } from '../../hooks/useFastClick';
import useLastCallback from '../../hooks/useLastCallback';
import useLongPress from '../../hooks/useLongPress';
import useOldLang from '../../hooks/useOldLang';
import usePrevious from '../../hooks/usePrevious';
import useShowTransition from '../../hooks/useShowTransition';
@ -81,6 +82,7 @@ import './MiddleHeader.scss';
const ANIMATION_DURATION = 350;
const BACK_BUTTON_INACTIVE_TIME = 450;
const EMOJI_STATUS_SIZE = 22;
const SEARCH_LONGTAP_THRESHOLD = 500;
type OwnProps = {
chatId: string;
@ -116,6 +118,7 @@ type StateProps = {
isSynced?: boolean;
isFetchingDifference?: boolean;
emojiStatusSticker?: ApiSticker;
isMiddleSearchOpen?: boolean;
};
const MiddleHeader: FC<OwnProps & StateProps> = ({
@ -148,6 +151,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
getLoadingPinnedId,
emojiStatusSticker,
isSavedDialog,
isMiddleSearchOpen,
onFocusPinnedMessage,
}) => {
const {
@ -162,6 +166,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
openPremiumModal,
openThread,
openStickerSet,
updateMiddleSearch,
} = getActions();
const lang = useOldLang();
@ -197,15 +202,28 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
const componentRef = useRef<HTMLDivElement>(null);
const shouldAnimateTools = useRef<boolean>(true);
const {
handleClick: handleHeaderClick,
handleMouseDown: handleHeaderMouseDown,
} = useFastClick((e: React.MouseEvent<HTMLDivElement | HTMLButtonElement>) => {
if (e.type === 'mousedown' && (e.target as Element).closest('.title > .custom-emoji')) return;
const handleOpenSearch = useLastCallback(() => {
updateMiddleSearch({ chatId, threadId, update: {} });
});
const handleOpenChat = useLastCallback((event: React.MouseEvent | React.TouchEvent) => {
if ((event.target as Element).closest('.title > .custom-emoji')) return;
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) => {
pinMessage({ messageId, isUnpin: true });
});
@ -305,7 +323,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
const {
shouldRender: shouldRenderPinnedMessage,
transitionClassNames: pinnedMessageClassNames,
} = useShowTransition(Boolean(pinnedMessage), undefined, true);
} = useShowTransition(Boolean(pinnedMessage) && !isMiddleSearchOpen, undefined, true);
const renderingPinnedMessage = useCurrentOrPrev(pinnedMessage, true);
const renderingPinnedMessagesCount = useCurrentOrPrev(pinnedMessagesCount, true);
@ -389,8 +407,11 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
{(isLeftColumnHideable || currentTransitionKey > 0) && renderBackButton(shouldShowCloseButton, !isSavedDialog)}
<div
className="chat-info-wrapper"
onClick={handleHeaderClick}
onMouseDown={handleHeaderMouseDown}
onMouseDown={handleLongPressMouseDown}
onMouseUp={handleLongPressMouseUp}
onMouseLeave={handleLongPressMouseLeave}
onTouchStart={handleLongPressTouchStart}
onTouchEnd={handleLongPressTouchEnd}
>
{isUserId(realChatId) ? (
<PrivateChatInfo
@ -562,6 +583,7 @@ export default memo(withGlobal<OwnProps>(
const emojiStatusSticker = emojiStatus && global.customEmojis.byId[emojiStatus.documentId];
const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
const isMiddleSearchOpen = Boolean(selectCurrentMiddleSearch(global));
const state: StateProps = {
typingStatus,
@ -581,6 +603,7 @@ export default memo(withGlobal<OwnProps>(
emojiStatusSticker,
hasButtonInHeader: canStartBot || canRestartBot || canSubscribe || shouldSendJoinRequest,
isSavedDialog,
isMiddleSearchOpen,
};
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,
selectTheme,
} from '../../../global/selectors';
import { getMessageKey } from '../../../util/messageKey';
import { getMessageKey } from '../../../util/keys/messageKey';
import { AlbumRectPart } from './helpers/calculateAlbumLayout';
import withSelectControl from './hocs/withSelectControl';

View File

@ -72,7 +72,7 @@ import {
selectChatFullInfo,
selectChatMessage,
selectChatTranslations,
selectCurrentTextSearch,
selectCurrentMiddleSearch,
selectDefaultReaction,
selectForwardedSender,
selectIsChatProtected,
@ -105,7 +105,7 @@ import {
import { isAnimatingScroll } from '../../../util/animateScroll';
import buildClassName from '../../../util/buildClassName';
import { isElementInViewport } from '../../../util/isElementInViewport';
import { getMessageKey } from '../../../util/messageKey';
import { getMessageKey } from '../../../util/keys/messageKey';
import stopEvent from '../../../util/stopEvent';
import { IS_ANDROID, IS_ELECTRON, IS_TRANSLATION_SUPPORTED } from '../../../util/windowEnvironment';
import {
@ -1038,6 +1038,7 @@ const Message: FC<OwnProps & StateProps> = ({
return (
<Reactions
message={reactionMessage!}
threadId={threadId}
metaChildren={meta}
observeIntersection={observeIntersectionForPlaying}
noRecentReactors={isChannel}
@ -1641,6 +1642,7 @@ const Message: FC<OwnProps & StateProps> = ({
{reactionsPosition === 'outside' && !isStoryMention && (
<Reactions
message={reactionMessage!}
threadId={threadId}
isOutside
isCurrentUserPremium={isPremium}
maxWidth={reactionsMaxWidth}
@ -1738,7 +1740,9 @@ export default memo(withGlobal<OwnProps>(
quote: focusedQuote, scrollTargetPosition,
} = (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 animatedEmoji = singleEmoji && selectAnimatedEmoji(global, singleEmoji) ? singleEmoji : undefined;

View File

@ -173,6 +173,7 @@
line-height: 1;
height: calc(var(--message-meta-height, 1rem));
margin-left: auto;
margin-top: -0.5rem;
margin-right: -0.5rem;
align-self: flex-end;

View File

@ -9,9 +9,6 @@
--reaction-background: var(--color-reaction-chosen);
--reaction-background-hover: var(--hover-color-reaction-chosen);
--reaction-text-color: var(--text-color-reaction-chosen);
position: relative;
z-index: 1;
}
display: flex;
@ -27,7 +24,9 @@
text-transform: none;
color: var(--reaction-text-color);
overflow: visible;
position: relative;
line-height: 1.75rem;
z-index: 1;
gap: 0.125rem;

View File

@ -10,12 +10,13 @@ import type {
ApiSavedReactionTag,
} from '../../../../api/types';
import type { ObserveFn } from '../../../../hooks/useIntersectionObserver';
import type { ThreadId } from '../../../../types';
import type { Signal } from '../../../../util/signals';
import { getReactionKey, isReactionChosen } from '../../../../global/helpers';
import { selectPeer } from '../../../../global/selectors';
import buildClassName from '../../../../util/buildClassName';
import { getMessageKey } from '../../../../util/messageKey';
import { getMessageKey } from '../../../../util/keys/messageKey';
import useDerivedState from '../../../../hooks/useDerivedState';
import useLastCallback from '../../../../hooks/useLastCallback';
@ -28,6 +29,7 @@ import './Reactions.scss';
type OwnProps = {
message: ApiMessage;
threadId?: ThreadId;
isOutside?: boolean;
maxWidth?: number;
metaChildren?: React.ReactNode;
@ -42,6 +44,7 @@ const MAX_RECENT_AVATARS = 3;
const Reactions: FC<OwnProps> = ({
message,
threadId,
isOutside,
maxWidth,
metaChildren,
@ -53,8 +56,8 @@ const Reactions: FC<OwnProps> = ({
}) => {
const {
toggleReaction,
setLocalTextSearchTag,
searchTextMessagesLocal,
updateMiddleSearch,
performMiddleSearch,
openPremiumModal,
} = getActions();
const lang = useOldLang();
@ -112,8 +115,8 @@ const Reactions: FC<OwnProps> = ({
return;
}
setLocalTextSearchTag({ tag: reaction });
searchTextMessagesLocal();
updateMiddleSearch({ chatId: message.chatId, threadId, update: { savedTag: reaction } });
performMiddleSearch({ chatId: message.chatId, threadId });
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}
peerId={modal?.peerId}
forceShowSelf
clickArg={modal?.peerId}
onClick={handleOpenChat}
/>
<p className={styles.description}>

View File

@ -33,7 +33,6 @@ import Management from './management/Management.async';
import PollResults from './PollResults.async';
import Profile from './Profile';
import RightHeader from './RightHeader';
import RightSearch from './RightSearch.async';
import BoostStatistics from './statistics/BoostStatistics';
import MessageStatistics from './statistics/MessageStatistics.async';
import Statistics from './statistics/Statistics.async';
@ -87,7 +86,6 @@ const RightColumn: FC<OwnProps & StateProps> = ({
const {
toggleChatInfo,
toggleManagement,
closeLocalTextSearch,
setStickerSearchQuery,
setGifSearchQuery,
closePollResults,
@ -117,7 +115,6 @@ const RightColumn: FC<OwnProps & StateProps> = ({
const isOpen = contentKey !== undefined;
const isProfile = contentKey === RightColumnContent.ChatInfo;
const isSearch = contentKey === RightColumnContent.Search;
const isManagement = contentKey === RightColumnContent.Management;
const isStatistics = contentKey === RightColumnContent.Statistics;
const isMessageStatistics = contentKey === RightColumnContent.MessageStatistics;
@ -200,11 +197,6 @@ const RightColumn: FC<OwnProps & StateProps> = ({
case RightColumnContent.BoostStatistics:
closeBoostStatistics();
break;
case RightColumnContent.Search: {
blurSearchInput();
closeLocalTextSearch();
break;
}
case RightColumnContent.StickerSearch:
blurSearchInput();
setStickerSearchQuery({ query: undefined });
@ -318,16 +310,6 @@ const RightColumn: FC<OwnProps & StateProps> = ({
onProfileStateChange={setProfileState}
/>
);
case RightColumnContent.Search:
return (
<RightSearch
key={`right_search_${chatId!}`}
chatId={chatId!}
threadId={threadId!}
onClose={close}
isActive={isOpen && isActive}
/>
);
case RightColumnContent.Management:
return (
<Management
@ -380,7 +362,6 @@ const RightColumn: FC<OwnProps & StateProps> = ({
threadId={threadId}
isColumnOpen={isOpen}
isProfile={isProfile}
isSearch={isSearch}
isManagement={isManagement}
isStatistics={isStatistics}
isBoostStatistics={isBoostStatistics}

View File

@ -16,14 +16,11 @@ import {
selectChatFullInfo,
selectCurrentGifSearch,
selectCurrentStickerSearch,
selectCurrentTextSearch,
selectIsChatWithSelf,
selectTabState,
selectUser,
} from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
import { getDayStartAt } from '../../util/dates/dateFormat';
import { debounce } from '../../util/schedulers';
import useAppLayout from '../../hooks/useAppLayout';
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
@ -46,7 +43,6 @@ type OwnProps = {
threadId?: ThreadId;
isColumnOpen?: boolean;
isProfile?: boolean;
isSearch?: boolean;
isManagement?: boolean;
isStatistics?: boolean;
isBoostStatistics?: boolean;
@ -71,7 +67,6 @@ type StateProps = {
isChannel?: boolean;
userId?: string;
isSelf?: boolean;
messageSearchQuery?: string;
stickerSearchQuery?: string;
gifSearchQuery?: string;
isEditingInvite?: boolean;
@ -85,7 +80,6 @@ type StateProps = {
};
const COLUMN_ANIMATION_DURATION = 450 + ANIMATION_END_DELAY;
const runDebouncedForSearch = debounce((cb) => cb(), 200, false);
enum HeaderContent {
Profile,
@ -132,7 +126,6 @@ const RightHeader: FC<OwnProps & StateProps> = ({
threadId,
isColumnOpen,
isProfile,
isSearch,
isManagement,
isStatistics,
isMessageStatistics,
@ -151,7 +144,6 @@ const RightHeader: FC<OwnProps & StateProps> = ({
isSelf,
canManage,
isChannel,
messageSearchQuery,
stickerSearchQuery,
gifSearchQuery,
isEditingInvite,
@ -167,12 +159,9 @@ const RightHeader: FC<OwnProps & StateProps> = ({
canEditBot,
}) => {
const {
setLocalTextSearchQuery,
setStickerSearchQuery,
setGifSearchQuery,
searchTextMessagesLocal,
toggleManagement,
openHistoryCalendar,
openAddContactDialog,
toggleStatistics,
setEditingExportedInvite,
@ -196,14 +185,6 @@ const RightHeader: FC<OwnProps & StateProps> = ({
closeDeleteDialog();
});
const handleMessageSearchQueryChange = useLastCallback((query: string) => {
setLocalTextSearchQuery({ query });
if (query.length) {
runDebouncedForSearch(searchTextMessagesLocal);
}
});
const handleStickerSearchQueryChange = useLastCallback((query: string) => {
setStickerSearchQuery({ query });
});
@ -254,8 +235,6 @@ const RightHeader: FC<OwnProps & StateProps> = ({
) : profileState === ProfileState.SavedDialogs ? (
HeaderContent.SavedDialogs
) : -1 // Never reached
) : isSearch ? (
HeaderContent.Search
) : isPollResults ? (
HeaderContent.PollResults
) : isStickerSearch ? (
@ -350,26 +329,6 @@ const RightHeader: FC<OwnProps & StateProps> = ({
switch (renderingContentKey) {
case HeaderContent.PollResults:
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:
return <h3 className="title">{lang(isChannel ? 'ChannelAddSubscribers' : 'GroupAddMembers')}</h3>;
case HeaderContent.ManageInitial:
@ -610,7 +569,6 @@ export default withGlobal<OwnProps>(
chatId, isProfile, isManagement, threadId,
}): StateProps => {
const tabState = selectTabState(global);
const { query: messageSearchQuery } = selectCurrentTextSearch(global) || {};
const { query: stickerSearchQuery } = selectCurrentStickerSearch(global) || {};
const { query: gifSearchQuery } = selectCurrentGifSearch(global) || {};
const chat = chatId ? selectChat(global, chatId) : undefined;
@ -643,7 +601,6 @@ export default withGlobal<OwnProps>(
canEditTopic,
userId: user?.id,
isSelf: user?.isSelf,
messageSearchQuery,
stickerSearchQuery,
gifSearchQuery,
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,
onTouchStart: handleLongPressTouchStart,
onTouchEnd: handleLongPressTouchEnd,
} = useLongPress(handleLongPressStart, handleLongPressEnd);
} = useLongPress({
onStart: handleLongPressStart,
onEnd: handleLongPressEnd,
});
const isUnsupported = useUnsupportedMedia(
videoRef,

View File

@ -71,15 +71,6 @@
}
}
&.round {
width: 3.5rem;
border-radius: 50%;
.icon {
font-size: 1.5rem;
}
}
&.primary {
background-color: var(--color-primary);
color: var(--color-white);
@ -357,6 +348,15 @@
}
}
&.round {
width: 3.5rem;
border-radius: 50%;
.icon {
font-size: 1.5rem;
}
}
&.fluid {
padding-left: 1.75rem;
padding-right: 1.75rem;

View File

@ -100,7 +100,8 @@ const InfiniteScroll: FC<OwnProps> = ({
// Initial preload
useEffect(() => {
if (!loadMoreBackwards) {
const container = containerRef.current;
if (!loadMoreBackwards || !container) {
return;
}
@ -109,7 +110,7 @@ const InfiniteScroll: FC<OwnProps> = ({
return;
}
const { scrollHeight, clientHeight } = containerRef.current!;
const { scrollHeight, clientHeight } = container;
if (clientHeight && scrollHeight < clientHeight) {
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 {
margin: 0;

View File

@ -6,11 +6,15 @@
border: 2px solid var(--color-chat-hover);
border-radius: 1.375rem;
transition: border-color 0.15s ease;
display: flex;
align-items: center;
padding-inline-end: 0.1875rem;
&.with-picker-item {
display: flex;
.icon-search {
.icon-container-left {
display: none;
}
@ -44,34 +48,46 @@
background-color: transparent !important;
box-shadow: none !important;
padding:
calc(0.4375rem - var(--border-width)) calc(2.625rem - var(--border-width))
calc(0.5rem - var(--border-width)) calc(2.75rem - var(--border-width));
calc(0.4375rem - var(--border-width)) calc(0.625rem - var(--border-width))
calc(0.5rem - var(--border-width)) calc(0.75rem - var(--border-width));
&::placeholder {
color: var(--color-placeholders);
}
}
.icon-container {
position: absolute;
top: 0;
left: 0;
pointer-events: none;
.icon-container-left {
width: 1.5rem;
flex-shrink: 0;
margin-inline-start: 0.75rem;
}
.search-icon {
position: absolute;
top: 50%;
left: 0.75rem;
transform: translateY(-50%);
font-size: 1.375rem;
.icon-container-right {
width: 2.5rem;
flex-shrink: 0;
margin-inline-start: 0.5rem;
}
.icon-container-slide {
display: flex;
align-items: center;
justify-content: center;
}
.search-icon, .back-icon {
font-size: 1.5rem;
line-height: 1;
}
.back-icon {
color: var(--color-text-secondary);
}
.Loading {
position: absolute;
top: 0;
left: 0.1875rem;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
height: 2.5rem;
width: 2.5rem;
@ -81,9 +97,6 @@
}
.Button {
position: absolute;
top: 0.125rem;
right: 0.125rem;
font-size: 1rem;
}
@ -91,7 +104,7 @@
input {
height: 2.5rem;
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 {
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 { FC } from '../../lib/teact/teact';
import React, {
memo, useCallback, useEffect, useRef,
memo, useEffect, useRef,
} from '../../lib/teact/teact';
import buildClassName from '../../util/buildClassName';
import useFlag from '../../hooks/useFlag';
import useInputFocusOnOpen from '../../hooks/useInputFocusOnOpen';
import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback';
import useOldLang from '../../hooks/useOldLang';
import Icon from '../common/icons/Icon';
import Button from './Button';
import Loading from './Loading';
import Transition from './Transition';
@ -19,7 +22,7 @@ import './SearchInput.scss';
type OwnProps = {
ref?: RefObject<HTMLInputElement>;
children?: React.ReactNode;
parentContainerClassName?: string;
resultsItemSelector?: string;
className?: string;
inputId?: string;
value?: string;
@ -32,17 +35,25 @@ type OwnProps = {
autoComplete?: string;
canClose?: boolean;
autoFocusSearch?: boolean;
hasUpButton?: boolean;
hasDownButton?: boolean;
teactExperimentControlled?: boolean;
withBackIcon?: boolean;
onChange: (value: string) => void;
onStartBackspace?: NoneToVoidFunction;
onReset?: NoneToVoidFunction;
onFocus?: NoneToVoidFunction;
onBlur?: NoneToVoidFunction;
onClick?: NoneToVoidFunction;
onUpClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
onDownClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
onSpinnerClick?: NoneToVoidFunction;
};
const SearchInput: FC<OwnProps> = ({
ref,
children,
parentContainerClassName,
resultsItemSelector,
value,
inputId,
className,
@ -55,10 +66,18 @@ const SearchInput: FC<OwnProps> = ({
autoComplete,
canClose,
autoFocusSearch,
hasUpButton,
hasDownButton,
teactExperimentControlled,
withBackIcon,
onChange,
onStartBackspace,
onReset,
onFocus,
onBlur,
onClick,
onUpClick,
onDownClick,
onSpinnerClick,
}) => {
// 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
const lang = useOldLang();
const oldLang = useOldLang();
const lang = useLang();
function handleChange(event: React.ChangeEvent<HTMLInputElement>) {
const { currentTarget } = event;
onChange(currentTarget.value);
if (!isInputFocused) {
handleFocus();
}
}
function handleFocus() {
markInputFocused();
if (onFocus) {
onFocus();
}
onFocus?.();
}
function handleBlur() {
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') {
const element = document.querySelector(`.${parentContainerClassName} .ListItem-button`) as HTMLElement;
const element = document.querySelector(resultsItemSelector) as HTMLElement;
if (element) {
element.focus();
}
}
}, [parentContainerClassName]);
if (e.key === 'Backspace' && e.currentTarget.selectionStart === 0 && e.currentTarget.selectionEnd === 0) {
onStartBackspace?.();
}
});
return (
<div
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
ref={inputRef}
id={inputId}
type="text"
dir="auto"
placeholder={placeholder || lang('Search')}
placeholder={placeholder || oldLang('Search')}
className="form-control"
value={value}
disabled={disabled}
@ -133,29 +174,54 @@ const SearchInput: FC<OwnProps> = ({
onFocus={handleFocus}
onBlur={handleBlur}
onKeyDown={handleKeyDown}
teactExperimentControlled={teactExperimentControlled}
/>
<Transition
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 && (
{hasUpButton && (
<Button
round
size="tiny"
color="translucent"
onClick={onReset}
onClick={onUpClick}
disabled={!onUpClick}
ariaLabel={lang('AriaSearchOlderResult')}
>
<span className="icon icon-close" />
<Icon name="up" />
</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>
);
};

View File

@ -3,7 +3,7 @@ import './api/chats';
import './api/messages';
import './api/symbols';
import './api/globalSearch';
import './api/localSearch';
import './api/middleSearch';
import './api/management';
import './api/sync';
import './api/accounts';
@ -19,7 +19,7 @@ import './ui/initial';
import './ui/chats';
import './ui/messages';
import './ui/globalSearch';
import './ui/localSearch';
import './ui/middleSearch';
import './ui/stickerSearch';
import './ui/users';
import './ui/settings';

View File

@ -38,7 +38,7 @@ import { isDeepLink } from '../../../util/deepLinkParser';
import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { getOrderedIds } from '../../../util/folderManager';
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 { debounce, pause, throttle } from '../../../util/schedulers';
import { extractCurrentThemeParams } from '../../../util/themeStyle';
@ -69,6 +69,7 @@ import {
removeChatFromChatLists,
replaceChatFullInfo,
replaceChatListIds,
replaceChatListLoadingParameters,
replaceChats,
replaceThreadParam,
replaceUsers,
@ -98,8 +99,8 @@ import {
selectChatByUsername,
selectChatFolder,
selectChatFullInfo,
selectChatLastMessage,
selectChatLastMessageId,
selectChatListLoadingParameters,
selectChatListType,
selectChatMessages,
selectCurrentChat,
@ -108,6 +109,7 @@ import {
selectIsChatPinned,
selectIsChatWithSelf,
selectLastServiceNotification,
selectPeer,
selectSimilarChannelIds,
selectStickerSet,
selectSupportChat,
@ -512,10 +514,6 @@ addActionHandler('loadAllChats', async (global, actions, payload): Promise<void>
let { shouldReplace } = payload;
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]) {
if (i++ >= INFINITE_LOOP_MARKER) {
if (DEBUG) {
@ -532,24 +530,8 @@ addActionHandler('loadAllChats', async (global, actions, payload): Promise<void>
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(
listType,
oldestChat?.id,
oldestChat ? getOrderDate(oldestChat) : undefined,
shouldReplace,
true,
);
@ -2709,21 +2691,30 @@ addActionHandler('requestCollectibleInfo', async (global, actions, payload): Pro
async function loadChats(
listType: ChatListType,
offsetId?: string,
offsetDate?: number,
shouldReplace = false,
isFullDraftSync?: boolean,
) {
// eslint-disable-next-line eslint-multitab-tt/no-immediate-global
let global = getGlobal();
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', {
limit: CHAT_LIST_LOAD_SLICE,
offsetDate,
offsetId,
offsetPeer,
withPinned: shouldReplace,
}) : await callApi('fetchChats', {
limit: CHAT_LIST_LOAD_SLICE,
offsetDate,
offsetId,
offsetPeer,
archived: listType === 'archived',
withPinned: shouldReplace,
lastLocalServiceMessageId,
@ -2735,10 +2726,6 @@ async function loadChats(
const { chatIds } = result;
if (chatIds.length > 0 && chatIds[0] === offsetId) {
chatIds.shift();
}
global = getGlobal();
lastLocalServiceMessageId = selectLastServiceNotification(global)?.id;
@ -2806,6 +2793,10 @@ async function loadChats(
global = addMessages(global, result.messages);
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);
idsToUpdateDraft.forEach((chatId) => {
const draft = result.draftsById[chatId];

View File

@ -72,7 +72,8 @@ addActionHandler('setGlobalSearchDate', (global, actions, payload): ActionReturn
const maxDate = date ? timestampPlusDay(date) : date;
global = updateGlobalSearch(global, {
date,
minDate: date,
maxDate,
query: '',
resultsByType: {
...selectTabState(global, tabId).globalSearch.resultsByType,
@ -85,29 +86,46 @@ addActionHandler('setGlobalSearchDate', (global, actions, payload): ActionReturn
}, tabId);
setGlobal(global);
const { chatId } = selectTabState(global, tabId).globalSearch;
const chat = chatId ? selectChat(global, chatId) : undefined;
searchMessagesGlobal(global, '', 'text', undefined, chat, maxDate, date, tabId);
actions.searchMessagesGlobal({ type: 'text', tabId });
});
addActionHandler('searchMessagesGlobal', (global, actions, payload): ActionReturnType => {
const { type, tabId = getCurrentTabId() } = payload;
const {
query, resultsByType, chatId, date,
query, resultsByType, chatId,
} = selectTabState(global, tabId).globalSearch;
const maxDate = date ? timestampPlusDay(date) : date;
const nextOffsetId = (resultsByType?.[type as ApiGlobalMessageSearchType])?.nextOffsetId;
const offsetId = (resultsByType?.[type])?.nextOffsetId;
const offsetRate = (resultsByType?.[type])?.nextOffsetRate;
const offsetPeerId = (resultsByType?.[type])?.nextOffsetPeerId;
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>(
global: T,
query = '', type: ApiGlobalMessageSearchType, offsetRate?: number, chat?: ApiChat, maxDate?: number, minDate?: number,
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
async function searchMessagesGlobal<T extends GlobalState>(global: T, params: {
query?: string;
type: ApiGlobalMessageSearchType;
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: {
messages: ApiMessage[];
users: ApiUser[];
@ -115,18 +133,20 @@ async function searchMessagesGlobal<T extends GlobalState>(
topics?: ApiTopic[];
totalTopicsCount?: number;
totalCount: number;
nextRate: number | undefined;
nextOffsetRate?: number;
nextOffsetId?: number;
nextOffsetPeerId?: string;
} | undefined;
let messageLink: ApiMessage | undefined;
if (chat) {
const localResultRequest = callApi('searchMessagesLocal', {
const inChatResultRequest = callApi('searchMessagesInChat', {
chat,
query,
type,
limit: GLOBAL_SEARCH_SLICE,
offsetId: offsetRate,
offsetId,
minDate,
maxDate,
});
@ -136,12 +156,12 @@ async function searchMessagesGlobal<T extends GlobalState>(
limit: GLOBAL_TOPIC_SEARCH_SLICE,
}) : undefined;
const [localResult, topics] = await Promise.all([localResultRequest, topicsRequest]);
const [inChatResult, topics] = await Promise.all([inChatResultRequest, topicsRequest]);
if (localResult) {
if (inChatResult) {
const {
messages, users, totalCount, nextOffsetId,
} = localResult;
} = inChatResult;
const { topics: localTopics, count } = topics || {};
@ -152,13 +172,15 @@ async function searchMessagesGlobal<T extends GlobalState>(
users,
chats: [],
totalCount,
nextRate: nextOffsetId,
nextOffsetId,
};
}
} else {
result = await callApi('searchMessagesGlobal', {
query,
offsetRate,
offsetId,
offsetPeer,
limit: GLOBAL_SEARCH_SLICE,
type,
maxDate,
@ -187,7 +209,7 @@ async function searchMessagesGlobal<T extends GlobalState>(
}
const {
messages, users, chats, totalCount, nextRate,
messages, users, chats, totalCount, nextOffsetRate, nextOffsetId, nextOffsetPeerId,
} = result;
if (chats.length) {
@ -207,7 +229,9 @@ async function searchMessagesGlobal<T extends GlobalState>(
messages,
totalCount,
type,
nextRate,
nextOffsetRate,
nextOffsetId,
nextOffsetPeerId,
tabId,
);

View File

@ -16,7 +16,7 @@ import type {
ApiUser,
ApiVideo,
} from '../../../api/types';
import type { MessageKey } from '../../../util/messageKey';
import type { MessageKey } from '../../../util/keys/messageKey';
import type { RequiredGlobalActions } from '../../index';
import type {
ActionReturnType, ApiDraft, GlobalState, TabArgs,
@ -46,7 +46,7 @@ import {
split,
unique,
} from '../../../util/iteratees';
import { getMessageKey, isLocalMessageId } from '../../../util/messageKey';
import { getMessageKey, isLocalMessageId } from '../../../util/keys/messageKey';
import { oldTranslate } from '../../../util/oldLangProvider';
import { debounce, onTickEnd, rafPromise } from '../../../util/schedulers';
import { IS_IOS } from '../../../util/windowEnvironment';

View File

@ -1,8 +1,8 @@
import type { ApiChat } from '../../../api/types';
import type {
ChatMediaSearchParams, ChatMediaSearchSegment, LoadingState, SharedMediaType, ThreadId,
} from '../../../types';
import type { ActionReturnType, GlobalState, TabArgs } from '../../types';
import { type ApiChat, MAIN_THREAD_ID } from '../../../api/types';
import { LoadMoreDirection } from '../../../types';
import {
@ -10,6 +10,7 @@ import {
} from '../../../config';
import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { buildCollectionByKey, isInsideSortedArrayRange } from '../../../util/iteratees';
import { getSearchResultKey } from '../../../util/keys/searchResultKey';
import { callApi } from '../../../api/gramjs';
import { getChatMediaMessageIds, getIsSavedDialog, isSameReaction } from '../../helpers';
import {
@ -18,27 +19,30 @@ import {
import {
addChatMessagesById,
addChats,
addMessages,
addUsers,
initializeChatMediaSearchResults,
mergeWithChatMediaSearchSegment,
setChatMediaSearchLoading,
updateChatMediaSearchResults,
updateLocalTextSearchResults,
updateMiddleSearch,
updateMiddleSearchResults,
updateSharedMediaSearchResults,
} from '../../reducers';
import {
selectChat,
selectCurrentChatMediaSearch,
selectCurrentMessageList,
selectCurrentMiddleSearch,
selectCurrentSharedMediaSearch,
selectCurrentTextSearch,
} from '../../selectors';
const MEDIA_PRELOAD_OFFSET = 9;
addActionHandler('searchTextMessagesLocal', async (global, actions, payload): Promise<void> => {
const { tabId = getCurrentTabId() } = payload || {};
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
addActionHandler('performMiddleSearch', async (global, actions, payload): Promise<void> => {
const {
query, chatId, threadId = MAIN_THREAD_ID, tabId = getCurrentTabId(),
} = payload || {};
if (!chatId) return;
@ -47,45 +51,91 @@ addActionHandler('searchTextMessagesLocal', async (global, actions, payload): Pr
const realChatId = isSavedDialog ? String(threadId) : chatId;
const chat = realChatId ? selectChat(global, realChatId) : undefined;
let currentSearch = selectCurrentTextSearch(global, tabId);
if (!chat || !threadId || !currentSearch) {
let currentSearch = selectCurrentMiddleSearch(global, tabId);
if (!chat) {
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 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;
}
const result = await callApi('searchMessagesLocal', {
chat,
type: 'text',
query,
threadId,
limit: MESSAGE_SEARCH_SLICE,
offsetId,
isSavedDialog,
savedTag,
});
global = updateMiddleSearch(global, realChatId, threadId, {
fetchingQuery: query,
}, tabId);
setGlobal(global);
let result;
if (type === 'chat') {
result = await callApi('searchMessagesInChat', {
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) {
return;
}
const {
chats, users, messages, totalCount, nextOffsetId,
chats, users, messages, totalCount, nextOffsetId, nextOffsetRate, nextOffsetPeerId,
} = result;
const byId = buildCollectionByKey(messages, 'id');
const newFoundIds = Object.keys(byId).map(Number);
const newFoundIds = messages.map(getSearchResultKey);
global = getGlobal();
currentSearch = selectCurrentTextSearch(global, tabId);
const hasTagChanged = !isSameReaction(savedTag, currentSearch?.savedTag);
if (!currentSearch || query !== currentSearch.query || hasTagChanged) {
currentSearch = selectCurrentMiddleSearch(global, tabId);
const hasTagChanged = currentSearch?.savedTag && !isSameReaction(savedTag, currentSearch.savedTag);
const hasSearchChanged = currentSearch?.fetchingQuery && currentSearch.fetchingQuery !== query;
if (!currentSearch || hasSearchChanged || hasTagChanged) {
return;
}
@ -93,11 +143,42 @@ addActionHandler('searchTextMessagesLocal', async (global, actions, payload): Pr
global = addChats(global, buildCollectionByKey(chats, 'id'));
global = addUsers(global, buildCollectionByKey(users, 'id'));
global = addChatMessagesById(global, resultChatId, byId);
global = updateLocalTextSearchResults(global, resultChatId, threadId, newFoundIds, totalCount, nextOffsetId, tabId);
global = addMessages(global, messages);
global = updateMiddleSearch(global, resultChatId, threadId, {
fetchingQuery: undefined,
}, tabId);
global = updateMiddleSearchResults(global, resultChatId, threadId, {
foundIds: newFoundIds,
totalCount,
nextOffsetId,
nextOffsetRate,
nextOffsetPeerId,
query: query || '',
}, tabId);
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 => {
const { tabId = getCurrentTabId() } = payload || {};
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
@ -203,7 +284,7 @@ async function searchSharedMedia<T extends GlobalState>(
) {
const resultChatId = isSavedDialog ? global.currentUserId! : chat.id;
const result = await callApi('searchMessagesLocal', {
const result = await callApi('searchMessagesInChat', {
chat,
type,
limit: SHARED_MEDIA_SLICE * 2,
@ -367,7 +448,7 @@ async function searchChatMedia<T extends GlobalState>(
global = setChatMediaSearchLoading(global, resultChatId, threadId, true, tabId);
setGlobal(global);
const result = await callApi('searchMessagesLocal', {
const result = await callApi('searchMessagesInChat', {
chat,
type: 'media',
limit,

View File

@ -7,8 +7,8 @@ import { getCurrentTabId } from '../../../util/establishMultitabRole';
import {
buildCollectionByCallback, buildCollectionByKey, omit, unique,
} from '../../../util/iteratees';
import { getMessageKey } from '../../../util/keys/messageKey';
import * as mediaLoader from '../../../util/mediaLoader';
import { getMessageKey } from '../../../util/messageKey';
import requestActionTimeout from '../../../util/requestActionTimeout';
import { callApi } from '../../../api/gramjs';
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 { buildCollectionByKey, omit } from '../../../util/iteratees';
import { isLocalMessageId } from '../../../util/messageKey';
import { isLocalMessageId } from '../../../util/keys/messageKey';
import { closeMessageNotifications, notifyAboutMessage } from '../../../util/notifications';
import { buildLocalMessage } from '../../../api/gramjs/apiBuilders/messages';
import { checkIfHasUnreadReactions, isChatChannel } from '../../helpers';

View File

@ -15,7 +15,7 @@ import { getCurrentTabId } from '../../../util/establishMultitabRole';
import {
buildCollectionByKey, omit, pickTruthy, unique,
} from '../../../util/iteratees';
import { getMessageKey, isLocalMessageId } from '../../../util/messageKey';
import { getMessageKey, isLocalMessageId } from '../../../util/keys/messageKey';
import { notifyAboutMessage } from '../../../util/notifications';
import { onTickEnd } from '../../../util/schedulers';
import {

View File

@ -6,13 +6,13 @@ import { createMessageHashUrl } from '../../../util/routing';
import { IS_ELECTRON } from '../../../util/windowEnvironment';
import { addActionHandler, setGlobal } from '../../index';
import {
closeMiddleSearch,
exitMessageSelectMode, replaceTabThreadParam, updateCurrentMessageList, updateRequestedChatTranslation,
} from '../../reducers';
import { updateTabState } from '../../reducers/tabs';
import {
selectChat, selectCurrentMessageList, selectTabState,
} from '../../selectors';
import { closeLocalTextSearch } from './localSearch';
addActionHandler('processOpenChatOrThread', (global, actions, payload): ActionReturnType => {
const {
@ -50,10 +50,11 @@ addActionHandler('processOpenChatOrThread', (global, actions, payload): ActionRe
activeReactions: {},
shouldPreventComposerAnimation: true,
}, tabId);
global = closeMiddleSearch(global, chatId, threadId, tabId);
}
global = exitMessageSelectMode(global, tabId);
global = closeLocalTextSearch(global, tabId);
global = updateTabState(global, {
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) {
cached.quickReplies = initialState.quickReplies;
}
if (!cached.chats.loadingParameters) {
cached.chats.loadingParameters = initialState.chats.loadingParameters;
}
}
function updateCache(force?: boolean) {

View File

@ -27,6 +27,14 @@ export function isUserId(entityId: string) {
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) {
return entityId.length === CHANNEL_ID_LENGTH && entityId.startsWith('-1');
}
@ -365,14 +373,12 @@ export function getMessageSenderName(lang: LangFn, chatId: string, sender?: ApiP
return undefined;
}
if (!isUserId(sender.id)) {
if (isPeerChat(sender)) {
if (chatId === sender.id) return undefined;
return (sender as ApiChat).title;
return sender.title;
}
sender = sender as ApiUser;
if (sender.isSelf) {
return lang('FromYou');
}

View File

@ -2,7 +2,7 @@ export * from './users';
export * from './chats';
export * from './messages';
export * from './messageMedia';
export * from './localSearch';
export * from './middleSearch';
export * from './reactions';
export * from './bots';
export * from './media';

View File

@ -1,11 +1,9 @@
import type {
ApiAttachment,
ApiChat,
ApiMessage,
ApiMessageEntityTextUrl,
ApiPeer,
ApiStory,
ApiUser,
} from '../../api/types';
import type { MediaContent } from '../../api/types/messages';
import type { LangFn } from '../../hooks/useOldLang';
@ -24,10 +22,15 @@ import {
VIDEO_STICKER_MIME_TYPE,
} from '../../config';
import { areSortedArraysIntersecting, unique } from '../../util/iteratees';
import { isLocalMessageId } from '../../util/messageKey';
import { isLocalMessageId } from '../../util/keys/messageKey';
import { getServerTime } from '../../util/serverTime';
import { getGlobal } from '../index';
import { getChatTitle, getCleanPeerId, isUserId } from './chats';
import {
getChatTitle,
getCleanPeerId,
isPeerUser,
isUserId,
} from './chats';
import { getMainUsername, getUserFullName } from './users';
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) {
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) {

View File

@ -6,12 +6,12 @@ import { isCacheApiSupported } from '../util/cacheApi';
import { getCurrentTabId, reestablishMasterToSelf } from '../util/establishMultitabRole';
import { initGlobal } from '../util/init';
import { cloneDeep } from '../util/iteratees';
import { isLocalMessageId } from '../util/messageKey';
import { isLocalMessageId } from '../util/keys/messageKey';
import { Bundles, loadBundle } from '../util/moduleLoader';
import { parseLocationHash } from '../util/routing';
import { updatePeerColors } from '../util/theme';
import { IS_MULTITAB_SUPPORTED } from '../util/windowEnvironment';
import { initializeChatMediaSearchResults } from './reducers/localSearch';
import { initializeChatMediaSearchResults } from './reducers/middleSearch';
import { updateTabState } from './reducers/tabs';
import { initCache } from './cache';
import {

View File

@ -108,6 +108,11 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
byId: {},
fullInfoById: {},
similarChannelsById: {},
loadingParameters: {
active: {},
archived: {},
saved: {},
},
},
messages: {
@ -317,7 +322,7 @@ export const INITIAL_TAB_STATE: TabState = {
userSearch: {},
localTextSearch: {
middleSearch: {
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>(
global: T, chatId: string, lastMessageId: number, listType?: ChatListType,
): T {

View File

@ -4,11 +4,10 @@ import type { GlobalState, TabArgs, TabState } from '../types';
import { getCurrentTabId } from '../../util/establishMultitabRole';
import { areSortedArraysEqual } from '../../util/iteratees';
import { getSearchResultKey } from '../../util/keys/searchResultKey';
import { selectTabState } from '../selectors';
import { updateTabState } from './tabs';
const getComplexKey = (message: ApiMessage) => `${message.chatId}_${message.id}`;
export function updateGlobalSearch<T extends GlobalState>(
global: T,
searchStatePartial: Partial<TabState['globalSearch']>,
@ -35,12 +34,14 @@ export function updateGlobalSearchResults<T extends GlobalState>(
newFoundMessages: ApiMessage[],
totalCount: number,
type: ApiGlobalMessageSearchType,
nextRate?: number,
nextOffsetRate?: number,
nextOffsetId?: number,
nextOffsetPeerId?: string,
...[tabId = getCurrentTabId()]: TabArgs<T>
): T {
const { resultsByType } = selectTabState(global, tabId).globalSearch || {};
const newFoundMessagesById = newFoundMessages.reduce((result, message) => {
result[getComplexKey(message)] = message;
result[getSearchResultKey(message)] = message;
return result;
}, {} as Record<string, ApiMessage>);
@ -48,7 +49,7 @@ export function updateGlobalSearchResults<T extends GlobalState>(
if (foundIdsForType !== undefined
&& Object.keys(newFoundMessagesById).every(
(newId) => foundIdsForType.includes(getComplexKey(newFoundMessagesById[newId])),
(newId) => foundIdsForType.includes(getSearchResultKey(newFoundMessagesById[newId])),
)
) {
return updateGlobalSearchFetchingStatus(global, { messages: false }, tabId);
@ -56,7 +57,7 @@ export function updateGlobalSearchResults<T extends GlobalState>(
const prevFoundIds = foundIdsForType || [];
const newFoundIds = newFoundMessages
.map((message) => getComplexKey(message))
.map((message) => getSearchResultKey(message))
.filter((id) => !prevFoundIds.includes(id));
const foundIds = Array.prototype.concat(prevFoundIds, newFoundIds);
const foundOrPrevFoundIds = areSortedArraysEqual(prevFoundIds, foundIds) ? prevFoundIds : foundIds;
@ -68,7 +69,9 @@ export function updateGlobalSearchResults<T extends GlobalState>(
...(selectTabState(global, tabId).globalSearch || {}).resultsByType,
[type]: {
totalCount,
nextOffsetId: nextRate,
nextOffsetId,
nextOffsetRate,
nextOffsetPeerId,
foundIds: foundOrPrevFoundIds,
},
},

View File

@ -3,7 +3,7 @@ export * from './messages';
export * from './symbols';
export * from './users';
export * from './globalSearch';
export * from './localSearch';
export * from './middleSearch';
export * from './management';
export * from './settings';
export * from './twoFaSettings';

View File

@ -14,7 +14,7 @@ import { getCurrentTabId } from '../../util/establishMultitabRole';
import {
areSortedArraysEqual, excludeSortedArray, omit, pick, pickTruthy, unique,
} from '../../util/iteratees';
import { isLocalMessageId, type MessageKey } from '../../util/messageKey';
import { isLocalMessageId, type MessageKey } from '../../util/keys/messageKey';
import {
hasMessageTtl, isMediaLoadableInViewer,
mergeIdRanges, orderHistoryIds, orderPinnedIds,
@ -38,7 +38,7 @@ import {
selectThreadInfo,
selectViewportIds,
} from '../selectors';
import { removeIdFromSearchResults } from './localSearch';
import { removeIdFromSearchResults } from './middleSearch';
import { updateTabState } from './tabs';
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 {
ChatMediaSearchParams, ChatMediaSearchSegment, LoadingState,
SharedMediaType, ThreadId,
ChatMediaSearchParams,
ChatMediaSearchSegment,
LoadingState,
MiddleSearchParams,
MiddleSearchResults,
SharedMediaType,
ThreadId,
} from '../../types';
import type { GlobalState, TabArgs } from '../types';
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 { selectTabState } from '../selectors';
import { selectChatMediaSearch } from '../selectors/localSearch';
import { selectChatMediaSearch } from '../selectors/middleSearch';
import { updateTabState } from './tabs';
interface TextSearchParams {
query?: string;
savedTag?: ApiReaction;
results?: {
totalCount?: number;
nextOffsetId?: number;
foundIds?: number[];
};
}
interface SharedMediaSearchParams {
currentType?: SharedMediaType;
resultsByType?: Partial<Record<SharedMediaType, {
@ -31,93 +28,128 @@ interface SharedMediaSearchParams {
}>>;
}
function replaceLocalTextSearch<T extends GlobalState>(
function replaceMiddleSearch<T extends GlobalState>(
global: T,
chatThreadKey: string,
searchParams: TextSearchParams,
searchParams?: MiddleSearchParams,
...[tabId = getCurrentTabId()]: TabArgs<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, {
localTextSearch: {
middleSearch: {
byChatThreadKey: {
...selectTabState(global, tabId).localTextSearch.byChatThreadKey,
[chatThreadKey]: searchParams,
...selectTabState(global, tabId).middleSearch.byChatThreadKey,
[chatThreadKey]: {
type,
...rest,
},
},
},
}, tabId);
}
export function updateLocalTextSearch<T extends GlobalState>(
export function updateMiddleSearch<T extends GlobalState>(
global: T,
chatId: string,
threadId: ThreadId,
query?: string,
update: Partial<MiddleSearchParams>,
...[tabId = getCurrentTabId()]: TabArgs<T>
): T {
const chatThreadKey = buildChatThreadKey(chatId, threadId);
const currentSearch = selectTabState(global, tabId).middleSearch.byChatThreadKey[chatThreadKey];
return replaceLocalTextSearch(global, chatThreadKey, {
...selectTabState(global, tabId).localTextSearch.byChatThreadKey[chatThreadKey],
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, {
const updated = {
type: 'chat',
...currentSearch,
query,
savedTag: tag,
}, tabId);
...update,
} satisfies MiddleSearchParams;
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,
chatId: string,
threadId: ThreadId,
foundIds?: number[],
totalCount?: number,
nextOffsetId?: number,
...[tabId = getCurrentTabId()]: TabArgs<T>
): T {
const chatThreadKey = buildChatThreadKey(chatId, threadId);
return replaceLocalTextSearch(global, chatThreadKey, {
...selectTabState(global, tabId).localTextSearch.byChatThreadKey[chatThreadKey],
results: {
foundIds,
totalCount,
nextOffsetId,
},
return replaceMiddleSearch(global, buildChatThreadKey(chatId, threadId), {
type: 'chat',
}, tabId);
}
export function updateLocalTextSearchResults<T extends GlobalState>(
function replaceMiddleSearchResults<T extends GlobalState>(
global: T,
chatId: string,
threadId: ThreadId,
newFoundIds: number[],
totalCount?: number,
nextOffsetId?: number,
results: MiddleSearchResults,
...[tabId = getCurrentTabId()]: TabArgs<T>
): 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>
): T {
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 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;
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>(

View File

@ -41,6 +41,12 @@ export function selectPeerFullInfo<T extends GlobalState>(global: T, peerId: str
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) {
const userId = getPrivateChatUserId(chat);
if (!userId) {

View File

@ -3,7 +3,7 @@ export * from './users';
export * from './chats';
export * from './messages';
export * from './globalSearch';
export * from './localSearch';
export * from './middleSearch';
export * from './management';
export * from './symbols';
export * from './payments';

View File

@ -22,8 +22,8 @@ import {
} from '../../config';
import { getCurrentTabId } from '../../util/establishMultitabRole';
import { findLast } from '../../util/iteratees';
import { getMessageKey, isLocalMessageId } from '../../util/keys/messageKey';
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
import { getMessageKey, isLocalMessageId } from '../../util/messageKey';
import { getServerTime } from '../../util/serverTime';
import { IS_TRANSLATION_SUPPORTED } from '../../util/windowEnvironment';
import {

View File

@ -2,11 +2,11 @@ import type { ThreadId } from '../../types';
import type { GlobalState, TabArgs } from '../types';
import { getCurrentTabId } from '../../util/establishMultitabRole';
import { buildChatThreadKey } from '../helpers/localSearch';
import { buildChatThreadKey } from '../helpers/middleSearch';
import { selectCurrentMessageList } from './messages';
import { selectTabState } from './tabs';
export function selectCurrentTextSearch<T extends GlobalState>(
export function selectCurrentMiddleSearch<T extends GlobalState>(
global: T,
...[tabId = getCurrentTabId()]: TabArgs<T>
) {
@ -16,12 +16,8 @@ export function selectCurrentTextSearch<T extends GlobalState>(
}
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>(

View File

@ -5,7 +5,6 @@ import { NewChatMembersProgress, RightColumnContent } from '../../types';
import { getCurrentTabId } from '../../util/establishMultitabRole';
import { getMessageVideo, getMessageWebPageVideo } from '../helpers/messageMedia';
import { selectCurrentTextSearch } from './localSearch';
import { selectCurrentManagement } from './management';
import { selectIsStatisticsShown } from './statistics';
import { selectTabState } from './tabs';
@ -38,8 +37,6 @@ export function selectRightColumnContentKey<T extends GlobalState>(
RightColumnContent.CreateTopic
) : tabState.pollResults.messageId ? (
RightColumnContent.PollResults
) : !isMobile && selectCurrentTextSearch(global, tabId) ? (
RightColumnContent.Search
) : selectCurrentManagement(global, tabId) ? (
RightColumnContent.Management
) : tabState.isStatisticsShown && tabState.statistics.currentMessageId ? (

View File

@ -112,6 +112,7 @@ import type {
ManagementState,
MediaViewerMedia,
MediaViewerOrigin,
MiddleSearchParams,
NewChatMembersProgress,
NotifyException,
PaymentStep,
@ -127,6 +128,7 @@ import type {
ThemeKey,
ThreadId,
} from '../types';
import type { SearchResultKey } from '../util/keys/searchResultKey';
import type { DownloadableMedia } from './helpers';
export type MessageListType =
@ -365,7 +367,8 @@ export type TabState = {
globalSearch: {
query?: string;
date?: number;
minDate?: number;
maxDate?: number;
currentContent?: GlobalSearchContent;
chatId?: string;
foundTopicIds?: number[];
@ -382,8 +385,10 @@ export type TabState = {
};
resultsByType?: Partial<Record<ApiGlobalMessageSearchType, {
totalCount?: number;
nextOffsetId: number;
foundIds: string[];
nextOffsetId?: number;
nextOffsetPeerId?: string;
nextOffsetRate?: number;
foundIds: SearchResultKey[];
}>>;
};
@ -397,16 +402,8 @@ export type TabState = {
activeEmojiInteractions?: ActiveEmojiInteraction[];
activeReactions: Record<string, ApiReaction[]>;
localTextSearch: {
byChatThreadKey: Record<string, {
query?: string;
savedTag?: ApiReaction;
results?: {
totalCount?: number;
nextOffsetId?: number;
foundIds?: number[];
};
}>;
middleSearch: {
byChatThreadKey: Record<string, MiddleSearchParams | undefined>;
};
sharedMediaSearch: {
@ -930,6 +927,11 @@ export type GlobalState = {
all?: Record<string, number>;
saved?: Record<string, number>;
};
loadingParameters: Record<ChatListType, {
nextOffsetId?: number;
nextOffsetPeerId?: string;
nextOffsetDate?: number;
}>;
forDiscussionIds?: string[];
// Obtained from GetFullChat / GetFullChannel
fullInfoById: Record<string, ApiChatFullInfo>;
@ -1342,19 +1344,26 @@ export interface ActionPayloads {
userIds: string[];
};
// message search
openLocalTextSearch: WithTabId | undefined;
closeLocalTextSearch: WithTabId | undefined;
setLocalTextSearchQuery: {
// Message search
openMiddleSearch: WithTabId | undefined;
closeMiddleSearch: WithTabId | undefined;
updateMiddleSearch: {
chatId: string;
threadId?: ThreadId;
update: Partial<Omit<MiddleSearchParams, 'results'>>;
} & WithTabId;
resetMiddleSearch: WithTabId | undefined;
performMiddleSearch: {
chatId: string;
threadId?: ThreadId;
query?: string;
} & WithTabId;
setLocalTextSearchTag: {
tag: ApiReaction | undefined;
searchHashtag: {
hashtag: string;
} & WithTabId;
setSharedMediaSearchType: {
mediaType: SharedMediaType;
} & WithTabId;
searchTextMessagesLocal: WithTabId | undefined;
searchSharedMediaMessages: WithTabId | undefined;
searchChatMediaMessages: {
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[],
isDisabled = false,
listSlice = DEFAULT_LIST_SLICE,
): [ListId[]?, GetMore?] => {
): [ListId[]?, GetMore?, number?] => {
const requestParamsRef = useRef<{
direction?: LoadMoreDirection;
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) {
const {
newViewportIds,
newIsOnTop,
fromOffset,
} = getViewportSlice(listIds, LoadMoreDirection.Forwards, listSlice, listIds[0]);
currentStateRef.current = { viewportIds: newViewportIds, isOnTop: newIsOnTop };
currentStateRef.current = { viewportIds: newViewportIds, isOnTop: newIsOnTop, offset: fromOffset };
}
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 defaultOffsetId = currentMiddleId && listIds.includes(currentMiddleId) ? currentMiddleId : listIds[0];
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 = {};
if (!viewportIds || !areSortedArraysEqual(viewportIds, newViewportIds)) {
currentStateRef.current = { viewportIds: newViewportIds, isOnTop: newIsOnTop };
currentStateRef.current = { viewportIds: newViewportIds, isOnTop: newIsOnTop, offset: fromOffset };
}
} else if (!listIds) {
currentStateRef.current = undefined;
@ -75,11 +76,11 @@ const useInfiniteScroll = <ListId extends string | number>(
}
const {
newViewportIds, areSomeLocal, areAllLocal, newIsOnTop,
newViewportIds, areSomeLocal, areAllLocal, newIsOnTop, fromOffset,
} = getViewportSlice(listIds, direction, listSlice, offsetId);
if (areSomeLocal && !(viewportIds && areSortedArraysEqual(viewportIds, newViewportIds))) {
currentStateRef.current = { viewportIds: newViewportIds, isOnTop: newIsOnTop };
currentStateRef.current = { viewportIds: newViewportIds, isOnTop: newIsOnTop, offset: fromOffset };
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>(
@ -127,6 +128,7 @@ function getViewportSlice<ListId extends string | number>(
areSomeLocal,
areAllLocal,
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;
function useLongPress(
onStart: NoneToVoidFunction,
onEnd: NoneToVoidFunction,
) {
function useLongPress({
onClick, onStart, onEnd, threshold = DEFAULT_THRESHOLD,
}: {
onStart?: NoneToVoidFunction;
onClick?: (event: React.MouseEvent | React.TouchEvent) => void;
onEnd?: NoneToVoidFunction;
threshold?: number;
}) {
const isLongPressActive = useRef(false);
const isPressed = useRef(false);
const timerId = useRef<number | undefined>(undefined);
@ -18,20 +22,24 @@ function useLongPress(
isPressed.current = true;
timerId.current = window.setTimeout(() => {
onStart();
onStart?.();
isLongPressActive.current = true;
}, DEFAULT_THRESHOLD);
}, [onStart]);
}, threshold);
}, [onStart, threshold]);
const cancel = useCallback((e: React.MouseEvent | React.TouchEvent) => {
if (!isPressed.current) return;
const cancel = useCallback(() => {
if (isLongPressActive.current) {
onEnd();
onEnd?.();
} else {
onClick?.(e);
}
isLongPressActive.current = false;
isPressed.current = false;
window.clearTimeout(timerId.current);
}, [onEnd]);
}, [onEnd, onClick]);
useEffect(() => {
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.getChannelRecommendations#25a71742 flags:# channel:flags.0?InputChannel = messages.Chats;
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.canSendMessage#1359f4e6 bot:InputUser = Bool;
bots.allowSendMessage#f132e3ef bot:InputUser = Updates;

View File

@ -224,6 +224,7 @@
"channels.getSponsoredMessages",
"channels.getChannelRecommendations",
"channels.reportSponsoredMessage",
"channels.searchPosts",
"bots.canSendMessage",
"bots.allowSendMessage",
"bots.invokeWebViewCustomMethod",

View File

@ -915,7 +915,7 @@ function DEBUG_checkKeyUniqueness(children: VirtualElementChildren) {
if (keys.length !== unique(keys).length) {
// 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');
}
}

View File

@ -256,7 +256,7 @@ $color-message-story-mention-to: #74bcff;
--z-forum-panel: 13;
--z-message-context-menu: 13;
--z-scroll-down-button: 12;
--z-mobile-search: 12;
--z-local-search: 12;
--z-left-header: 11;
--z-middle-header: 11;
--z-middle-footer: 11;

View File

@ -17,6 +17,7 @@ import type {
ApiUser,
ApiVideo,
} from '../api/types';
import type { SearchResultKey } from '../util/keys/searchResultKey';
import type { IconName } from './icons';
export type TextPart = TeactNode;
@ -299,7 +300,6 @@ export enum GlobalSearchContent {
export enum RightColumnContent {
ChatInfo,
Search,
Management,
Statistics,
BoostStatistics,
@ -399,6 +399,24 @@ export type ProfileTabType =
| 'similarChannels'
| 'dialogs';
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' |
'forwards' | 'chatInvite' | 'phoneCall' | 'phoneP2P' | 'bio' | 'birthday';
export type PrivacyVisibility = 'everybody' | 'contacts' | 'closeFriends' | 'nonContacts' | 'nobody';

View File

@ -1185,7 +1185,6 @@ export interface LangPair {
'AttachSticker': undefined;
'AttachMusic': undefined;
'AttachContact': undefined;
'PaymentInvoice': undefined;
'MessageLocation': undefined;
'MessageLiveLocation': undefined;
'ServiceNotifications': undefined;
@ -1511,8 +1510,11 @@ export interface LangPair {
'MenuBetaChangelog': undefined;
'MenuSwitchToK': undefined;
'MenuInstallApp': undefined;
'RemoveEffect' : undefined;
'RemoveEffect': undefined;
'ReplyInPrivateMessage': undefined;
'AriaSearchOlderResult': undefined;
'AriaSearchNewerResult': undefined;
}
export type LangKey = keyof LangPair;

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