Search: New design (#4718)
This commit is contained in:
parent
9bf26bbb1c
commit
5f5536b6a0
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -1271,3 +1271,5 @@
|
||||
"MenuInstallApp" = "Install App";
|
||||
"RemoveEffect" = "Remove effect";
|
||||
"ReplyInPrivateMessage" = "Reply In Private Message";
|
||||
"AriaSearchOlderResult" = "Focus next result";
|
||||
"AriaSearchNewerResult" = "Focus previous result";
|
||||
|
||||
@ -57,7 +57,7 @@ export { default as SponsoredMessageContextMenuContainer }
|
||||
export { default as StickerSetModal } from '../components/common/StickerSetModal';
|
||||
export { default as 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';
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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} />
|
||||
)}
|
||||
|
||||
@ -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 (
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -152,7 +152,6 @@ const EmbeddedMessage: FC<OwnProps> = ({
|
||||
|
||||
return (
|
||||
<MessageSummary
|
||||
lang={lang}
|
||||
message={message}
|
||||
noEmoji={Boolean(mediaThumbnail)}
|
||||
translatedText={translatedText}
|
||||
|
||||
@ -254,7 +254,6 @@ function renderMessageContent(
|
||||
|
||||
const messageSummary = (
|
||||
<MessageSummary
|
||||
lang={lang}
|
||||
message={message}
|
||||
truncateLength={MAX_LENGTH}
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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>) {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -141,4 +141,10 @@
|
||||
pointer-events: none;
|
||||
}
|
||||
}
|
||||
|
||||
.left-search-picker-item {
|
||||
color: var(--color-text-secondary);
|
||||
font-weight: 500;
|
||||
padding-right: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@ -222,18 +222,22 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
|
||||
<PickerSelectedItem
|
||||
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,
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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]);
|
||||
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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]);
|
||||
|
||||
@ -4,7 +4,6 @@
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
margin-left: 0.5rem;
|
||||
margin-bottom: 0.5rem;
|
||||
|
||||
.date-item {
|
||||
display: flex;
|
||||
|
||||
@ -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[];
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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]);
|
||||
|
||||
|
||||
@ -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]);
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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]);
|
||||
|
||||
|
||||
@ -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]);
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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),
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -1,18 +0,0 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React from '../../lib/teact/teact';
|
||||
|
||||
import type { OwnProps } from './MobileSearch';
|
||||
|
||||
import { Bundles } from '../../util/moduleLoader';
|
||||
|
||||
import useModuleLoader from '../../hooks/useModuleLoader';
|
||||
|
||||
const MobileSearchAsync: FC<OwnProps> = (props) => {
|
||||
const { isActive } = props;
|
||||
const MobileSearch = useModuleLoader(Bundles.Extra, 'MobileSearch', !isActive, true);
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return MobileSearch ? <MobileSearch {...props} /> : undefined;
|
||||
};
|
||||
|
||||
export default MobileSearchAsync;
|
||||
@ -1,84 +0,0 @@
|
||||
#MobileSearch > .header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: var(--z-mobile-search);
|
||||
width: 100%;
|
||||
height: 3.5rem;
|
||||
background: var(--color-background);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: max(0.25rem, env(safe-area-inset-left));
|
||||
padding-right: max(0.5rem, env(safe-area-inset-right));
|
||||
|
||||
> .SearchInput {
|
||||
margin-left: 0.25rem;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
body.is-electron.is-macos & {
|
||||
padding-left: 4.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
#MobileSearch > .tags-subheader {
|
||||
--color-reaction: var(--color-background-secondary);
|
||||
--hover-color-reaction: var(--color-background-secondary-accent);
|
||||
--text-color-reaction: var(--color-text-secondary);
|
||||
--color-reaction-chosen: var(--color-primary);
|
||||
--text-color-reaction-chosen: #FFFFFF;
|
||||
--hover-color-reaction-chosen: var(--color-primary-shade);
|
||||
|
||||
position: absolute;
|
||||
top: 3.5rem;
|
||||
left: 0;
|
||||
z-index: var(--z-mobile-search);
|
||||
width: 100%;
|
||||
height: 3rem;
|
||||
background: var(--color-background);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
padding-left: max(0.25rem, env(safe-area-inset-left));
|
||||
padding-right: max(0.5rem, env(safe-area-inset-right));
|
||||
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
#MobileSearch > .footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
z-index: var(--z-mobile-search);
|
||||
width: 100%;
|
||||
height: 3.5rem;
|
||||
background: var(--color-background);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: max(1rem, env(safe-area-inset-left));
|
||||
padding-right: max(0.5rem, env(safe-area-inset-right));
|
||||
|
||||
body:not(.keyboard-visible) & {
|
||||
padding-bottom: 0;
|
||||
height: 3.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
body:not(.keyboard-visible) & {
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
height: calc(3.5rem + env(safe-area-inset-bottom));
|
||||
}
|
||||
}
|
||||
|
||||
> .counter {
|
||||
flex: 1;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
#MobileSearch:not(.active) {
|
||||
.header, .tags-subheader, .footer {
|
||||
// `display: none` will prevent synchronous focus on iOS
|
||||
transform: translateX(-999rem);
|
||||
}
|
||||
}
|
||||
@ -1,322 +0,0 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useEffect, useLayoutEffect,
|
||||
useMemo,
|
||||
useRef, useState,
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type {
|
||||
ApiChat, ApiReaction, ApiReactionKey, ApiSavedReactionTag,
|
||||
} from '../../api/types';
|
||||
import type { ThreadId } from '../../types';
|
||||
|
||||
import { requestMutation } from '../../lib/fasterdom/fasterdom';
|
||||
import { getIsSavedDialog, getReactionKey, isSameReaction } from '../../global/helpers';
|
||||
import {
|
||||
selectChat,
|
||||
selectCurrentMessageList,
|
||||
selectCurrentTextSearch,
|
||||
selectIsChatWithSelf,
|
||||
selectIsCurrentUserPremium,
|
||||
selectTabState,
|
||||
} from '../../global/selectors';
|
||||
import { getDayStartAt } from '../../util/dates/dateFormat';
|
||||
import { debounce } from '../../util/schedulers';
|
||||
import { IS_IOS } from '../../util/windowEnvironment';
|
||||
|
||||
import useHorizontalScroll from '../../hooks/useHorizontalScroll';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
|
||||
import Button from '../ui/Button';
|
||||
import SearchInput from '../ui/SearchInput';
|
||||
import SavedTagButton from './message/reactions/SavedTagButton';
|
||||
|
||||
import './MobileSearch.scss';
|
||||
|
||||
export type OwnProps = {
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
isActive?: boolean;
|
||||
chat?: ApiChat;
|
||||
threadId?: ThreadId;
|
||||
query?: string;
|
||||
savedTags?: Record<ApiReactionKey, ApiSavedReactionTag>;
|
||||
searchTag?: ApiReaction;
|
||||
totalCount?: number;
|
||||
foundIds?: number[];
|
||||
isHistoryCalendarOpen?: boolean;
|
||||
isCurrentUserPremium?: boolean;
|
||||
};
|
||||
|
||||
const runDebouncedForSearch = debounce((cb) => cb(), 200, false);
|
||||
|
||||
const MobileSearchFooter: FC<StateProps> = ({
|
||||
isActive,
|
||||
chat,
|
||||
threadId,
|
||||
query,
|
||||
savedTags,
|
||||
searchTag,
|
||||
totalCount,
|
||||
foundIds,
|
||||
isHistoryCalendarOpen,
|
||||
isCurrentUserPremium,
|
||||
}) => {
|
||||
const {
|
||||
setLocalTextSearchQuery,
|
||||
setLocalTextSearchTag,
|
||||
searchTextMessagesLocal,
|
||||
focusMessage,
|
||||
closeLocalTextSearch,
|
||||
openHistoryCalendar,
|
||||
openPremiumModal,
|
||||
loadSavedReactionTags,
|
||||
} = getActions();
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const tagsRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const [focusedIndex, setFocusedIndex] = useState(0);
|
||||
|
||||
const hasQueryData = Boolean(query || searchTag);
|
||||
|
||||
// Fix for iOS keyboard
|
||||
useEffect(() => {
|
||||
const { visualViewport } = window as any;
|
||||
if (!visualViewport) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const mainEl = document.getElementById('Main') as HTMLDivElement;
|
||||
const handleResize = () => {
|
||||
const { activeElement } = document;
|
||||
if (activeElement && (activeElement === inputRef.current)) {
|
||||
const { pageTop, height } = visualViewport;
|
||||
|
||||
requestMutation(() => {
|
||||
mainEl.style.transform = `translateY(${pageTop}px)`;
|
||||
mainEl.style.height = `${height}px`;
|
||||
document.documentElement.scrollTop = pageTop;
|
||||
});
|
||||
} else {
|
||||
requestMutation(() => {
|
||||
mainEl.style.transform = '';
|
||||
mainEl.style.height = '';
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
visualViewport.addEventListener('resize', handleResize);
|
||||
|
||||
return () => {
|
||||
visualViewport.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Focus message
|
||||
useEffect(() => {
|
||||
if (chat?.id && foundIds?.length) {
|
||||
focusMessage({ chatId: chat.id, messageId: foundIds[0], threadId });
|
||||
setFocusedIndex(0);
|
||||
} else {
|
||||
setFocusedIndex(-1);
|
||||
}
|
||||
}, [chat?.id, focusMessage, foundIds, threadId]);
|
||||
|
||||
// Disable native up/down buttons on iOS
|
||||
useLayoutEffect(() => {
|
||||
if (!IS_IOS) return;
|
||||
|
||||
Array.from(document.querySelectorAll<HTMLInputElement>('input')).forEach((input) => {
|
||||
input.disabled = Boolean(isActive && input !== inputRef.current);
|
||||
});
|
||||
}, [isActive]);
|
||||
|
||||
// Blur on exit
|
||||
useEffect(() => {
|
||||
if (!isActive) {
|
||||
inputRef.current!.blur();
|
||||
}
|
||||
}, [isActive]);
|
||||
|
||||
useEffect(() => {
|
||||
const searchInput = document.querySelector<HTMLInputElement>('#MobileSearch input')!;
|
||||
searchInput.blur();
|
||||
}, [isHistoryCalendarOpen]);
|
||||
|
||||
const tags = useMemo(() => {
|
||||
if (!savedTags) return undefined;
|
||||
return Object.values(savedTags);
|
||||
}, [savedTags]);
|
||||
|
||||
const hasTags = Boolean(tags?.length);
|
||||
const areTagsDisabled = hasTags && !isCurrentUserPremium;
|
||||
|
||||
useHorizontalScroll(tagsRef, !hasTags);
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive) loadSavedReactionTags();
|
||||
}, [hasTags, isActive]);
|
||||
|
||||
const handleMessageSearchQueryChange = useLastCallback((newQuery: string) => {
|
||||
setLocalTextSearchQuery({ query: newQuery });
|
||||
|
||||
if (hasQueryData) {
|
||||
runDebouncedForSearch(searchTextMessagesLocal);
|
||||
}
|
||||
});
|
||||
|
||||
const handleTagClick = useLastCallback((tag: ApiReaction) => {
|
||||
if (areTagsDisabled) {
|
||||
openPremiumModal({
|
||||
initialSection: 'saved_tags',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
setLocalTextSearchTag({ tag });
|
||||
|
||||
runDebouncedForSearch(searchTextMessagesLocal);
|
||||
});
|
||||
|
||||
const handleUp = useLastCallback(() => {
|
||||
if (chat && foundIds) {
|
||||
const newFocusIndex = focusedIndex + 1;
|
||||
focusMessage({ chatId: chat.id, messageId: foundIds[newFocusIndex], threadId });
|
||||
setFocusedIndex(newFocusIndex);
|
||||
}
|
||||
});
|
||||
|
||||
const handleDown = useLastCallback(() => {
|
||||
if (chat && foundIds) {
|
||||
const newFocusIndex = focusedIndex - 1;
|
||||
focusMessage({ chatId: chat.id, messageId: foundIds[newFocusIndex], threadId });
|
||||
setFocusedIndex(newFocusIndex);
|
||||
}
|
||||
});
|
||||
|
||||
const handleCloseLocalTextSearch = useLastCallback(() => {
|
||||
closeLocalTextSearch();
|
||||
});
|
||||
|
||||
return (
|
||||
<div id="MobileSearch" className={isActive ? 'active' : ''}>
|
||||
<div className="header">
|
||||
<Button
|
||||
size="smaller"
|
||||
round
|
||||
color="translucent"
|
||||
onClick={handleCloseLocalTextSearch}
|
||||
>
|
||||
<i className="icon icon-arrow-left" />
|
||||
</Button>
|
||||
<SearchInput
|
||||
ref={inputRef}
|
||||
value={query}
|
||||
onChange={handleMessageSearchQueryChange}
|
||||
/>
|
||||
</div>
|
||||
{hasTags && (
|
||||
<div
|
||||
ref={tagsRef}
|
||||
className="tags-subheader custom-scroll-x no-scrollbar"
|
||||
>
|
||||
{tags.map((tag) => (
|
||||
<SavedTagButton
|
||||
containerId="mobile-search"
|
||||
key={getReactionKey(tag.reaction)}
|
||||
reaction={tag.reaction}
|
||||
tag={tag}
|
||||
withCount
|
||||
isDisabled={areTagsDisabled}
|
||||
isChosen={isSameReaction(tag.reaction, searchTag)}
|
||||
onClick={handleTagClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
<div className="footer">
|
||||
<div className="counter">
|
||||
{hasQueryData ? (
|
||||
foundIds?.length ? (
|
||||
`${focusedIndex + 1} of ${totalCount}`
|
||||
) : foundIds && !foundIds.length ? (
|
||||
'No results'
|
||||
) : (
|
||||
''
|
||||
)
|
||||
) : (
|
||||
<Button
|
||||
round
|
||||
size="smaller"
|
||||
color="translucent"
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => openHistoryCalendar({ selectedAt: getDayStartAt(Date.now()) })}
|
||||
ariaLabel="Search messages by date"
|
||||
>
|
||||
<i className="icon icon-calendar" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
round
|
||||
size="smaller"
|
||||
color="translucent"
|
||||
onClick={handleUp}
|
||||
disabled={!foundIds || !foundIds.length || focusedIndex === foundIds.length - 1}
|
||||
>
|
||||
<i className="icon icon-up" />
|
||||
</Button>
|
||||
<Button
|
||||
round
|
||||
size="smaller"
|
||||
color="translucent"
|
||||
onClick={handleDown}
|
||||
disabled={!foundIds || !foundIds.length || focusedIndex === 0}
|
||||
>
|
||||
<i className="icon icon-down" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global): StateProps => {
|
||||
const currentMessageList = selectCurrentMessageList(global);
|
||||
if (!currentMessageList) {
|
||||
return {};
|
||||
}
|
||||
const { chatId, threadId } = currentMessageList;
|
||||
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const { query, savedTag, results } = selectCurrentTextSearch(global) || {};
|
||||
const { totalCount, foundIds } = results || {};
|
||||
|
||||
const isSavedMessages = selectIsChatWithSelf(global, chatId);
|
||||
const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
|
||||
|
||||
const savedTags = isSavedMessages && !isSavedDialog ? global.savedReactionTags?.byKey : undefined;
|
||||
|
||||
return {
|
||||
chat,
|
||||
query,
|
||||
totalCount,
|
||||
threadId,
|
||||
foundIds,
|
||||
isHistoryCalendarOpen: Boolean(selectTabState(global).historyCalendarSelectedAt),
|
||||
savedTags,
|
||||
searchTag: savedTag,
|
||||
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
||||
};
|
||||
},
|
||||
)(MobileSearchFooter));
|
||||
@ -17,7 +17,7 @@ import {
|
||||
selectCanAutoPlayMedia,
|
||||
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';
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
|
||||
18
src/components/middle/search/MiddleSearch.async.tsx
Normal file
18
src/components/middle/search/MiddleSearch.async.tsx
Normal file
@ -0,0 +1,18 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React from '../../../lib/teact/teact';
|
||||
|
||||
import type { OwnProps } from './MiddleSearch';
|
||||
|
||||
import { Bundles } from '../../../util/moduleLoader';
|
||||
|
||||
import useModuleLoader from '../../../hooks/useModuleLoader';
|
||||
|
||||
const MiddleSearchAsync: FC<OwnProps> = (props) => {
|
||||
const { isActive } = props;
|
||||
const MiddleSearch = useModuleLoader(Bundles.Extra, 'MiddleSearch', !isActive, true);
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return MiddleSearch ? <MiddleSearch {...props} /> : undefined;
|
||||
};
|
||||
|
||||
export default MiddleSearchAsync;
|
||||
300
src/components/middle/search/MiddleSearch.module.scss
Normal file
300
src/components/middle/search/MiddleSearch.module.scss
Normal file
@ -0,0 +1,300 @@
|
||||
@use "../../../styles/mixins";
|
||||
|
||||
.root {
|
||||
--color-reaction: var(--color-background-secondary);
|
||||
--hover-color-reaction: var(--color-background-secondary-accent);
|
||||
--text-color-reaction: var(--color-text-secondary);
|
||||
--color-reaction-chosen: var(--color-primary);
|
||||
--text-color-reaction-chosen: #FFFFFF;
|
||||
--hover-color-reaction-chosen: var(--color-primary-shade);
|
||||
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
pointer-events: none;
|
||||
z-index: var(--z-local-search);
|
||||
|
||||
@media (min-width: 1276px) {
|
||||
:global(#Main.right-column-open) & {
|
||||
width: calc(100% - var(--right-column-width));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 3.5rem;
|
||||
background-color: var(--color-background);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: max(1.5rem, env(safe-area-inset-left));
|
||||
padding-right: max(0.875rem, env(safe-area-inset-right));
|
||||
|
||||
pointer-events: auto;
|
||||
|
||||
opacity: 0;
|
||||
transition: opacity 200ms ease-in-out;
|
||||
|
||||
.active & {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
padding-left: max(0.5rem, env(safe-area-inset-left));
|
||||
padding-right: max(0.5rem, env(safe-area-inset-right));
|
||||
}
|
||||
|
||||
:global(body.is-electron.is-macos) & {
|
||||
padding-left: 4.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
// Same as in MiddleHeader.scss
|
||||
.avatar {
|
||||
width: 2.5rem !important;
|
||||
height: 2.5rem !important;
|
||||
margin-right: 0.625rem;
|
||||
}
|
||||
|
||||
.input {
|
||||
border: none;
|
||||
margin-left: 0.25rem;
|
||||
margin-right: 0.75rem;
|
||||
flex: 1;
|
||||
|
||||
transition-property: background-color, box-shadow, border-radius;
|
||||
transition-duration: 200ms;
|
||||
transition-timing-function: ease-in-out;
|
||||
|
||||
.mobile & {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.focused .input {
|
||||
box-shadow: 0 0 0.625rem 0 var(--color-default-shadow);
|
||||
}
|
||||
|
||||
.withDropdown {
|
||||
background-color: var(--color-background);
|
||||
box-shadow: 0 0 0.625rem 0 var(--color-default-shadow);
|
||||
}
|
||||
|
||||
.adaptSearchBorders {
|
||||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
|
||||
.dropdown {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
transform: translateY(100%);
|
||||
max-height: min(24rem, 80vh);
|
||||
pointer-events: all;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
background-color: var(--color-background);
|
||||
|
||||
overflow: hidden;
|
||||
box-shadow: 0 0 0.625rem 0 var(--color-default-shadow);
|
||||
clip-path: inset(0 -0.625rem -0.625rem -0.625rem); // Hide top shadow
|
||||
|
||||
border-bottom-left-radius: 1.375rem;
|
||||
border-bottom-right-radius: 1.375rem;
|
||||
|
||||
transition-behavior: allow-discrete;
|
||||
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
|
||||
transition-property: display, opacity;
|
||||
transition-duration: 200ms;
|
||||
|
||||
.mobile & {
|
||||
position: absolute;
|
||||
top: 3.375rem; // Subpixel rendering can leave 1px gap otherwise
|
||||
right: 0;
|
||||
bottom: 3.375rem;
|
||||
left: 0;
|
||||
|
||||
max-height: none;
|
||||
padding: 0;
|
||||
|
||||
transform: none;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
@starting-style {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.dropdownHidden {
|
||||
display: none;
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.results {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
overflow-y: scroll;
|
||||
padding: 0.5rem;
|
||||
|
||||
@include mixins.adapt-padding-to-scrollbar(0.5rem);
|
||||
}
|
||||
|
||||
.placeholder {
|
||||
color: var(--color-text-secondary);
|
||||
text-align: center;
|
||||
margin: 0.5rem;
|
||||
}
|
||||
|
||||
.separator {
|
||||
margin-inline: 1rem;
|
||||
border-top: 1px solid var(--color-borders);
|
||||
}
|
||||
|
||||
.savedTags {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.375rem;
|
||||
flex-shrink: 0;
|
||||
|
||||
padding-block: 1rem;
|
||||
margin-inline: 1rem;
|
||||
|
||||
border-bottom: 1px solid var(--color-borders);
|
||||
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
.wrap {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.searchTags {
|
||||
display: flex;
|
||||
gap: 0.125rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.savedSearchTag {
|
||||
margin-inline: 0.5rem;
|
||||
}
|
||||
|
||||
.hash {
|
||||
margin-inline-end: -0.5rem;
|
||||
margin-inline-start: 0.5rem;
|
||||
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 1.5rem;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.searchTypes {
|
||||
display: flex;
|
||||
flex-shrink: 0;
|
||||
|
||||
padding-block: 1rem;
|
||||
margin-inline: 1rem;
|
||||
|
||||
border-bottom: 1px solid var(--color-borders);
|
||||
|
||||
overflow-x: scroll;
|
||||
}
|
||||
|
||||
.searchType {
|
||||
--accent-color: var(--color-primary);
|
||||
|
||||
flex-grow: 0 !important;
|
||||
flex-shrink: 0;
|
||||
|
||||
color: var(--color-text-secondary);
|
||||
background-color: var(--color-item-active);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.selectedType {
|
||||
background-color: var(--color-primary);
|
||||
color: var(--color-white) !important;
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-primary-shade);
|
||||
}
|
||||
}
|
||||
|
||||
.footer {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 3.5rem;
|
||||
background-color: var(--color-background);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding-left: max(1rem, env(safe-area-inset-left));
|
||||
padding-right: max(0.5rem, env(safe-area-inset-right));
|
||||
|
||||
pointer-events: auto;
|
||||
|
||||
box-shadow: 0 -2px 2px var(--color-light-shadow);
|
||||
|
||||
transform: translateY(100%);
|
||||
transition: transform 200ms ease-in-out;
|
||||
|
||||
.active & {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
:global {
|
||||
body:not(.keyboard-visible) & {
|
||||
padding-bottom: 0;
|
||||
height: 3.5rem;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
body:not(.keyboard-visible) & {
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
height: calc(3.5rem + env(safe-area-inset-bottom));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.counter {
|
||||
flex: 1;
|
||||
color: var(--color-text-secondary);
|
||||
}
|
||||
|
||||
.mobileNavigation {
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
bottom: 4rem;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.5rem;
|
||||
}
|
||||
|
||||
.navigationButton {
|
||||
transition-property: background-color, color, filter;
|
||||
}
|
||||
|
||||
.navigationDisabled {
|
||||
filter: brightness(0.9);
|
||||
}
|
||||
|
||||
@keyframes jumpIn {
|
||||
from {
|
||||
transform: translateY(100%);
|
||||
}
|
||||
to {
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
818
src/components/middle/search/MiddleSearch.tsx
Normal file
818
src/components/middle/search/MiddleSearch.tsx
Normal file
@ -0,0 +1,818 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useEffect, useLayoutEffect,
|
||||
useMemo,
|
||||
useRef, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, getGlobal, withGlobal } from '../../../global';
|
||||
|
||||
import type {
|
||||
ApiChat, ApiMessage, ApiReaction, ApiReactionKey, ApiSavedReactionTag,
|
||||
} from '../../../api/types';
|
||||
import type {
|
||||
CustomPeer, MiddleSearchParams, MiddleSearchType, ThreadId,
|
||||
} from '../../../types';
|
||||
|
||||
import { ANONYMOUS_USER_ID, REPLIES_USER_ID } from '../../../config';
|
||||
import { requestMeasure, requestMutation, requestNextMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
import { getIsSavedDialog, getReactionKey, isSameReaction } from '../../../global/helpers';
|
||||
import {
|
||||
selectChat,
|
||||
selectChatMessage,
|
||||
selectCurrentMessageList,
|
||||
selectCurrentMiddleSearch,
|
||||
selectForwardedSender,
|
||||
selectIsChatWithSelf,
|
||||
selectIsCurrentUserPremium,
|
||||
selectSender,
|
||||
selectTabState,
|
||||
} from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
||||
import { getDayStartAt } from '../../../util/dates/dateFormat';
|
||||
import focusEditableElement from '../../../util/focusEditableElement';
|
||||
import { getSearchResultKey, parseSearchResultKey, type SearchResultKey } from '../../../util/keys/searchResultKey';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||
import { debounce, fastRaf } from '../../../util/schedulers';
|
||||
import { IS_IOS } from '../../../util/windowEnvironment';
|
||||
|
||||
import { useClickOutside } from '../../../hooks/events/useOutsideClick';
|
||||
import useAppLayout from '../../../hooks/useAppLayout';
|
||||
import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||
import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
|
||||
import useKeyboardListNavigation from '../../../hooks/useKeyboardListNavigation';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import Avatar from '../../common/Avatar';
|
||||
import Icon from '../../common/icons/Icon';
|
||||
import PickerSelectedItem from '../../common/PickerSelectedItem';
|
||||
import Button from '../../ui/Button';
|
||||
import InfiniteScroll from '../../ui/InfiniteScroll';
|
||||
import SearchInput from '../../ui/SearchInput';
|
||||
import SavedTagButton from '../message/reactions/SavedTagButton';
|
||||
import MiddleSearchResult from './MiddleSearchResult';
|
||||
|
||||
import styles from './MiddleSearch.module.scss';
|
||||
|
||||
export type OwnProps = {
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
isActive?: boolean;
|
||||
chat?: ApiChat;
|
||||
threadId?: ThreadId;
|
||||
requestedQuery?: string;
|
||||
savedTags?: Record<ApiReactionKey, ApiSavedReactionTag>;
|
||||
savedTag?: ApiReaction;
|
||||
totalCount?: number;
|
||||
lastSearchQuery?: string;
|
||||
foundIds?: SearchResultKey[];
|
||||
isHistoryCalendarOpen?: boolean;
|
||||
isCurrentUserPremium?: boolean;
|
||||
isSavedMessages?: boolean;
|
||||
fetchingQuery?: string;
|
||||
isHashtagQuery?: boolean;
|
||||
searchType?: MiddleSearchType;
|
||||
currentUserId?: string;
|
||||
};
|
||||
|
||||
const CHANNELS_PEER: CustomPeer = {
|
||||
isCustomPeer: true,
|
||||
avatarIcon: 'channel-filled',
|
||||
titleKey: 'SearchPublicPosts',
|
||||
};
|
||||
const FOCUSED_SEARCH_TRIGGER_OFFSET = 5;
|
||||
const HIDE_TIMEOUT = 200;
|
||||
const RESULT_ITEM_CLASS_NAME = 'MiddleSearchResult';
|
||||
|
||||
const runDebouncedForSearch = debounce((cb) => cb(), 200, false);
|
||||
|
||||
const MiddleSearch: FC<StateProps> = ({
|
||||
isActive,
|
||||
chat,
|
||||
threadId,
|
||||
requestedQuery,
|
||||
savedTags,
|
||||
savedTag,
|
||||
totalCount,
|
||||
lastSearchQuery,
|
||||
foundIds,
|
||||
isHistoryCalendarOpen,
|
||||
isCurrentUserPremium,
|
||||
isSavedMessages,
|
||||
fetchingQuery,
|
||||
isHashtagQuery,
|
||||
searchType = 'chat',
|
||||
currentUserId,
|
||||
}) => {
|
||||
const {
|
||||
updateMiddleSearch,
|
||||
resetMiddleSearch,
|
||||
performMiddleSearch,
|
||||
focusMessage,
|
||||
closeMiddleSearch,
|
||||
openHistoryCalendar,
|
||||
openPremiumModal,
|
||||
loadSavedReactionTags,
|
||||
} = getActions();
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const inputRef = useRef<HTMLInputElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const { isMobile } = useAppLayout();
|
||||
const oldLang = useOldLang();
|
||||
const lang = useLang();
|
||||
|
||||
const [query, setQuery] = useState(requestedQuery || '');
|
||||
const [focusedIndex, setFocusedIndex] = useState(0);
|
||||
const canFocusNewer = foundIds && focusedIndex > 0;
|
||||
const canFocusOlder = foundIds && focusedIndex < foundIds.length - 1;
|
||||
|
||||
const [isFullyHidden, setIsFullyHidden] = useState(!isActive);
|
||||
const hiddenTimerRef = useRef<number>();
|
||||
const maybeLongPressActiveRef = useRef(true);
|
||||
|
||||
const [isFocused, markFocused, markBlurred] = useFlag();
|
||||
const [isViewAsList, setIsViewAsList] = useState(true);
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
const handleClickOutside = useLastCallback((event: MouseEvent) => {
|
||||
if (maybeLongPressActiveRef.current) return;
|
||||
// Ignore clicks inside modals
|
||||
if ((event.target as HTMLElement).closest('.Modal')) return;
|
||||
markBlurred();
|
||||
});
|
||||
useClickOutside([ref], handleClickOutside);
|
||||
|
||||
const hasResultsContainer = Boolean((query && foundIds) || isHashtagQuery);
|
||||
const isOnlyHash = isHashtagQuery && !query;
|
||||
const areResultsEmpty = Boolean(query && foundIds && !foundIds.length && !isLoading && !isOnlyHash);
|
||||
const hasResultsPlaceholder = areResultsEmpty || isOnlyHash;
|
||||
const isNonFocusedDropdownForced = searchType === 'myChats' || searchType === 'channels';
|
||||
const hasResultsDropdown = isActive && (isViewAsList || !isMobile) && (isFocused || isNonFocusedDropdownForced)
|
||||
&& Boolean(
|
||||
hasResultsContainer || hasResultsPlaceholder || savedTags,
|
||||
);
|
||||
|
||||
const hasQueryData = Boolean((query && !isOnlyHash) || savedTag);
|
||||
const hasNavigationButtons = searchType === 'chat' && Boolean(foundIds?.length);
|
||||
|
||||
const handleClose = useLastCallback(() => {
|
||||
closeMiddleSearch();
|
||||
});
|
||||
|
||||
const focusInput = useLastCallback(() => {
|
||||
requestMeasure(() => {
|
||||
inputRef.current?.focus();
|
||||
});
|
||||
});
|
||||
|
||||
const blurInput = useLastCallback(() => {
|
||||
inputRef.current?.blur();
|
||||
});
|
||||
|
||||
// Fix for iOS keyboard
|
||||
useEffect(() => {
|
||||
const { visualViewport } = window;
|
||||
if (!visualViewport) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const mainEl = document.getElementById('Main') as HTMLDivElement;
|
||||
const handleResize = () => {
|
||||
const { activeElement } = document;
|
||||
if (activeElement && (activeElement === inputRef.current)) {
|
||||
const { pageTop, height } = visualViewport;
|
||||
|
||||
requestMutation(() => {
|
||||
mainEl.style.transform = `translateY(${pageTop}px)`;
|
||||
mainEl.style.height = `${height}px`;
|
||||
document.documentElement.scrollTop = pageTop;
|
||||
});
|
||||
} else {
|
||||
requestMutation(() => {
|
||||
mainEl.style.transform = '';
|
||||
mainEl.style.height = '';
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
visualViewport.addEventListener('resize', handleResize);
|
||||
|
||||
return () => {
|
||||
visualViewport.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Focus message
|
||||
useEffect(() => {
|
||||
if (foundIds?.length) {
|
||||
if (searchType === 'chat') {
|
||||
const [chatId, messageId] = parseSearchResultKey(foundIds[0]);
|
||||
focusMessage({ chatId, messageId, threadId });
|
||||
}
|
||||
setFocusedIndex(0);
|
||||
} else {
|
||||
setFocusedIndex(-1);
|
||||
}
|
||||
}, [searchType, focusMessage, foundIds, threadId]);
|
||||
|
||||
// Disable native up/down buttons on iOS
|
||||
useLayoutEffect(() => {
|
||||
if (!IS_IOS) return;
|
||||
|
||||
Array.from(document.querySelectorAll<HTMLInputElement>('input')).forEach((input) => {
|
||||
input.disabled = Boolean(isActive && input !== inputRef.current);
|
||||
});
|
||||
}, [isActive]);
|
||||
|
||||
// Blur on exit
|
||||
useEffect(() => {
|
||||
if (!isActive) {
|
||||
inputRef.current!.blur();
|
||||
setIsViewAsList(true);
|
||||
setFocusedIndex(0);
|
||||
setQuery('');
|
||||
hiddenTimerRef.current = window.setTimeout(() => setIsFullyHidden(true), HIDE_TIMEOUT);
|
||||
} else {
|
||||
setIsFullyHidden(false);
|
||||
clearTimeout(hiddenTimerRef.current);
|
||||
}
|
||||
}, [isActive]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!requestedQuery || !chat?.id) return;
|
||||
setQuery(requestedQuery);
|
||||
updateMiddleSearch({ chatId: chat.id, threadId, update: { requestedQuery: undefined } });
|
||||
setIsLoading(true);
|
||||
|
||||
requestNextMutation(() => {
|
||||
const input = inputRef.current;
|
||||
if (!input) return;
|
||||
focusEditableElement(input, true, true);
|
||||
markFocused();
|
||||
});
|
||||
}, [chat?.id, requestedQuery, threadId]);
|
||||
|
||||
useEffectWithPrevDeps(([prevIsActive]) => {
|
||||
if (isActive !== prevIsActive && !query && lastSearchQuery) {
|
||||
setQuery(lastSearchQuery); // Restore query when returning back
|
||||
}
|
||||
}, [isActive, lastSearchQuery, query]);
|
||||
|
||||
useEffectWithPrevDeps(([prevIsCalendarOpen]) => {
|
||||
if (!isActive || prevIsCalendarOpen === isHistoryCalendarOpen) return;
|
||||
if (isHistoryCalendarOpen) {
|
||||
blurInput();
|
||||
markBlurred();
|
||||
} else {
|
||||
focusInput();
|
||||
}
|
||||
}, [isHistoryCalendarOpen, isActive]);
|
||||
|
||||
const handleReset = useLastCallback(() => {
|
||||
if (!query?.length && !savedTag) {
|
||||
handleClose();
|
||||
return;
|
||||
}
|
||||
|
||||
setQuery('');
|
||||
setIsLoading(false);
|
||||
resetMiddleSearch();
|
||||
focusInput();
|
||||
});
|
||||
|
||||
useEffect(() => (isActive ? captureEscKeyListener(handleReset) : undefined), [isActive, handleClose]);
|
||||
|
||||
const savedTagsArray = useMemo(() => {
|
||||
if (!savedTags) return undefined;
|
||||
return Object.values(savedTags);
|
||||
}, [savedTags]);
|
||||
|
||||
const hasSavedTags = Boolean(savedTagsArray?.length);
|
||||
const areSavedTagsDisabled = hasSavedTags && !isCurrentUserPremium;
|
||||
|
||||
useEffect(() => {
|
||||
if (isSavedMessages && isActive) loadSavedReactionTags();
|
||||
}, [isSavedMessages, isActive]);
|
||||
|
||||
const handleSearch = useLastCallback(() => {
|
||||
const chatId = chat?.id;
|
||||
if (!chatId) {
|
||||
return;
|
||||
}
|
||||
|
||||
runDebouncedForSearch(() => performMiddleSearch({ chatId, threadId, query }));
|
||||
});
|
||||
|
||||
const handleQueryChange = useLastCallback((newQuery: string) => {
|
||||
if (newQuery.startsWith('#') && !isHashtagQuery) {
|
||||
updateMiddleSearch({ chatId: chat!.id, threadId, update: { isHashtag: true } });
|
||||
setQuery(newQuery.slice(1));
|
||||
handleSearch();
|
||||
return;
|
||||
}
|
||||
|
||||
setQuery(newQuery);
|
||||
|
||||
if (!newQuery) {
|
||||
setIsLoading(false);
|
||||
resetMiddleSearch();
|
||||
}
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (query) {
|
||||
handleSearch();
|
||||
}
|
||||
}, [query]);
|
||||
|
||||
useEffect(() => {
|
||||
setIsLoading(Boolean(fetchingQuery));
|
||||
}, [fetchingQuery]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!foundIds?.length) return;
|
||||
const isClose = foundIds.length - focusedIndex < FOCUSED_SEARCH_TRIGGER_OFFSET;
|
||||
if (isClose) {
|
||||
handleSearch();
|
||||
}
|
||||
}, [focusedIndex, foundIds?.length]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive) return undefined;
|
||||
|
||||
maybeLongPressActiveRef.current = true;
|
||||
|
||||
function focus() {
|
||||
inputRef.current?.focus();
|
||||
markFocused();
|
||||
|
||||
fastRaf(() => {
|
||||
maybeLongPressActiveRef.current = false;
|
||||
});
|
||||
}
|
||||
|
||||
function removeListeners() {
|
||||
window.removeEventListener('touchend', focus);
|
||||
window.removeEventListener('mouseup', focus);
|
||||
|
||||
fastRaf(() => {
|
||||
maybeLongPressActiveRef.current = false;
|
||||
});
|
||||
}
|
||||
|
||||
window.addEventListener('touchend', focus);
|
||||
window.addEventListener('mouseup', focus);
|
||||
|
||||
window.addEventListener('touchstart', removeListeners);
|
||||
window.addEventListener('mousedown', removeListeners);
|
||||
|
||||
return () => {
|
||||
removeListeners();
|
||||
window.removeEventListener('touchstart', removeListeners);
|
||||
window.removeEventListener('mousedown', removeListeners);
|
||||
};
|
||||
}, [isActive]);
|
||||
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: handleClose,
|
||||
});
|
||||
|
||||
const [viewportIds, getMore, viewportOffset = 0] = useInfiniteScroll(handleSearch, foundIds);
|
||||
|
||||
const viewportResults = useMemo(() => {
|
||||
if ((!query && !savedTag) || !viewportIds?.length) {
|
||||
return MEMO_EMPTY_ARRAY;
|
||||
}
|
||||
const global = getGlobal();
|
||||
|
||||
return viewportIds.map((searchResultKey) => {
|
||||
const [chatId, id] = parseSearchResultKey(searchResultKey);
|
||||
const message = selectChatMessage(global, chatId, id);
|
||||
if (!message) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const originalSender = (isSavedMessages || chatId === REPLIES_USER_ID || chatId === ANONYMOUS_USER_ID)
|
||||
? selectForwardedSender(global, message) : undefined;
|
||||
const messageSender = selectSender(global, message);
|
||||
const messageChat = selectChat(global, message.chatId);
|
||||
|
||||
const senderPeer = originalSender || messageSender;
|
||||
|
||||
return {
|
||||
searchResultKey,
|
||||
message,
|
||||
messageChat,
|
||||
senderPeer,
|
||||
};
|
||||
}).filter(Boolean);
|
||||
}, [query, savedTag, viewportIds, isSavedMessages]);
|
||||
|
||||
const handleMessageClick = useLastCallback((message: ApiMessage) => {
|
||||
const searchResultKey = getSearchResultKey(message);
|
||||
const index = foundIds?.indexOf(searchResultKey) || 0;
|
||||
const realIndex = index + viewportOffset;
|
||||
setFocusedIndex(realIndex);
|
||||
|
||||
if (searchType === 'chat') {
|
||||
setIsViewAsList(false);
|
||||
}
|
||||
|
||||
focusMessage({
|
||||
chatId: message.chatId,
|
||||
messageId: message.id,
|
||||
threadId: !isHashtagQuery ? threadId : undefined,
|
||||
});
|
||||
|
||||
markBlurred();
|
||||
});
|
||||
|
||||
const handleTriggerViewStyle = useLastCallback(() => {
|
||||
setIsViewAsList((prev) => !prev);
|
||||
markFocused();
|
||||
});
|
||||
|
||||
const handleKeyDown = useKeyboardListNavigation(containerRef, hasResultsContainer, (index) => {
|
||||
const foundResult = viewportResults?.[index === -1 ? 0 : index];
|
||||
if (foundResult) {
|
||||
handleMessageClick(foundResult.message);
|
||||
setFocusedIndex(index + viewportOffset);
|
||||
}
|
||||
}, `.${RESULT_ITEM_CLASS_NAME}`, true);
|
||||
|
||||
const updateSearchParams = useLastCallback((update: Partial<MiddleSearchParams>) => {
|
||||
updateMiddleSearch({ chatId: chat!.id, threadId, update });
|
||||
|
||||
handleSearch();
|
||||
});
|
||||
|
||||
const activateSearchTag = useLastCallback((tag: ApiReaction) => {
|
||||
if (areSavedTagsDisabled) {
|
||||
openPremiumModal({
|
||||
initialSection: 'saved_tags',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
updateSearchParams({ savedTag: tag });
|
||||
});
|
||||
|
||||
const removeSearchSavedTag = useLastCallback(() => {
|
||||
updateSearchParams({ savedTag: undefined });
|
||||
});
|
||||
|
||||
const handleDeleteTag = useLastCallback(() => {
|
||||
if (isHashtagQuery) {
|
||||
updateSearchParams({ isHashtag: false });
|
||||
return;
|
||||
}
|
||||
|
||||
if (savedTag) {
|
||||
removeSearchSavedTag();
|
||||
}
|
||||
});
|
||||
|
||||
const handleChangeSearchType = useLastCallback((type: MiddleSearchType) => {
|
||||
updateSearchParams({ type });
|
||||
setIsViewAsList(true);
|
||||
});
|
||||
|
||||
const handleFocusOlder = useLastCallback(() => {
|
||||
if (searchType !== 'chat') return;
|
||||
markBlurred();
|
||||
blurInput();
|
||||
if (foundIds) {
|
||||
const newFocusIndex = focusedIndex + 1;
|
||||
const [chatId, messageId] = parseSearchResultKey(foundIds[newFocusIndex]);
|
||||
focusMessage({ chatId, messageId, threadId });
|
||||
setFocusedIndex(newFocusIndex);
|
||||
}
|
||||
});
|
||||
|
||||
const handleFocusNewer = useLastCallback(() => {
|
||||
if (searchType !== 'chat') return;
|
||||
markBlurred();
|
||||
blurInput();
|
||||
if (foundIds) {
|
||||
const newFocusIndex = focusedIndex - 1;
|
||||
const [chatId, messageId] = parseSearchResultKey(foundIds[newFocusIndex]);
|
||||
focusMessage({ chatId, messageId, threadId });
|
||||
setFocusedIndex(newFocusIndex);
|
||||
}
|
||||
});
|
||||
|
||||
function renderTypeTag(type: MiddleSearchType, isForTag?: boolean) {
|
||||
const isSelected = !isForTag && searchType === type;
|
||||
switch (type) {
|
||||
case 'chat':
|
||||
return (
|
||||
<PickerSelectedItem
|
||||
className={buildClassName(styles.searchType, isSelected && styles.selectedType)}
|
||||
fluid
|
||||
peerId={chat?.id}
|
||||
title={oldLang('SearchThisChat')}
|
||||
clickArg="chat"
|
||||
onClick={isForTag ? handleDeleteTag : handleChangeSearchType}
|
||||
/>
|
||||
);
|
||||
case 'myChats':
|
||||
return (
|
||||
<PickerSelectedItem
|
||||
className={buildClassName(styles.searchType, isSelected && styles.selectedType)}
|
||||
fluid
|
||||
peerId={currentUserId}
|
||||
forceShowSelf
|
||||
title={oldLang('SearchMyMessages')}
|
||||
clickArg="myChats"
|
||||
onClick={isForTag ? handleDeleteTag : handleChangeSearchType}
|
||||
/>
|
||||
);
|
||||
case 'channels':
|
||||
return (
|
||||
<PickerSelectedItem
|
||||
className={buildClassName(styles.searchType, isSelected && styles.selectedType)}
|
||||
fluid
|
||||
customPeer={CHANNELS_PEER}
|
||||
title={oldLang('SearchPublicPosts')}
|
||||
clickArg="channels"
|
||||
onClick={isForTag ? handleDeleteTag : handleChangeSearchType}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function renderDropdown() {
|
||||
return (
|
||||
<div className={buildClassName(styles.dropdown, !hasResultsDropdown && styles.dropdownHidden)}>
|
||||
{!isMobile && <div className={styles.separator} />}
|
||||
{hasSavedTags && !isHashtagQuery && (
|
||||
<div
|
||||
className={buildClassName(
|
||||
styles.savedTags,
|
||||
!isMobile && styles.wrap,
|
||||
'no-scrollbar',
|
||||
)}
|
||||
>
|
||||
{savedTagsArray.map((tag) => {
|
||||
const isChosen = isSameReaction(tag.reaction, savedTag);
|
||||
return (
|
||||
<SavedTagButton
|
||||
containerId="local-search"
|
||||
key={getReactionKey(tag.reaction)}
|
||||
reaction={tag.reaction}
|
||||
tag={tag}
|
||||
withCount
|
||||
isDisabled={areSavedTagsDisabled}
|
||||
isChosen={isChosen}
|
||||
onClick={isChosen ? removeSearchSavedTag : activateSearchTag}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
{isHashtagQuery && (
|
||||
<div
|
||||
className={buildClassName(styles.searchTypes, 'no-scrollbar')}
|
||||
>
|
||||
{renderTypeTag('chat')}
|
||||
{renderTypeTag('myChats')}
|
||||
{renderTypeTag('channels')}
|
||||
</div>
|
||||
)}
|
||||
{hasResultsContainer && (
|
||||
<InfiniteScroll
|
||||
ref={containerRef}
|
||||
className={buildClassName(styles.results, 'custom-scroll')}
|
||||
items={viewportResults}
|
||||
preloadBackwards={0}
|
||||
onLoadMore={getMore}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
{areResultsEmpty && (
|
||||
<span key="nothing" className={styles.placeholder}>
|
||||
{oldLang('NoResultFoundFor', query)}
|
||||
</span>
|
||||
)}
|
||||
{isOnlyHash && (
|
||||
<span key="enterhash" className={styles.placeholder}>
|
||||
{oldLang('HashtagSearchPlaceholder')}
|
||||
</span>
|
||||
)}
|
||||
{viewportResults?.map(({
|
||||
message, senderPeer, messageChat, searchResultKey,
|
||||
}, i) => (
|
||||
<MiddleSearchResult
|
||||
key={searchResultKey}
|
||||
teactOrderKey={-message.date}
|
||||
className={RESULT_ITEM_CLASS_NAME}
|
||||
query={query}
|
||||
message={message}
|
||||
senderPeer={senderPeer}
|
||||
messageChat={messageChat}
|
||||
shouldShowChat={isHashtagQuery}
|
||||
isActive={focusedIndex - viewportOffset === i}
|
||||
onClick={handleMessageClick}
|
||||
/>
|
||||
))}
|
||||
</InfiniteScroll>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
id="MiddleSearch"
|
||||
className={buildClassName(
|
||||
styles.root,
|
||||
isActive && styles.active,
|
||||
!isActive && isFullyHidden && 'visually-hidden', // `display: none` would prevent focus on iOS
|
||||
isFocused && styles.focused,
|
||||
isMobile && styles.mobile,
|
||||
)}
|
||||
ref={ref}
|
||||
>
|
||||
<div className={styles.header}>
|
||||
{!isMobile && (
|
||||
<Avatar
|
||||
className={styles.avatar}
|
||||
peer={chat}
|
||||
size="medium"
|
||||
isSavedMessages={isSavedMessages}
|
||||
/>
|
||||
)}
|
||||
<SearchInput
|
||||
ref={inputRef}
|
||||
value={query}
|
||||
className={buildClassName(
|
||||
styles.input,
|
||||
hasResultsDropdown && styles.withDropdown,
|
||||
hasResultsDropdown && !isMobile && styles.adaptSearchBorders,
|
||||
)}
|
||||
canClose={!isMobile}
|
||||
isLoading={isLoading}
|
||||
resultsItemSelector={`.${styles.results} .${RESULT_ITEM_CLASS_NAME}`}
|
||||
hasUpButton={hasNavigationButtons && !isMobile}
|
||||
hasDownButton={hasNavigationButtons && !isMobile}
|
||||
placeholder={isHashtagQuery ? oldLang('SearchHashtagsHint') : oldLang('Search')}
|
||||
teactExperimentControlled
|
||||
onChange={handleQueryChange}
|
||||
onStartBackspace={handleDeleteTag}
|
||||
onReset={handleReset}
|
||||
withBackIcon={isMobile}
|
||||
onFocus={markFocused}
|
||||
focused={isFocused}
|
||||
onUpClick={canFocusOlder ? handleFocusOlder : undefined}
|
||||
onDownClick={canFocusNewer ? handleFocusNewer : undefined}
|
||||
>
|
||||
<div className={styles.searchTags}>
|
||||
{savedTag && (
|
||||
<SavedTagButton
|
||||
containerId="local-search-tags"
|
||||
className={styles.savedSearchTag}
|
||||
reaction={savedTag}
|
||||
tag={savedTags![getReactionKey(savedTag)]}
|
||||
onClick={removeSearchSavedTag}
|
||||
/>
|
||||
)}
|
||||
{isHashtagQuery && <div className={styles.hash}>#</div>}
|
||||
</div>
|
||||
{!isMobile && renderDropdown()}
|
||||
</SearchInput>
|
||||
{!isMobile && (
|
||||
<div className={styles.icons}>
|
||||
<Button
|
||||
round
|
||||
size="smaller"
|
||||
color="translucent"
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => openHistoryCalendar({ selectedAt: getDayStartAt(Date.now()) })}
|
||||
ariaLabel={oldLang('JumpToDate')}
|
||||
>
|
||||
<Icon name="calendar" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{isMobile && renderDropdown()}
|
||||
{isMobile && (
|
||||
<div className={styles.footer}>
|
||||
<Button
|
||||
round
|
||||
size="smaller"
|
||||
color="translucent"
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => openHistoryCalendar({ selectedAt: getDayStartAt(Date.now()) })}
|
||||
ariaLabel={oldLang('JumpToDate')}
|
||||
>
|
||||
<Icon name="calendar" />
|
||||
</Button>
|
||||
<div className={styles.counter}>
|
||||
{hasQueryData && (
|
||||
foundIds?.length ? (
|
||||
oldLang('Of', [focusedIndex + 1, totalCount])
|
||||
) : foundIds && !foundIds.length && (
|
||||
oldLang('NoResult')
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
{searchType === 'chat' && Boolean(foundIds?.length) && (
|
||||
<Button
|
||||
className={styles.viewStyle}
|
||||
size="smaller"
|
||||
isText
|
||||
fluid
|
||||
noForcedUpperCase
|
||||
onClick={handleTriggerViewStyle}
|
||||
>
|
||||
{isViewAsList ? oldLang('SearchAsChat') : oldLang('SearchAsList')}
|
||||
</Button>
|
||||
)}
|
||||
{hasNavigationButtons && !hasResultsDropdown && (
|
||||
<div className={styles.mobileNavigation}>
|
||||
<Button
|
||||
className={buildClassName(styles.navigationButton, !canFocusOlder && styles.navigationDisabled)}
|
||||
round
|
||||
size="smaller"
|
||||
color="secondary"
|
||||
onClick={handleFocusOlder}
|
||||
nonInteractive={!canFocusOlder}
|
||||
ariaLabel={lang('AriaSearchOlderResult')}
|
||||
>
|
||||
<Icon name="up" />
|
||||
</Button>
|
||||
<Button
|
||||
className={buildClassName(styles.navigationButton, !canFocusNewer && styles.navigationDisabled)}
|
||||
round
|
||||
size="smaller"
|
||||
color="secondary"
|
||||
onClick={handleFocusNewer}
|
||||
nonInteractive={!canFocusNewer}
|
||||
ariaLabel={lang('AriaSearchNewerResult')}
|
||||
>
|
||||
<Icon name="down" />
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global): StateProps => {
|
||||
const currentMessageList = selectCurrentMessageList(global);
|
||||
if (!currentMessageList) {
|
||||
return {};
|
||||
}
|
||||
const { chatId, threadId } = currentMessageList;
|
||||
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const {
|
||||
requestedQuery, savedTag, results, fetchingQuery, isHashtag, type,
|
||||
} = selectCurrentMiddleSearch(global) || {};
|
||||
const { totalCount, foundIds, query: lastSearchQuery } = results || {};
|
||||
|
||||
const currentUserId = global.currentUserId;
|
||||
const isSavedMessages = selectIsChatWithSelf(global, chatId);
|
||||
const isSavedDialog = getIsSavedDialog(chatId, threadId, currentUserId);
|
||||
|
||||
const savedTags = isSavedMessages && !isSavedDialog ? global.savedReactionTags?.byKey : undefined;
|
||||
|
||||
return {
|
||||
chat,
|
||||
requestedQuery,
|
||||
totalCount,
|
||||
threadId,
|
||||
foundIds,
|
||||
isHistoryCalendarOpen: Boolean(selectTabState(global).historyCalendarSelectedAt),
|
||||
savedTags,
|
||||
savedTag,
|
||||
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
||||
isSavedMessages,
|
||||
fetchingQuery,
|
||||
isHashtagQuery: isHashtag,
|
||||
currentUserId,
|
||||
searchType: type,
|
||||
lastSearchQuery,
|
||||
};
|
||||
},
|
||||
)(MiddleSearch));
|
||||
57
src/components/middle/search/MiddleSearchResult.module.scss
Normal file
57
src/components/middle/search/MiddleSearchResult.module.scss
Normal file
@ -0,0 +1,57 @@
|
||||
.root {
|
||||
display: flex;
|
||||
gap: 0.75rem;
|
||||
padding: 0.375rem 0.75rem;
|
||||
border-radius: 0.625rem;
|
||||
outline: none;
|
||||
|
||||
color: var(--color-text);
|
||||
font-size: 0.875rem;
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
|
||||
@media (hover: hover) {
|
||||
&:hover,
|
||||
&:focus-visible,
|
||||
&.active {
|
||||
background-color: var(--color-chat-hover);
|
||||
}
|
||||
}
|
||||
|
||||
:global {
|
||||
.matching-text-highlight {
|
||||
color: var(--color-text);
|
||||
background: #cae3f7;
|
||||
border-radius: 0.25rem;
|
||||
padding: 0 0.125rem;
|
||||
display: inline-block;
|
||||
|
||||
:global(.theme-dark) & {
|
||||
--color-text: #000;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex-grow: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.topRow {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.meta {
|
||||
margin-inline-start: auto;
|
||||
padding-inline-start: 0.5rem;
|
||||
}
|
||||
|
||||
.subtitle {
|
||||
color: var(--color-text-secondary);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
line-height: 1.25;
|
||||
}
|
||||
85
src/components/middle/search/MiddleSearchResult.tsx
Normal file
85
src/components/middle/search/MiddleSearchResult.tsx
Normal file
@ -0,0 +1,85 @@
|
||||
import React, { memo } from '../../../lib/teact/teact';
|
||||
|
||||
import type { ApiChat, ApiMessage, ApiPeer } from '../../../api/types';
|
||||
|
||||
import { getMessageSenderName } from '../../../global/helpers';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import Avatar from '../../common/Avatar';
|
||||
import FullNameTitle from '../../common/FullNameTitle';
|
||||
import LastMessageMeta from '../../common/LastMessageMeta';
|
||||
import MessageSummary from '../../common/MessageSummary';
|
||||
|
||||
import styles from './MiddleSearchResult.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
isActive?: boolean;
|
||||
message: ApiMessage;
|
||||
senderPeer?: ApiPeer;
|
||||
messageChat?: ApiChat;
|
||||
shouldShowChat?: boolean;
|
||||
query?: string;
|
||||
className?: string;
|
||||
onClick: (message: ApiMessage) => void;
|
||||
};
|
||||
|
||||
const TRUNCATE_LENGTH = 200;
|
||||
|
||||
const MiddleSearchResult = ({
|
||||
isActive,
|
||||
message,
|
||||
senderPeer,
|
||||
messageChat,
|
||||
shouldShowChat,
|
||||
query,
|
||||
className,
|
||||
onClick,
|
||||
}: OwnProps) => {
|
||||
const lang = useOldLang();
|
||||
const hiddenForwardTitle = message.forwardInfo?.hiddenUserName;
|
||||
|
||||
const peer = shouldShowChat ? messageChat : senderPeer;
|
||||
|
||||
const senderName = shouldShowChat ? getMessageSenderName(lang, message.chatId, senderPeer) : undefined;
|
||||
|
||||
const handleClick = useLastCallback(() => {
|
||||
onClick(message);
|
||||
});
|
||||
|
||||
return (
|
||||
<div
|
||||
role="button"
|
||||
tabIndex={0}
|
||||
className={buildClassName(styles.root, isActive && styles.active, className)}
|
||||
onClick={handleClick}
|
||||
>
|
||||
<Avatar
|
||||
className={styles.avatar}
|
||||
peer={peer}
|
||||
text={hiddenForwardTitle}
|
||||
size="medium"
|
||||
/>
|
||||
<div className={styles.info}>
|
||||
<div className={styles.topRow}>
|
||||
{(peer && <FullNameTitle peer={peer} withEmojiStatus />) || hiddenForwardTitle}
|
||||
<LastMessageMeta className={styles.meta} message={message} />
|
||||
</div>
|
||||
<div className={styles.subtitle} dir="auto">
|
||||
{senderName && (
|
||||
<>
|
||||
<span className="sender-name">{renderText(senderName)}</span>
|
||||
<span className="colon">:</span>
|
||||
</>
|
||||
)}
|
||||
<MessageSummary message={message} highlight={query} truncateLength={TRUNCATE_LENGTH} />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(MiddleSearchResult);
|
||||
@ -128,6 +128,7 @@ const CollectibleInfoModal: FC<OwnProps & StateProps> = ({
|
||||
className={styles.chip}
|
||||
peerId={modal?.peerId}
|
||||
forceShowSelf
|
||||
clickArg={modal?.peerId}
|
||||
onClick={handleOpenChat}
|
||||
/>
|
||||
<p className={styles.description}>
|
||||
|
||||
@ -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}
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -1,19 +0,0 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React from '../../lib/teact/teact';
|
||||
|
||||
import type { OwnProps } from './RightSearch';
|
||||
|
||||
import { Bundles } from '../../util/moduleLoader';
|
||||
|
||||
import useModuleLoader from '../../hooks/useModuleLoader';
|
||||
|
||||
import Loading from '../ui/Loading';
|
||||
|
||||
const RightSearchAsync: FC<OwnProps> = (props) => {
|
||||
const RightSearch = useModuleLoader(Bundles.Extra, 'RightSearch');
|
||||
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
return RightSearch ? <RightSearch {...props} /> : <Loading />;
|
||||
};
|
||||
|
||||
export default RightSearchAsync;
|
||||
@ -1,29 +0,0 @@
|
||||
.RightSearch {
|
||||
height: 100%;
|
||||
padding: 0 0.5rem;
|
||||
overflow-y: auto;
|
||||
|
||||
.helper-text {
|
||||
padding: 1rem;
|
||||
margin-bottom: 0.125rem;
|
||||
font-weight: 500;
|
||||
color: var(--color-text-secondary);
|
||||
unicode-bidi: plaintext;
|
||||
text-align: initial;
|
||||
}
|
||||
|
||||
.search-tags {
|
||||
--color-reaction: var(--color-background-secondary);
|
||||
--hover-color-reaction: var(--color-background-secondary-accent);
|
||||
--text-color-reaction: var(--color-text-secondary);
|
||||
--color-reaction-chosen: var(--color-primary);
|
||||
--text-color-reaction-chosen: #FFFFFF;
|
||||
--hover-color-reaction-chosen: var(--color-primary-shade);
|
||||
|
||||
display: flex;
|
||||
overflow-x: scroll;
|
||||
|
||||
margin-top: 0.25rem;
|
||||
gap: 0.375rem;
|
||||
}
|
||||
}
|
||||
@ -1,290 +0,0 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useEffect, useMemo, useRef,
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions, getGlobal, withGlobal } from '../../global';
|
||||
|
||||
import type {
|
||||
ApiMessage, ApiPeer, ApiReaction, ApiReactionKey, ApiSavedReactionTag,
|
||||
} from '../../api/types';
|
||||
import type { ThreadId } from '../../types';
|
||||
|
||||
import { ANONYMOUS_USER_ID, REPLIES_USER_ID } from '../../config';
|
||||
import { getIsSavedDialog, getReactionKey, isSameReaction } from '../../global/helpers';
|
||||
import {
|
||||
selectChatMessages,
|
||||
selectCurrentTextSearch,
|
||||
selectForwardedSender,
|
||||
selectIsChatWithSelf,
|
||||
selectIsCurrentUserPremium,
|
||||
selectSender,
|
||||
} from '../../global/selectors';
|
||||
import { disableDirectTextInput, enableDirectTextInput } from '../../util/directInputManager';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../util/memo';
|
||||
import { debounce } from '../../util/schedulers';
|
||||
import { renderMessageSummary } from '../common/helpers/renderMessageText';
|
||||
|
||||
import useHistoryBack from '../../hooks/useHistoryBack';
|
||||
import useHorizontalScroll from '../../hooks/useHorizontalScroll';
|
||||
import useInfiniteScroll from '../../hooks/useInfiniteScroll';
|
||||
import useKeyboardListNavigation from '../../hooks/useKeyboardListNavigation';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
|
||||
import Avatar from '../common/Avatar';
|
||||
import FullNameTitle from '../common/FullNameTitle';
|
||||
import LastMessageMeta from '../common/LastMessageMeta';
|
||||
import SavedTagButton from '../middle/message/reactions/SavedTagButton';
|
||||
import InfiniteScroll from '../ui/InfiniteScroll';
|
||||
import ListItem from '../ui/ListItem';
|
||||
|
||||
import './RightSearch.scss';
|
||||
|
||||
export type OwnProps = {
|
||||
chatId: string;
|
||||
threadId: ThreadId;
|
||||
onClose: NoneToVoidFunction;
|
||||
isActive: boolean;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
messagesById?: Record<number, ApiMessage>;
|
||||
query?: string;
|
||||
savedTags?: Record<ApiReactionKey, ApiSavedReactionTag>;
|
||||
searchTag?: ApiReaction;
|
||||
totalCount?: number;
|
||||
foundIds?: number[];
|
||||
isSavedMessages?: boolean;
|
||||
isCurrentUserPremium?: boolean;
|
||||
};
|
||||
|
||||
const runDebouncedForSearch = debounce((cb) => cb(), 200, false);
|
||||
|
||||
const RightSearch: FC<OwnProps & StateProps> = ({
|
||||
chatId,
|
||||
threadId,
|
||||
isActive,
|
||||
messagesById,
|
||||
query,
|
||||
totalCount,
|
||||
foundIds,
|
||||
savedTags,
|
||||
searchTag,
|
||||
isSavedMessages,
|
||||
isCurrentUserPremium,
|
||||
onClose,
|
||||
}) => {
|
||||
const {
|
||||
searchTextMessagesLocal,
|
||||
setLocalTextSearchTag,
|
||||
focusMessage,
|
||||
openPremiumModal,
|
||||
loadSavedReactionTags,
|
||||
} = getActions();
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const tagsRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const lang = useOldLang();
|
||||
useHistoryBack({
|
||||
isActive,
|
||||
onBack: onClose,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
disableDirectTextInput();
|
||||
|
||||
return enableDirectTextInput;
|
||||
}, [isActive]);
|
||||
|
||||
const tags = useMemo(() => {
|
||||
if (!savedTags) return undefined;
|
||||
return Object.values(savedTags);
|
||||
}, [savedTags]);
|
||||
|
||||
const hasTags = Boolean(tags?.length);
|
||||
const areTagsDisabled = hasTags && !isCurrentUserPremium;
|
||||
|
||||
useHorizontalScroll(tagsRef, !hasTags);
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive) loadSavedReactionTags();
|
||||
}, [hasTags, isActive]);
|
||||
|
||||
const handleSearchTextMessagesLocal = useLastCallback(() => {
|
||||
runDebouncedForSearch(searchTextMessagesLocal);
|
||||
});
|
||||
|
||||
const handleTagClick = useLastCallback((tag: ApiReaction) => {
|
||||
if (areTagsDisabled) {
|
||||
openPremiumModal({
|
||||
initialSection: 'saved_tags',
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (isSameReaction(tag, searchTag)) {
|
||||
setLocalTextSearchTag({ tag: undefined });
|
||||
return;
|
||||
}
|
||||
|
||||
setLocalTextSearchTag({ tag });
|
||||
handleSearchTextMessagesLocal();
|
||||
});
|
||||
|
||||
const [viewportIds, getMore] = useInfiniteScroll(handleSearchTextMessagesLocal, foundIds);
|
||||
|
||||
const viewportResults = useMemo(() => {
|
||||
if ((!query && !searchTag) || !viewportIds?.length || !messagesById) {
|
||||
return MEMO_EMPTY_ARRAY;
|
||||
}
|
||||
|
||||
return viewportIds.map((id) => {
|
||||
const message = messagesById[id];
|
||||
if (!message) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const global = getGlobal();
|
||||
|
||||
const originalSender = (isSavedMessages || chatId === REPLIES_USER_ID || chatId === ANONYMOUS_USER_ID)
|
||||
? selectForwardedSender(global, message) : undefined;
|
||||
const messageSender = selectSender(global, message);
|
||||
|
||||
const senderPeer = originalSender || messageSender;
|
||||
|
||||
const hiddenForwardTitle = message.forwardInfo?.hiddenUserName;
|
||||
|
||||
return {
|
||||
message,
|
||||
senderPeer,
|
||||
hiddenForwardTitle,
|
||||
onClick: () => focusMessage({ chatId, threadId, messageId: id }),
|
||||
};
|
||||
}).filter(Boolean);
|
||||
}, [query, searchTag, viewportIds, messagesById, isSavedMessages, chatId, threadId]);
|
||||
|
||||
const handleKeyDown = useKeyboardListNavigation(containerRef, true, (index) => {
|
||||
const foundResult = viewportResults?.[index === -1 ? 0 : index];
|
||||
if (foundResult) {
|
||||
foundResult.onClick();
|
||||
}
|
||||
}, '.ListItem-button', true);
|
||||
|
||||
const renderSearchResult = ({
|
||||
message, senderPeer, hiddenForwardTitle, onClick,
|
||||
}: {
|
||||
message: ApiMessage;
|
||||
senderPeer?: ApiPeer;
|
||||
hiddenForwardTitle?: string;
|
||||
onClick: NoneToVoidFunction;
|
||||
}) => {
|
||||
const text = renderMessageSummary(lang, message, undefined, query);
|
||||
|
||||
return (
|
||||
<ListItem
|
||||
key={message.id}
|
||||
teactOrderKey={-message.date}
|
||||
className="chat-item-clickable search-result-message m-0"
|
||||
onClick={onClick}
|
||||
>
|
||||
<Avatar
|
||||
peer={senderPeer}
|
||||
text={hiddenForwardTitle}
|
||||
/>
|
||||
<div className="info">
|
||||
<div className="search-result-message-top">
|
||||
{senderPeer && <FullNameTitle peer={senderPeer} withEmojiStatus />}
|
||||
{!senderPeer && hiddenForwardTitle}
|
||||
<LastMessageMeta message={message} />
|
||||
</div>
|
||||
<div className="subtitle" dir="auto">
|
||||
{text}
|
||||
</div>
|
||||
</div>
|
||||
</ListItem>
|
||||
);
|
||||
};
|
||||
|
||||
const isOnTop = viewportIds?.[0] === foundIds?.[0];
|
||||
|
||||
return (
|
||||
<InfiniteScroll
|
||||
ref={containerRef}
|
||||
className="RightSearch custom-scroll"
|
||||
items={viewportResults}
|
||||
preloadBackwards={0}
|
||||
onLoadMore={getMore}
|
||||
onKeyDown={handleKeyDown}
|
||||
>
|
||||
{hasTags && (
|
||||
<div
|
||||
ref={tagsRef}
|
||||
className="search-tags custom-scroll-x no-scrollbar"
|
||||
key="search-tags"
|
||||
>
|
||||
{tags.map((tag) => (
|
||||
<SavedTagButton
|
||||
containerId="local-search"
|
||||
key={getReactionKey(tag.reaction)}
|
||||
reaction={tag.reaction}
|
||||
tag={tag}
|
||||
withCount
|
||||
isDisabled={areTagsDisabled}
|
||||
isChosen={isSameReaction(tag.reaction, searchTag)}
|
||||
onClick={handleTagClick}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
{isOnTop && (
|
||||
<p key="helper-text" className="helper-text" dir="auto">
|
||||
{!query ? (
|
||||
lang('lng_dlg_search_for_messages')
|
||||
) : (totalCount === 0 || !viewportResults.length) ? (
|
||||
lang('lng_search_no_results')
|
||||
) : totalCount === 1 ? (
|
||||
'1 message found'
|
||||
) : (
|
||||
`${(viewportResults.length && (totalCount || viewportResults.length))} messages found`
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{viewportResults.map(renderSearchResult)}
|
||||
</InfiniteScroll>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { chatId, threadId }): StateProps => {
|
||||
const messagesById = selectChatMessages(global, chatId);
|
||||
if (!messagesById) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const { query, savedTag, results } = selectCurrentTextSearch(global) || {};
|
||||
const { totalCount, foundIds } = results || {};
|
||||
|
||||
const isSavedMessages = selectIsChatWithSelf(global, chatId);
|
||||
const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
|
||||
|
||||
const savedTags = isSavedMessages && !isSavedDialog ? global.savedReactionTags?.byKey : undefined;
|
||||
|
||||
return {
|
||||
messagesById,
|
||||
query,
|
||||
totalCount,
|
||||
foundIds,
|
||||
isSavedMessages,
|
||||
savedTags,
|
||||
searchTag: savedTag,
|
||||
isCurrentUserPremium: selectIsCurrentUserPremium(global),
|
||||
};
|
||||
},
|
||||
)(RightSearch));
|
||||
@ -324,7 +324,10 @@ function Story({
|
||||
onMouseLeave: handleLongPressMouseLeave,
|
||||
onTouchStart: handleLongPressTouchStart,
|
||||
onTouchEnd: handleLongPressTouchEnd,
|
||||
} = useLongPress(handleLongPressStart, handleLongPressEnd);
|
||||
} = useLongPress({
|
||||
onStart: handleLongPressStart,
|
||||
onEnd: handleLongPressEnd,
|
||||
});
|
||||
|
||||
const isUnsupported = useUnsupportedMedia(
|
||||
videoRef,
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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();
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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];
|
||||
|
||||
@ -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,
|
||||
);
|
||||
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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,
|
||||
@ -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 {
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -1,93 +0,0 @@
|
||||
import type { ActionReturnType, GlobalState, TabArgs } from '../../types';
|
||||
|
||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
|
||||
import { buildChatThreadKey, isSameReaction } from '../../helpers';
|
||||
import { addActionHandler } from '../../index';
|
||||
import {
|
||||
replaceLocalTextSearchResults,
|
||||
updateLocalTextSearch,
|
||||
updateLocalTextSearchTag,
|
||||
updateSharedMediaSearchType,
|
||||
} from '../../reducers';
|
||||
import { selectCurrentMessageList, selectTabState } from '../../selectors';
|
||||
|
||||
addActionHandler('openLocalTextSearch', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
|
||||
if (!chatId || !threadId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return updateLocalTextSearch(global, chatId, threadId, '', tabId);
|
||||
});
|
||||
|
||||
addActionHandler('closeLocalTextSearch', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
return closeLocalTextSearch(global, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('setLocalTextSearchQuery', (global, actions, payload): ActionReturnType => {
|
||||
const { query, tabId = getCurrentTabId() } = payload!;
|
||||
|
||||
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
|
||||
if (!chatId || !threadId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const chatThreadKey = buildChatThreadKey(chatId, threadId);
|
||||
const { query: currentQuery } = selectTabState(global, tabId).localTextSearch.byChatThreadKey[chatThreadKey] || {};
|
||||
|
||||
if (query !== currentQuery) {
|
||||
global = replaceLocalTextSearchResults(global, chatId, threadId, MEMO_EMPTY_ARRAY, undefined, undefined, tabId);
|
||||
}
|
||||
|
||||
global = updateLocalTextSearch(global, chatId, threadId, query, tabId);
|
||||
|
||||
return global;
|
||||
});
|
||||
|
||||
addActionHandler('setLocalTextSearchTag', (global, actions, payload): ActionReturnType => {
|
||||
const { tag, tabId = getCurrentTabId() } = payload!;
|
||||
|
||||
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
|
||||
if (!chatId || !threadId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const chatThreadKey = buildChatThreadKey(chatId, threadId);
|
||||
const { savedTag } = selectTabState(global, tabId).localTextSearch.byChatThreadKey[chatThreadKey] || {};
|
||||
|
||||
if (!isSameReaction(tag, savedTag)) {
|
||||
global = replaceLocalTextSearchResults(global, chatId, threadId, MEMO_EMPTY_ARRAY, undefined, undefined, tabId);
|
||||
}
|
||||
|
||||
global = updateLocalTextSearchTag(global, chatId, threadId, tag, tabId);
|
||||
|
||||
return global;
|
||||
});
|
||||
|
||||
addActionHandler('setSharedMediaSearchType', (global, actions, payload): ActionReturnType => {
|
||||
const { mediaType, tabId = getCurrentTabId() } = payload;
|
||||
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
|
||||
if (!chatId || !threadId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return updateSharedMediaSearchType(global, chatId, threadId, mediaType, tabId);
|
||||
});
|
||||
|
||||
export function closeLocalTextSearch<T extends GlobalState>(
|
||||
global: T,
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
): T {
|
||||
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
|
||||
if (!chatId || !threadId) {
|
||||
return global;
|
||||
}
|
||||
|
||||
global = updateLocalTextSearchTag(global, chatId, threadId, undefined, tabId);
|
||||
global = updateLocalTextSearch(global, chatId, threadId, undefined, tabId);
|
||||
global = replaceLocalTextSearchResults(global, chatId, threadId, undefined, undefined, undefined, tabId);
|
||||
return global;
|
||||
}
|
||||
76
src/global/actions/ui/middleSearch.ts
Normal file
76
src/global/actions/ui/middleSearch.ts
Normal file
@ -0,0 +1,76 @@
|
||||
import type { ActionReturnType } from '../../types';
|
||||
import { MAIN_THREAD_ID } from '../../../api/types';
|
||||
|
||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||
import { addActionHandler } from '../../index';
|
||||
import {
|
||||
closeMiddleSearch,
|
||||
resetMiddleSearch,
|
||||
updateMiddleSearch,
|
||||
updateSharedMediaSearchType,
|
||||
} from '../../reducers';
|
||||
import { selectCurrentMessageList } from '../../selectors';
|
||||
|
||||
addActionHandler('openMiddleSearch', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
|
||||
if (!chatId || !threadId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return updateMiddleSearch(global, chatId, threadId, {}, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('closeMiddleSearch', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
|
||||
if (!chatId || !threadId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return closeMiddleSearch(global, chatId, threadId, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('updateMiddleSearch', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
update, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
|
||||
let chatId;
|
||||
let threadId;
|
||||
if (payload.chatId) {
|
||||
chatId = payload.chatId;
|
||||
threadId = payload.threadId || MAIN_THREAD_ID;
|
||||
} else {
|
||||
const currentMessageList = selectCurrentMessageList(global, tabId);
|
||||
if (!currentMessageList) {
|
||||
return undefined;
|
||||
}
|
||||
chatId = currentMessageList.chatId;
|
||||
threadId = currentMessageList.threadId;
|
||||
}
|
||||
|
||||
global = updateMiddleSearch(global, chatId, threadId, update, tabId);
|
||||
|
||||
return global;
|
||||
});
|
||||
|
||||
addActionHandler('resetMiddleSearch', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
|
||||
if (!chatId || !threadId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return resetMiddleSearch(global, chatId, threadId, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('setSharedMediaSearchType', (global, actions, payload): ActionReturnType => {
|
||||
const { mediaType, tabId = getCurrentTabId() } = payload;
|
||||
const { chatId, threadId } = selectCurrentMessageList(global, tabId) || {};
|
||||
if (!chatId || !threadId) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return updateSharedMediaSearchType(global, chatId, threadId, mediaType, tabId);
|
||||
});
|
||||
@ -252,6 +252,10 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
|
||||
if (!cached.quickReplies) {
|
||||
cached.quickReplies = initialState.quickReplies;
|
||||
}
|
||||
|
||||
if (!cached.chats.loadingParameters) {
|
||||
cached.chats.loadingParameters = initialState.chats.loadingParameters;
|
||||
}
|
||||
}
|
||||
|
||||
function updateCache(force?: boolean) {
|
||||
|
||||
@ -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');
|
||||
}
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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: {},
|
||||
},
|
||||
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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,
|
||||
},
|
||||
},
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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';
|
||||
|
||||
|
||||
@ -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>(
|
||||
@ -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) {
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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>(
|
||||
@ -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 ? (
|
||||
|
||||
@ -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;
|
||||
|
||||
22
src/hooks/events/useOutsideClick.ts
Normal file
22
src/hooks/events/useOutsideClick.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import { useEffect } from '../../lib/teact/teact';
|
||||
|
||||
import useLastCallback from '../useLastCallback';
|
||||
|
||||
export function useClickOutside(
|
||||
refs: React.RefObject<HTMLElement>[], callback: (event: MouseEvent) => void,
|
||||
) {
|
||||
const handleClickOutside = useLastCallback((event: MouseEvent) => {
|
||||
const clickedOutside = refs.every((ref) => {
|
||||
return ref.current && !ref.current.contains(event.target as Node);
|
||||
});
|
||||
|
||||
if (clickedOutside) callback(event);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
document.addEventListener('click', handleClickOutside);
|
||||
return () => {
|
||||
document.removeEventListener('click', handleClickOutside);
|
||||
};
|
||||
}, [handleClickOutside]);
|
||||
}
|
||||
@ -17,19 +17,20 @@ const useInfiniteScroll = <ListId extends string | number>(
|
||||
listIds?: ListId[],
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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 () => {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -224,6 +224,7 @@
|
||||
"channels.getSponsoredMessages",
|
||||
"channels.getChannelRecommendations",
|
||||
"channels.reportSponsoredMessage",
|
||||
"channels.searchPosts",
|
||||
"bots.canSendMessage",
|
||||
"bots.allowSendMessage",
|
||||
"bots.invokeWebViewCustomMethod",
|
||||
|
||||
@ -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');
|
||||
}
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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';
|
||||
|
||||
6
src/types/language.d.ts
vendored
6
src/types/language.d.ts
vendored
@ -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
Loading…
x
Reference in New Issue
Block a user