From 09323114d58231c415e36ed2246e548aaaa12ecc Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Fri, 15 Aug 2025 18:25:31 +0200 Subject: [PATCH] Global Search: Support search public posts (#6114) --- CLAUDE.md | 10 + src/api/gramjs/apiBuilders/messages.ts | 15 ++ src/api/gramjs/apiBuilders/payments.ts | 3 +- src/api/gramjs/methods/messages.ts | 44 +++- src/api/types/messages.ts | 12 +- src/api/types/stars.ts | 1 + src/assets/localization/fallback.strings | 19 ++ src/assets/tgs-previews/DuckNothingFound.svg | 1 + src/assets/tgs-previews/Search.svg | 1 + src/assets/tgs/DuckNothingFound.tgs | Bin 0 -> 8590 bytes src/assets/tgs/Search.tgs | Bin 0 -> 12475 bytes src/components/common/NothingFound.scss | 7 +- src/components/common/NothingFound.tsx | 23 +- .../common/helpers/animatedAssets.ts | 11 + src/components/left/LeftColumn.tsx | 7 +- src/components/left/main/LeftMainHeader.tsx | 14 +- src/components/left/search/AudioResults.tsx | 1 + src/components/left/search/BotAppResults.tsx | 1 + .../left/search/ChatMessageResults.tsx | 1 + src/components/left/search/ChatResults.tsx | 1 + src/components/left/search/FileResults.tsx | 1 + src/components/left/search/LeftSearch.tsx | 17 ++ src/components/left/search/LinkResults.tsx | 1 + src/components/left/search/MediaResults.tsx | 1 + .../left/search/PublicPostsResults.tsx | 163 ++++++++++++ .../PublicPostsSearchLauncher.module.scss | 129 ++++++++++ .../left/search/PublicPostsSearchLauncher.tsx | 237 ++++++++++++++++++ .../modals/stars/helpers/transaction.ts | 9 +- .../transaction/StarsTransactionItem.tsx | 16 +- .../transaction/StarsTransactionModal.tsx | 25 +- src/components/ui/Button.scss | 2 +- src/components/ui/SearchInput.tsx | 20 +- src/components/ui/TextTimer.tsx | 32 ++- src/config.ts | 4 + src/global/actions/api/globalSearch.ts | 51 +++- src/global/actions/api/middleSearch.ts | 2 +- src/global/actions/ui/globalSearch.ts | 5 +- src/global/helpers/payments.ts | 21 +- src/global/reducers/globalSearch.ts | 17 +- src/global/types/actions.ts | 3 + src/global/types/tabState.ts | 3 + src/lib/gramjs/tl/api.d.ts | 2 + src/lib/gramjs/tl/apiTl.ts | 3 +- src/lib/gramjs/tl/static/api.json | 1 + src/lib/gramjs/tl/static/api.tl | 2 +- src/types/index.ts | 1 + src/types/language.d.ts | 28 +++ 47 files changed, 918 insertions(+), 50 deletions(-) create mode 100644 src/assets/tgs-previews/DuckNothingFound.svg create mode 100644 src/assets/tgs-previews/Search.svg create mode 100644 src/assets/tgs/DuckNothingFound.tgs create mode 100644 src/assets/tgs/Search.tgs create mode 100644 src/components/left/search/PublicPostsResults.tsx create mode 100644 src/components/left/search/PublicPostsSearchLauncher.module.scss create mode 100644 src/components/left/search/PublicPostsSearchLauncher.tsx diff --git a/CLAUDE.md b/CLAUDE.md index 440df53d3..68ab89c9a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,6 +51,16 @@ You are an expert in TypeScript, JavaScript, HTML, SCSS and Teact with deep expe style={{ transform: `translateX(${value}%)` }} style={{ '--custom-prop': value } as React.CSSProperties} ``` + - **IMPORTANT: Font weights in CSS** - Always use existing CSS variables for font-weight. Never use numeric values or custom values. + ```scss + // ✅ CORRECT + font-weight: var(--font-weight-medium); + font-weight: var(--font-weight-bold); + + // ❌ WRONG + font-weight: 600; + font-weight: bold; + ``` - **Localization & Text Rules:** - **ALWAYS** use `lang()` for all text content - never hardcode strings. diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index 6ca778ce3..b4f21b072 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -22,6 +22,7 @@ import type { ApiPreparedInlineMessage, ApiQuickReply, ApiReplyInfo, + ApiSearchPostsFlood, ApiSponsoredMessage, ApiSticker, ApiStory, @@ -820,3 +821,17 @@ export function buildPreparedInlineMessage( cacheTime: result.cacheTime, }; } + +export function buildApiSearchPostsFlood( + searchFlood: GramJs.SearchPostsFlood, + query?: string, +): ApiSearchPostsFlood { + return { + query, + queryIsFree: searchFlood.queryIsFree, + totalDaily: searchFlood.totalDaily, + remains: searchFlood.remains, + waitTill: searchFlood.waitTill, + starsAmount: searchFlood.starsAmount.toJSNumber(), + }; +} diff --git a/src/api/gramjs/apiBuilders/payments.ts b/src/api/gramjs/apiBuilders/payments.ts index 85f42abe6..9812162ae 100644 --- a/src/api/gramjs/apiBuilders/payments.ts +++ b/src/api/gramjs/apiBuilders/payments.ts @@ -549,7 +549,7 @@ export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction): const { date, id, peer, amount, description, photo, title, refund, extendedMedia, failed, msgId, pending, gift, reaction, subscriptionPeriod, stargift, giveawayPostId, starrefCommissionPermille, stargiftUpgrade, paidMessages, - stargiftResale, + stargiftResale, postsSearch, } = transaction; if (photo) { @@ -588,6 +588,7 @@ export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction): isGiftUpgrade: stargiftUpgrade, isGiftResale: stargiftResale, paidMessages, + isPostsSearch: postsSearch, }; } diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index 76a29de4c..942f814f5 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -24,6 +24,7 @@ import type { ApiPeer, ApiPoll, ApiReaction, + ApiSearchPostsFlood, ApiSendMessageAction, ApiTodoItem, ApiUser, @@ -66,6 +67,7 @@ import { buildApiMessage, buildApiQuickReply, buildApiReportResult, + buildApiSearchPostsFlood, buildApiSponsoredMessage, buildApiThreadInfo, buildLocalForwardedMessage, @@ -128,6 +130,7 @@ type SearchResults = { nextOffsetRate?: number; nextOffsetPeerId?: string; nextOffsetId?: number; + searchFlood?: ApiSearchPostsFlood; }; export async function fetchMessages({ @@ -1537,6 +1540,16 @@ export async function searchMessagesGlobal({ minDate?: number; maxDate?: number; }): Promise { + if (type === 'publicPosts') { + return searchPublicPosts({ + query, + offsetRate, + offsetPeer, + offsetId, + limit, + }); + } + let filter; switch (type) { case 'media': @@ -1613,22 +1626,32 @@ export async function searchMessagesGlobal({ }; } -export async function searchHashtagPosts({ - hashtag, offsetRate, offsetPeer, offsetId, limit, +export async function searchPublicPosts({ + hashtag, query, offsetRate, offsetPeer, offsetId, limit, }: { - hashtag: string; + hashtag?: string; + query?: string; offsetRate?: number; offsetPeer?: ApiPeer; offsetId?: number; limit?: number; }): Promise { const peer = (offsetPeer && buildInputPeer(offsetPeer.id, offsetPeer.accessHash)) || new GramJs.InputPeerEmpty(); + + const resultFlood = await checkSearchPostsFlood(query); + + if (!resultFlood) { + return undefined; + } + const result = await invokeRequest(new GramJs.channels.SearchPosts({ hashtag, + query, offsetRate: offsetRate ?? DEFAULT_PRIMITIVES.INT, offsetId: offsetId ?? DEFAULT_PRIMITIVES.INT, offsetPeer: peer, limit: limit ?? DEFAULT_PRIMITIVES.INT, + allowPaidStars: BigInt(resultFlood.starsAmount), })); if (!result || result instanceof GramJs.messages.MessagesNotModified) { @@ -1650,6 +1673,10 @@ export async function searchHashtagPosts({ const nextOffsetRate = 'nextRate' in result && result.nextRate ? result.nextRate : undefined; const nextOffsetId = lastMessage?.id; + const searchFlood = result instanceof GramJs.messages.MessagesSlice && result.searchFlood + ? buildApiSearchPostsFlood(result.searchFlood, query) + : undefined; + return { messages, userStatusesById, @@ -1657,9 +1684,20 @@ export async function searchHashtagPosts({ nextOffsetRate, nextOffsetPeerId, nextOffsetId, + searchFlood, }; } +export async function checkSearchPostsFlood(query?: string) { + const result = await invokeRequest(new GramJs.channels.CheckSearchPostsFlood({ query })); + + if (!result) { + return undefined; + } + + return buildApiSearchPostsFlood(result, query); +} + export async function fetchWebPagePreview({ text, }: { diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index df72eb111..ddbbd0776 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -919,7 +919,8 @@ export type ApiTranscription = { }; export type ApiMessageSearchType = 'text' | 'media' | 'documents' | 'links' | 'audio' | 'voice' | 'profilePhoto'; -export type ApiGlobalMessageSearchType = 'text' | 'channels' | 'media' | 'documents' | 'links' | 'audio' | 'voice'; +export type ApiGlobalMessageSearchType = 'text' | + 'channels' | 'media' | 'documents' | 'links' | 'audio' | 'voice' | 'publicPosts'; export type ApiMessageSearchContext = 'all' | 'users' | 'groups' | 'channels'; export type ApiReportReason = 'spam' | 'violence' | 'pornography' | 'childAbuse' @@ -992,6 +993,15 @@ export type ApiPreparedInlineMessage = { cacheTime: number; }; +export type ApiSearchPostsFlood = { + query?: string; + queryIsFree?: boolean; + totalDaily: number; + remains: number; + waitTill?: number; + starsAmount: number; +}; + export const MAIN_THREAD_ID = -1; // `Symbol` can not be transferred from worker diff --git a/src/api/types/stars.ts b/src/api/types/stars.ts index 4055ec8f6..07299692a 100644 --- a/src/api/types/stars.ts +++ b/src/api/types/stars.ts @@ -239,6 +239,7 @@ export interface ApiStarsTransaction { isGiftUpgrade?: true; isGiftResale?: true; paidMessages?: number; + isPostsSearch?: true; } export interface ApiStarsSubscription { diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index c9f38d161..cc722e643 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -1639,6 +1639,7 @@ "SearchTabMusic" = "Music"; "SearchTabVoice" = "Voice"; "SearchTabMessages" = "Messages"; +"SearchTabPublicPosts" = "Posts"; "StarsTransactionsAll" = "All Transactions"; "StarsTransactionsIncoming" = "Incoming"; "StarsTransactionsOutgoing" = "Outgoing"; @@ -1657,6 +1658,7 @@ "ProfileTabSharedGroups" = "Groups"; "ProfileTabSimilarChannels" = "Similar Channels"; "ProfileTabSimilarBots" = "Similar Bots"; +"ProfileTabPublicPosts" = "Public Posts"; "ActionUnsupportedTitle" = "Action not supported yet"; "ActionUnsupportedDescription" = "Please use one of our apps to complete this action."; "LocationPermissionText" = "**{name}** requests access to set your **location**. You will be able to revoke this access in the profile page of **{name}**."; @@ -2171,4 +2173,21 @@ "PriceChanged" = "Price Changed"; "PayNewPrice" = "Pay New Price"; "PriceChangedText" = "The price has already changed from **{originalAmount}** to **{newAmount}**. Do you want to pay the new price?"; +"GlobalSearch" = "Global Search"; +"DescriptionPublicPostsSearch" = "Type a keyword to search for posts from public channels."; +"ButtonSearchPublicPosts" = "Search {query}"; +"RemainingPublicPostsSearch_one" = "{count} free search remaining today."; +"RemainingPublicPostsSearch_other" = "{count} free searches remaining today."; +"PublicPosts" = "Public Posts"; +"PublicPostsLimitReached" = "Limit Reached"; +"HintPublicPostsSearchQuota_one" = "You can make up to {count} search query per day."; +"HintPublicPostsSearchQuota_other" = "You can make up to {count} search queries per day."; +"PublicPostsSearchForStars" = "Search for {stars}"; +"UnlockTimerPublicPostsSearch" = "free search unlocks in {time}"; +"PublicPostsPremiumFeatureDescription" = "Type a keyword to search for posts from public channels."; +"PublicPostsPremiumFeatureSubtitle" = "Global search is a Premium feature."; +"PublicPostsSubscribeToPremium" = "Subscribe to Premium"; +"NotificationPaidExtraSearch" = "{stars} spent on extra search."; +"PostsSearchTransaction" = "Posts Search"; + diff --git a/src/assets/tgs-previews/DuckNothingFound.svg b/src/assets/tgs-previews/DuckNothingFound.svg new file mode 100644 index 000000000..5288391e5 --- /dev/null +++ b/src/assets/tgs-previews/DuckNothingFound.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/tgs-previews/Search.svg b/src/assets/tgs-previews/Search.svg new file mode 100644 index 000000000..8156ca297 --- /dev/null +++ b/src/assets/tgs-previews/Search.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/tgs/DuckNothingFound.tgs b/src/assets/tgs/DuckNothingFound.tgs new file mode 100644 index 0000000000000000000000000000000000000000..62a21279fb766aa9aa25ddc077936f2f51f34031 GIT binary patch literal 8590 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 literal 0 HcmV?d00001 diff --git a/src/assets/tgs/Search.tgs b/src/assets/tgs/Search.tgs new file mode 100644 index 0000000000000000000000000000000000000000..83f9cd05c0e81f383873d3fe2c97967937831b87 GIT binary patch literal 12475 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 b4+DQXG$Qa;-euq3m#E%~Gp>mZ6vY1n7O = ({ text = DEFAULT_TEXT, description }) => { +const NothingFound: FC = ({ text = DEFAULT_TEXT, description, withSticker }) => { const lang = useOldLang(); const { transitionClassNames } = useShowTransitionDeprecated(true); return ( -
+
+ {withSticker && ( + + )} {text} {description &&

{renderText(lang(description), ['br'])}

}
diff --git a/src/components/common/helpers/animatedAssets.ts b/src/components/common/helpers/animatedAssets.ts index 57870b913..1a81e8c42 100644 --- a/src/components/common/helpers/animatedAssets.ts +++ b/src/components/common/helpers/animatedAssets.ts @@ -9,6 +9,7 @@ import VoiceMini from '../../../assets/tgs/calls/VoiceMini.tgs'; import VoiceMuted from '../../../assets/tgs/calls/VoiceMuted.tgs'; import VoiceOutlined from '../../../assets/tgs/calls/VoiceOutlined.tgs'; import Diamond from '../../../assets/tgs/Diamond.tgs'; +import DuckNothingFound from '../../../assets/tgs/DuckNothingFound.tgs'; import Flame from '../../../assets/tgs/general/Flame.tgs'; import Fragment from '../../../assets/tgs/general/Fragment.tgs'; import Mention from '../../../assets/tgs/general/Mention.tgs'; @@ -22,6 +23,7 @@ import MonkeyPeek from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyPeek.tgs import MonkeyTracking from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyTracking.tgs'; import ReadTime from '../../../assets/tgs/ReadTime.tgs'; import Report from '../../../assets/tgs/Report.tgs'; +import Search from '../../../assets/tgs/Search.tgs'; import SearchingDuck from '../../../assets/tgs/SearchingDuck.tgs'; import Congratulations from '../../../assets/tgs/settings/Congratulations.tgs'; import DiscussionGroups from '../../../assets/tgs/settings/DiscussionGroupsDucks.tgs'; @@ -33,6 +35,13 @@ import Lock from '../../../assets/tgs/settings/Lock.tgs'; import StarReaction from '../../../assets/tgs/stars/StarReaction.tgs'; import StarReactionEffect from '../../../assets/tgs/stars/StarReactionEffect.tgs'; import Unlock from '../../../assets/tgs/Unlock.tgs'; +import DuckNothingFoundPreview from '../../../assets/tgs-previews/DuckNothingFound.svg'; +import SearchPreview from '../../../assets/tgs-previews/Search.svg'; + +export const LOCAL_TGS_PREVIEW_URLS = { + DuckNothingFound: DuckNothingFoundPreview, + Search: SearchPreview, +}; export const LOCAL_TGS_URLS = { MonkeyIdle, @@ -70,4 +79,6 @@ export const LOCAL_TGS_URLS = { SearchingDuck, BannedDuck, Diamond, + Search, + DuckNothingFound, }; diff --git a/src/components/left/LeftColumn.tsx b/src/components/left/LeftColumn.tsx index c26ee1f1a..e41f58027 100644 --- a/src/components/left/LeftColumn.tsx +++ b/src/components/left/LeftColumn.tsx @@ -17,6 +17,7 @@ import { IS_APP, IS_FIREFOX, IS_MAC_OS, IS_TOUCH_ENV, LAYERS_ANIMATION_NAME, } from '../../util/browser/windowEnvironment'; import captureEscKeyListener from '../../util/captureEscKeyListener'; +import { debounce } from '../../util/schedulers'; import { captureControlledSwipe } from '../../util/swipeController'; import useFoldersReducer from '../../hooks/reducers/useFoldersReducer'; @@ -110,6 +111,10 @@ function LeftColumn({ const [contactsFilter, setContactsFilter] = useState(''); const [foldersState, foldersDispatch] = useFoldersReducer(); + const debouncedSetGlobalSearchQuery = useMemo(() => debounce((query: string) => { + setGlobalSearchQuery({ query }); + }, 200, false, true), [setGlobalSearchQuery]); + // Used to reset child components in background. const [lastResetTime, setLastResetTime] = useState(0); @@ -375,7 +380,7 @@ function LeftColumn({ openLeftColumnContent({ contentKey: LeftColumnContent.GlobalSearch }); if (query !== searchQuery) { - setGlobalSearchQuery({ query }); + debouncedSetGlobalSearchQuery(query); } }); diff --git a/src/components/left/main/LeftMainHeader.tsx b/src/components/left/main/LeftMainHeader.tsx index a8da9d301..26d437a45 100644 --- a/src/components/left/main/LeftMainHeader.tsx +++ b/src/components/left/main/LeftMainHeader.tsx @@ -114,6 +114,7 @@ const LeftMainHeader: FC = ({ setGlobalSearchChatId, lockScreen, openSettingsScreen, + searchMessagesGlobal, } = getActions(); const oldLang = useOldLang(); @@ -193,6 +194,15 @@ const LeftMainHeader: FC = ({ lockScreen(); }); + const handleSearchEnter = useLastCallback(() => { + if (searchQuery && content === LeftColumnContent.GlobalSearch) { + searchMessagesGlobal({ + type: 'publicPosts', + shouldResetResultsByType: true, + }); + } + }); + const isSearchRelevant = Boolean(globalSearchChatId) || content === LeftColumnContent.GlobalSearch || content === LeftColumnContent.Contacts; @@ -295,6 +305,7 @@ const LeftMainHeader: FC = ({ onReset={onReset} onFocus={handleSearchFocus} onSpinnerClick={connectionStatusPosition === 'minimized' ? toggleConnectionStatus : undefined} + onEnter={handleSearchEnter} > {searchContent} ( return { searchQuery, - isLoading: fetchingStatus ? Boolean(fetchingStatus.chats || fetchingStatus.messages) : false, + isLoading: fetchingStatus ? Boolean(fetchingStatus.chats + || fetchingStatus.messages || fetchingStatus.publicPosts) : false, globalSearchChatId: chatId, searchDate: minDate, theme: selectTheme(global), diff --git a/src/components/left/search/AudioResults.tsx b/src/components/left/search/AudioResults.tsx index aa691a466..9b630ca3b 100644 --- a/src/components/left/search/AudioResults.tsx +++ b/src/components/left/search/AudioResults.tsx @@ -134,6 +134,7 @@ const AudioResults: FC = ({ {!canRenderContents && } {canRenderContents && (!foundIds || foundIds.length === 0) && ( diff --git a/src/components/left/search/BotAppResults.tsx b/src/components/left/search/BotAppResults.tsx index 49c181463..b93f40758 100644 --- a/src/components/left/search/BotAppResults.tsx +++ b/src/components/left/search/BotAppResults.tsx @@ -89,6 +89,7 @@ const BotAppResults: FC = ({ {!canRenderContents && } {canRenderContents && !filteredFoundIds?.length && ( diff --git a/src/components/left/search/ChatMessageResults.tsx b/src/components/left/search/ChatMessageResults.tsx index 7f694dac8..f0f5d5e9c 100644 --- a/src/components/left/search/ChatMessageResults.tsx +++ b/src/components/left/search/ChatMessageResults.tsx @@ -132,6 +132,7 @@ const ChatMessageResults: FC = ({ )} {nothingFound && ( diff --git a/src/components/left/search/ChatResults.tsx b/src/components/left/search/ChatResults.tsx index 2b367c195..33d90c36a 100644 --- a/src/components/left/search/ChatResults.tsx +++ b/src/components/left/search/ChatResults.tsx @@ -374,6 +374,7 @@ const ChatResults: FC = ({ )} {nothingFound && ( diff --git a/src/components/left/search/FileResults.tsx b/src/components/left/search/FileResults.tsx index 256ea8b98..2c55f8a89 100644 --- a/src/components/left/search/FileResults.tsx +++ b/src/components/left/search/FileResults.tsx @@ -139,6 +139,7 @@ const FileResults: FC = ({ {!canRenderContents && } {canRenderContents && (!foundIds || foundIds.length === 0) && ( diff --git a/src/components/left/search/LeftSearch.tsx b/src/components/left/search/LeftSearch.tsx index d7abb7103..e2fef4d01 100644 --- a/src/components/left/search/LeftSearch.tsx +++ b/src/components/left/search/LeftSearch.tsx @@ -1,6 +1,7 @@ import type { FC } from '../../../lib/teact/teact'; import { memo, + useEffect, useMemo, useRef, useState, @@ -27,6 +28,7 @@ import ChatResults from './ChatResults'; import FileResults from './FileResults'; import LinkResults from './LinkResults'; import MediaResults from './MediaResults'; +import PublicPostsResults from './PublicPostsResults'; import './LeftSearch.scss'; @@ -51,6 +53,7 @@ const TABS: TabInfo[] = [ { type: GlobalSearchContent.ChatList, key: 'SearchTabChats' }, { type: GlobalSearchContent.ChannelList, key: 'SearchTabChannels' }, { type: GlobalSearchContent.BotApps, key: 'SearchTabApps' }, + { type: GlobalSearchContent.PublicPosts, key: 'SearchTabPublicPosts' }, { type: GlobalSearchContent.Media, key: 'SearchTabMedia' }, { type: GlobalSearchContent.Links, key: 'SearchTabLinks' }, { type: GlobalSearchContent.Files, key: 'SearchTabFiles' }, @@ -74,12 +77,19 @@ const LeftSearch: FC = ({ const { setGlobalSearchContent, setGlobalSearchDate, + checkSearchPostsFlood, } = getActions(); const lang = useLang(); const [activeTab, setActiveTab] = useState(currentContent); const dateSearchQuery = useMemo(() => parseDateString(searchQuery), [searchQuery]); + useEffect(() => { + if (isActive) { + checkSearchPostsFlood({}); + } + }, [isActive]); + const tabs = useMemo(() => { const arr = chatId ? CHAT_TABS : TABS; return arr.map((tab) => ({ @@ -166,6 +176,13 @@ const LeftSearch: FC = ({ searchQuery={searchQuery} /> ); + case GlobalSearchContent.PublicPosts: + return ( + + ); default: return undefined; } diff --git a/src/components/left/search/LinkResults.tsx b/src/components/left/search/LinkResults.tsx index 53375904b..f3f27da67 100644 --- a/src/components/left/search/LinkResults.tsx +++ b/src/components/left/search/LinkResults.tsx @@ -132,6 +132,7 @@ const LinkResults: FC = ({ {!canRenderContents && } {canRenderContents && (!foundIds || foundIds.length === 0) && ( diff --git a/src/components/left/search/MediaResults.tsx b/src/components/left/search/MediaResults.tsx index 250630509..11cc0651f 100644 --- a/src/components/left/search/MediaResults.tsx +++ b/src/components/left/search/MediaResults.tsx @@ -133,6 +133,7 @@ const MediaResults: FC = ({ {!canRenderContents && } {canRenderContents && (!foundIds || foundIds.length === 0) && ( diff --git a/src/components/left/search/PublicPostsResults.tsx b/src/components/left/search/PublicPostsResults.tsx new file mode 100644 index 000000000..e6202cb52 --- /dev/null +++ b/src/components/left/search/PublicPostsResults.tsx @@ -0,0 +1,163 @@ +import { memo, useCallback, useMemo } from '../../../lib/teact/teact'; +import { getActions, withGlobal } from '../../../global'; +import { getGlobal } from '../../../global'; + +import type { ApiMessage, ApiSearchPostsFlood } 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'; + +import useLang from '../../../hooks/useLang'; +import useLastCallback from '../../../hooks/useLastCallback'; +import useOldLang from '../../../hooks/useOldLang'; + +import NothingFound from '../../common/NothingFound'; +import InfiniteScroll from '../../ui/InfiniteScroll'; +import Transition from '../../ui/Transition'; +import ChatMessage from './ChatMessage'; +import PublicPostsSearchLauncher from './PublicPostsSearchLauncher.tsx'; + +export type OwnProps = { + searchQuery?: string; +}; + +type StateProps = { + foundIds?: SearchResultKey[]; + globalMessagesByChatId?: Record }>; + searchFlood?: ApiSearchPostsFlood; + shouldShowSearchLauncher?: boolean; + isNothingFound?: boolean; +}; + +const runThrottled = throttle((cb) => cb(), 500, true); + +const PublicPostsResults = ({ + searchQuery, + foundIds, + globalMessagesByChatId, + searchFlood, + shouldShowSearchLauncher, + isNothingFound, +}: OwnProps & StateProps) => { + const { searchMessagesGlobal } = getActions(); + + const lang = useLang(); + const oldLang = useOldLang(); + + const handleSearch = useLastCallback(() => { + if (!searchQuery) return; + + searchMessagesGlobal({ + type: 'publicPosts', + shouldResetResultsByType: true, + }); + }); + + const handleLoadMore = useCallback(({ direction }: { direction: LoadMoreDirection }) => { + if (direction === LoadMoreDirection.Backwards) { + runThrottled(() => { + searchMessagesGlobal({ + type: 'publicPosts', + }); + }); + } + }, []); + + const foundMessages = useMemo(() => { + if (!foundIds || foundIds.length === 0) { + return MEMO_EMPTY_ARRAY; + } + + return foundIds + .map((id) => { + const [chatId, messageId] = parseSearchResultKey(id); + return globalMessagesByChatId?.[chatId]?.byId[messageId]; + }) + .filter(Boolean); + }, [foundIds, globalMessagesByChatId]); + + function renderFoundMessage(message: ApiMessage) { + const chatsById = getGlobal().chats.byId; + + const text = renderMessageSummary(oldLang, message); + const chat = chatsById[message.chatId]; + + if (!text || !chat) { + return undefined; + } + + return ( + + ); + } + + return ( + + {shouldShowSearchLauncher ? ( + + ) : ( +
+ + {isNothingFound && ( + + )} + {Boolean(foundMessages.length) && ( +
+

+ {lang('PublicPosts')} +

+ {foundMessages.map(renderFoundMessage)} +
+ )} +
+
+ )} +
+ ); +}; + +export default memo(withGlobal( + (global): StateProps => { + const { messages: { byChatId: globalMessagesByChatId } } = global; + const { resultsByType, searchFlood } = selectTabState(global).globalSearch; + + const publicPostsResult = resultsByType?.publicPosts; + const { foundIds } = publicPostsResult || {}; + const shouldShowSearchLauncher = !publicPostsResult; + const isNothingFound = publicPostsResult && !foundIds?.length; + + return { + foundIds, + globalMessagesByChatId, + searchFlood, + shouldShowSearchLauncher, + isNothingFound, + }; + }, +)(PublicPostsResults)); diff --git a/src/components/left/search/PublicPostsSearchLauncher.module.scss b/src/components/left/search/PublicPostsSearchLauncher.module.scss new file mode 100644 index 000000000..54db58b01 --- /dev/null +++ b/src/components/left/search/PublicPostsSearchLauncher.module.scss @@ -0,0 +1,129 @@ +.container { + display: flex; + flex-direction: column; + align-items: center; + + height: 100%; + padding: 2rem 1.5rem; +} + +.content { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + + max-width: 20rem; + + text-align: center; +} + +.searchButtonContent { + display: flex; + align-items: center; + justify-content: center; + height: 100%; +} + +.sticker { + margin-bottom: 1.5rem; +} + +.title { + margin-bottom: 0.75rem; + font-size: 1.25rem; + font-weight: 500; + color: var(--color-text); +} + +.description { + margin-bottom: 1.5rem; + font-size: 0.875rem; + line-height: 1.3125rem; + color: var(--color-text-secondary); +} + +.searchButton { + overflow: hidden; + + width: 100%; + min-width: 0; + margin-bottom: 1rem; + + text-overflow: ellipsis; + white-space: nowrap; +} + +.remainingSearches { + font-size: 0.8125rem; + color: var(--color-text-secondary); +} + +.searchIcon { + margin-right: 0.25rem; +} + +.searchQuery { + overflow: hidden; + display: inline-block; + + max-width: 10rem; + margin-inline-start: 0.25rem; + + color: var(--color-white); + text-overflow: ellipsis; + white-space: nowrap; + vertical-align: bottom; + + opacity: 0.75; +} + +.limitTitle { + margin-bottom: 0.75rem; + font-size: 1.5rem; + font-weight: var(--font-weight-medium); + color: var(--color-text); +} + +.limitDescription { + margin-bottom: 1.5rem; + font-size: 0.9375rem; + line-height: 1.375rem; + color: var(--color-text-secondary); +} + +.paidSearchButton { + width: 100%; + margin-bottom: 1rem; + font-weight: var(--font-weight-medium); +} + +.freeSearchUnlock { + font-size: 0.875rem; + color: var(--color-text-secondary); +} + +.premiumTitle { + margin-bottom: 0.5rem; + font-size: 1.25rem; + font-weight: var(--font-weight-medium); + color: var(--color-text); +} + +.premiumDescription { + margin-bottom: 1.5rem; + font-size: 0.875rem; + line-height: 1.4; + color: var(--color-text-secondary); +} + +.subscribePremiumButton { + width: 100%; + margin-bottom: 1rem; +} + +.premiumSubtitle { + font-size: 0.8125rem; + color: var(--color-text-secondary); + text-align: center; +} diff --git a/src/components/left/search/PublicPostsSearchLauncher.tsx b/src/components/left/search/PublicPostsSearchLauncher.tsx new file mode 100644 index 000000000..5da01b803 --- /dev/null +++ b/src/components/left/search/PublicPostsSearchLauncher.tsx @@ -0,0 +1,237 @@ +import { memo, useEffect } from '../../../lib/teact/teact'; +import { getActions, withGlobal } from '../../../global'; + +import type { ApiSearchPostsFlood } from '../../../api/types'; + +import { + PUBLIC_POSTS_SEARCH_DEFAULT_STARS_AMOUNT, + PUBLIC_POSTS_SEARCH_DEFAULT_TOTAL_DAILY, +} from '../../../config'; +import { selectIsCurrentUserPremium } from '../../../global/selectors'; +import { formatStarsAsIcon } from '../../../util/localization/format'; +import { getServerTime } from '../../../util/serverTime'; +import { LOCAL_TGS_PREVIEW_URLS, LOCAL_TGS_URLS } from '../../common/helpers/animatedAssets'; + +import { useTransitionActiveKey } from '../../../hooks/animations/useTransitionActiveKey'; +import useLang from '../../../hooks/useLang'; +import useLastCallback from '../../../hooks/useLastCallback'; + +import AnimatedIconWithPreview from '../../common/AnimatedIconWithPreview'; +import Icon from '../../common/icons/Icon'; +import Button from '../../ui/Button'; +import TextTimer from '../../ui/TextTimer'; +import Transition from '../../ui/Transition'; + +import styles from './PublicPostsSearchLauncher.module.scss'; + +type OwnProps = { + searchQuery?: string; + searchFlood?: ApiSearchPostsFlood; + onSearch: () => void; +}; + +type StateProps = { + isCurrentUserPremium?: boolean; + starsBalance: number; +}; + +const WAIT_DELAY = 2; + +const PublicPostsSearchLauncher = ({ + searchQuery, + searchFlood, + onSearch, + isCurrentUserPremium, + starsBalance, +}: OwnProps & StateProps) => { + const lang = useLang(); + const queryIsFree = searchFlood?.queryIsFree; + const queryFromFlood = searchFlood?.query; + + const searchButtonActiveKey = useTransitionActiveKey([searchQuery?.slice(0, 18).trimEnd()]); + + const handleSearchClick = useLastCallback(() => { + onSearch(); + }); + + useEffect(() => { + if (queryIsFree && searchQuery && queryFromFlood === searchQuery) { + onSearch(); + } + }, [queryIsFree, searchQuery, queryFromFlood, onSearch]); + + const handlePaidSearchClick = useLastCallback(() => { + const starsAmount = searchFlood?.starsAmount || 0; + const currentBalance = starsBalance; + + if (currentBalance < starsAmount) { + openStarsBalanceModal({ + topup: { + balanceNeeded: starsAmount, + }, + }); + } else { + onSearch(); + } + }); + + const { + checkSearchPostsFlood, + openPremiumModal, + openStarsBalanceModal, + } = getActions(); + + const onCheckFlood = useLastCallback(() => { + checkSearchPostsFlood({}); + }); + + const handleSubscribePremiumClick = useLastCallback(() => { + openPremiumModal(); + }); + + const renderLimitReached = () => { + const waitTill = searchFlood?.waitTill; + const starsAmount = searchFlood?.starsAmount || PUBLIC_POSTS_SEARCH_DEFAULT_STARS_AMOUNT; + const totalDaily = searchFlood?.totalDaily || PUBLIC_POSTS_SEARCH_DEFAULT_TOTAL_DAILY; + + return ( +
+
+ +
+ {lang('PublicPostsLimitReached')} +
+
+ {lang('HintPublicPostsSearchQuota', { count: totalDaily }, { pluralValue: totalDaily })} +
+ + {Boolean(waitTill) && ( +
+ +
+ )} +
+
+ ); + }; + + const renderSearchButton = () => { + const remainingSearches = searchFlood?.remains || 0; + + return ( +
+
+ +
+ {lang('GlobalSearch')} +
+
+ {lang('DescriptionPublicPostsSearch')} +
+ +
+ {lang('RemainingPublicPostsSearch', { count: remainingSearches }, { pluralValue: remainingSearches })} +
+
+
+ ); + }; + + const renderPremiumRequired = () => { + return ( +
+
+
+ {lang('GlobalSearch')} +
+
+ {lang('PublicPostsPremiumFeatureDescription')} +
+ +
+ {lang('PublicPostsPremiumFeatureSubtitle')} +
+
+
+ ); + }; + + if (!isCurrentUserPremium) { + return renderPremiumRequired(); + } + + const serverTime = getServerTime(); + const shouldRenderPaidScreen = searchFlood?.remains === 0 + || (searchFlood?.waitTill && searchFlood.waitTill > serverTime); + + return ( + + {shouldRenderPaidScreen ? renderLimitReached() : renderSearchButton()} + + ); +}; + +export default memo(withGlobal((global): StateProps => ({ + isCurrentUserPremium: selectIsCurrentUserPremium(global), + starsBalance: global.stars?.balance?.amount || 0, +}))(PublicPostsSearchLauncher)); diff --git a/src/components/modals/stars/helpers/transaction.ts b/src/components/modals/stars/helpers/transaction.ts index 8918af192..9774ca519 100644 --- a/src/components/modals/stars/helpers/transaction.ts +++ b/src/components/modals/stars/helpers/transaction.ts @@ -2,7 +2,7 @@ import type { ApiStarsAmount, ApiStarsTransaction, ApiTypeCurrencyAmount } from import type { OldLangFn } from '../../../../hooks/useOldLang'; import { STARS_CURRENCY_CODE, TON_CURRENCY_CODE } from '../../../../config'; -import { buildStarsTransactionCustomPeer } from '../../../../global/helpers/payments'; +import { buildStarsTransactionCustomPeer, shouldUseCustomPeer } from '../../../../global/helpers/payments'; import { type LangFn, } from '../../../../util/localization'; @@ -25,6 +25,9 @@ export function getTransactionTitle(oldLang: OldLangFn, lang: LangFn, transactio ? lang('StarGiftSaleTransaction') : lang('StarGiftPurchaseTransaction'); } + if (transaction.isPostsSearch) { + return lang('PostsSearchTransaction'); + } if (transaction.starRefCommision) { return oldLang('StarTransactionCommission', formatPercent(transaction.starRefCommision)); @@ -45,8 +48,8 @@ export function getTransactionTitle(oldLang: OldLangFn, lang: LangFn, transactio return isNegativeAmount(transaction.amount) ? oldLang('Gift2TransactionSent') : oldLang('Gift2ConvertedTitle'); } - const customPeer = (transaction.peer && transaction.peer.type !== 'peer' - && buildStarsTransactionCustomPeer(transaction.peer)) || undefined; + const customPeer = (transaction.peer && shouldUseCustomPeer(transaction) + && buildStarsTransactionCustomPeer(transaction)) || undefined; if (customPeer) return customPeer.title || oldLang(customPeer.titleKey!); diff --git a/src/components/modals/stars/transaction/StarsTransactionItem.tsx b/src/components/modals/stars/transaction/StarsTransactionItem.tsx index 6b00d4177..567df7cd9 100644 --- a/src/components/modals/stars/transaction/StarsTransactionItem.tsx +++ b/src/components/modals/stars/transaction/StarsTransactionItem.tsx @@ -9,7 +9,9 @@ import type { GlobalState } from '../../../../global/types'; import type { CustomPeer } from '../../../../types'; import { STARS_CURRENCY_CODE, TON_CURRENCY_CODE } from '../../../../config'; -import { buildStarsTransactionCustomPeer, formatStarsTransactionAmount } from '../../../../global/helpers/payments'; +import { buildStarsTransactionCustomPeer, + formatStarsTransactionAmount, + shouldUseCustomPeer } from '../../../../global/helpers/payments'; import { getPeerTitle } from '../../../../global/helpers/peers'; import { selectPeer } from '../../../../global/selectors'; import buildClassName from '../../../../util/buildClassName'; @@ -71,14 +73,11 @@ const StarsTransactionItem = ({ transaction, className }: OwnProps) => { let status: string | undefined; let avatarPeer: ApiPeer | CustomPeer | undefined; - if (transaction.peer.type === 'peer') { + if (!shouldUseCustomPeer(transaction)) { description = peer && getPeerTitle(oldLang, peer); avatarPeer = peer || CUSTOM_PEER_PREMIUM; } else { - const customPeer = buildStarsTransactionCustomPeer( - transaction.peer, - transaction.amount.currency === TON_CURRENCY_CODE, - ); + const customPeer = buildStarsTransactionCustomPeer(transaction); title = customPeer.title || oldLang(customPeer.titleKey!); description = oldLang(customPeer.subtitleKey!); avatarPeer = customPeer; @@ -92,6 +91,11 @@ const StarsTransactionItem = ({ transaction, className }: OwnProps) => { description = lang('GiftUnique', { title: transaction.starGift.title, number: transaction.starGift.number }); } + if (transaction.isPostsSearch) { + title = getTransactionTitle(oldLang, lang, transaction); + description = undefined; + } + if (transaction.photo) { avatarPeer = undefined; } diff --git a/src/components/modals/stars/transaction/StarsTransactionModal.tsx b/src/components/modals/stars/transaction/StarsTransactionModal.tsx index 62fe4cbc3..62b06196a 100644 --- a/src/components/modals/stars/transaction/StarsTransactionModal.tsx +++ b/src/components/modals/stars/transaction/StarsTransactionModal.tsx @@ -14,6 +14,7 @@ import { getMessageLink } from '../../../../global/helpers'; import { buildStarsTransactionCustomPeer, formatStarsTransactionAmount, + shouldUseCustomPeer, } from '../../../../global/helpers/payments'; import { selectCanPlayAnimatedEmojis, @@ -97,8 +98,8 @@ const StarsTransactionModal: FC = ({ const giftAttributes = isUniqueGift ? getGiftAttributes(gift) : undefined; - const customPeer = (transaction.peer && transaction.peer.type !== 'peer' - && buildStarsTransactionCustomPeer(transaction.peer)) || undefined; + const customPeer = (transaction.peer && shouldUseCustomPeer(transaction) + && buildStarsTransactionCustomPeer(transaction)) || undefined; const peerId = transaction.peer?.type === 'peer' ? transaction.peer.id : undefined; const toName = transaction.peer && oldLang(getStarsPeerTitleKey(transaction.peer)); @@ -123,8 +124,8 @@ const StarsTransactionModal: FC = ({ || (isGiftUpgrade && starGift?.type === 'starGiftUnique' ? starGift.title : undefined) || (media ? mediaText : undefined); - const shouldDisplayAvatar = !media && !sticker; - const avatarPeer = !photo ? (peer || customPeer) : undefined; + const shouldDisplayAvatar = !media && !sticker && !transaction.isPostsSearch; + const avatarPeer = !photo ? ((!shouldUseCustomPeer(transaction) && peer) || customPeer) : undefined; const uniqueGiftHeader = isUniqueGift && (
@@ -161,7 +162,7 @@ const StarsTransactionModal: FC = ({ {shouldDisplayAvatar && ( )} - {!sticker && ( + {!sticker && !transaction.isPostsSearch && ( = ({ peerLabel = oldLang('Stars.Transaction.Via'); } - tableData.push([ - peerLabel, - peerId ? { chatId: peerId } : toName || '', - ]); + if (!transaction.isPostsSearch) { + tableData.push([ + peerLabel, + peerId ? { chatId: peerId } : toName || '', + ]); + } if (transaction.starRefCommision && transaction.paidMessages) { tableData.push([ @@ -329,8 +332,8 @@ export default memo(withGlobal( const currencyAmount = modal?.transaction.amount; const starsGiftSticker = modal?.transaction.isGift - && currencyAmount?.currency === STARS_CURRENCY_CODE ? selectGiftStickerForStars(global, currencyAmount?.amount) - : selectGiftStickerForTon(global, currencyAmount?.amount); + ? (currencyAmount?.currency === STARS_CURRENCY_CODE ? selectGiftStickerForStars(global, currencyAmount?.amount) + : selectGiftStickerForTon(global, currencyAmount?.amount)) : undefined; return { peer, diff --git a/src/components/ui/Button.scss b/src/components/ui/Button.scss index e749a46ec..acade3d31 100644 --- a/src/components/ui/Button.scss +++ b/src/components/ui/Button.scss @@ -53,7 +53,7 @@ background-size: cover; outline: none !important; - transition: background-color 0.15s, color 0.15s; + transition: background-color 0.15s, color 0.15s, opacity 0.15s; // @optimization &:active, diff --git a/src/components/ui/SearchInput.tsx b/src/components/ui/SearchInput.tsx index 613475421..1f45b8165 100644 --- a/src/components/ui/SearchInput.tsx +++ b/src/components/ui/SearchInput.tsx @@ -49,6 +49,7 @@ type OwnProps = { onUpClick?: (event: React.MouseEvent) => void; onDownClick?: (event: React.MouseEvent) => void; onSpinnerClick?: NoneToVoidFunction; + onEnter?: NoneToVoidFunction; }; const SearchInput: FC = ({ @@ -80,6 +81,7 @@ const SearchInput: FC = ({ onUpClick, onDownClick, onSpinnerClick, + onEnter, }) => { let inputRef = useRef(); if (ref) { @@ -125,8 +127,22 @@ const SearchInput: FC = ({ } const handleKeyDown = useLastCallback((e: React.KeyboardEvent) => { - if (!resultsItemSelector) return; - if (e.key === 'ArrowDown' || e.key === 'Enter') { + if (e.key === 'Enter') { + if (onEnter) { + e.preventDefault(); + onEnter(); + return; + } + + if (resultsItemSelector) { + const element = document.querySelector(resultsItemSelector) as HTMLElement; + if (element) { + element.focus(); + } + } + } + + if (resultsItemSelector && e.key === 'ArrowDown') { const element = document.querySelector(resultsItemSelector) as HTMLElement; if (element) { element.focus(); diff --git a/src/components/ui/TextTimer.tsx b/src/components/ui/TextTimer.tsx index e1257da4c..157aeb2c2 100644 --- a/src/components/ui/TextTimer.tsx +++ b/src/components/ui/TextTimer.tsx @@ -5,8 +5,11 @@ import { getServerTime } from '../../util/serverTime'; import useInterval from '../../hooks/schedulers/useInterval'; import useForceUpdate from '../../hooks/useForceUpdate'; +import useLang from '../../hooks/useLang'; import useOldLang from '../../hooks/useOldLang'; +import AnimatedCounter from '../common/AnimatedCounter'; + type OwnProps = { langKey: string; endsAt: number; @@ -16,7 +19,8 @@ type OwnProps = { const UPDATE_FREQUENCY = 500; // Sometimes second gets skipped if using 1000 const TextTimer: FC = ({ langKey, endsAt, onEnd }) => { - const lang = useOldLang(); + const lang = useLang(); + const oldLang = useOldLang(); const forceUpdate = useForceUpdate(); const serverTime = getServerTime(); @@ -32,11 +36,33 @@ const TextTimer: FC = ({ langKey, endsAt, onEnd }) => { if (!isActive) return undefined; const timeLeft = endsAt - serverTime; - const formattedTime = formatMediaDuration(timeLeft); + const time = formatMediaDuration(timeLeft); + + const timeParts = time.split(':'); + const timeCounter = ( + + {timeParts.map((part, index) => ( + <> + {index > 0 && ':'} + + + ))} + + ); + + const isTypedKey = langKey === 'UnlockTimerPublicPostsSearch'; + + if (isTypedKey) { + return ( + + {lang(langKey, { time: timeCounter }, { withNodes: true })} + + ); + } return ( - {lang(langKey, formattedTime)} + {oldLang(langKey, time)} ); }; diff --git a/src/config.ts b/src/config.ts index 699a80c85..58b18491d 100644 --- a/src/config.ts +++ b/src/config.ts @@ -110,6 +110,10 @@ export const TODO_ITEMS_LIMIT = 30; export const TODO_TITLE_LENGTH_LIMIT = 32; export const TODO_ITEM_LENGTH_LIMIT = 64; +// Public Posts Search defaults +export const PUBLIC_POSTS_SEARCH_DEFAULT_STARS_AMOUNT = 10; +export const PUBLIC_POSTS_SEARCH_DEFAULT_TOTAL_DAILY = 2; + // Suggested Posts defaults export const STARS_SUGGESTED_POST_AMOUNT_MAX = 100000; export const STARS_SUGGESTED_POST_AMOUNT_MIN = 5; diff --git a/src/global/actions/api/globalSearch.ts b/src/global/actions/api/globalSearch.ts index 75e409b68..824316e52 100644 --- a/src/global/actions/api/globalSearch.ts +++ b/src/global/actions/api/globalSearch.ts @@ -1,5 +1,7 @@ +import { getActions } from '../../../global'; + import type { - ApiChat, ApiGlobalMessageSearchType, ApiMessage, ApiMessageSearchContext, ApiPeer, ApiTopic, + ApiChat, ApiGlobalMessageSearchType, ApiMessage, ApiMessageSearchContext, ApiPeer, ApiSearchPostsFlood, ApiTopic, ApiUserStatus, } from '../../../api/types'; import type { ActionReturnType, GlobalState, TabArgs } from '../../types'; @@ -9,6 +11,8 @@ import { timestampPlusDay } from '../../../util/dates/dateFormat'; import { isDeepLink, tryParseDeepLink } from '../../../util/deepLinkParser'; import { toChannelId } from '../../../util/entities/ids'; import { getCurrentTabId } from '../../../util/establishMultitabRole'; +import { getTranslationFn } from '../../../util/localization'; +import { formatStarsAsText } from '../../../util/localization/format'; import { throttle } from '../../../util/schedulers'; import { callApi } from '../../../api/gramjs'; import { isChatChannel, isChatGroup } from '../../helpers/chats'; @@ -158,6 +162,23 @@ addActionHandler('searchPopularBotApps', async (global, actions, payload): Promi setGlobal(global); }); +addActionHandler('checkSearchPostsFlood', async (global, actions, payload): Promise => { + const { query, tabId = getCurrentTabId() } = payload; + + const result = await callApi('checkSearchPostsFlood', query); + + global = getGlobal(); + if (!result) { + return; + } + + global = updateGlobalSearch(global, { + searchFlood: result, + }, tabId); + + setGlobal(global); +}); + async function searchMessagesGlobal(global: T, params: { query?: string; type: ApiGlobalMessageSearchType; @@ -175,6 +196,12 @@ async function searchMessagesGlobal(global: T, params: { query = '', type, context, offsetRate, offsetId, offsetPeer, peer, maxDate, minDate, shouldResetResultsByType, tabId = getCurrentTabId(), } = params; + + if (type === 'publicPosts') { + global = updateGlobalSearchFetchingStatus(global, { publicPosts: true }, tabId); + setGlobal(global); + } + let result: { messages: ApiMessage[]; userStatusesById?: Record; @@ -184,10 +211,13 @@ async function searchMessagesGlobal(global: T, params: { nextOffsetRate?: number; nextOffsetId?: number; nextOffsetPeerId?: string; + searchFlood?: ApiSearchPostsFlood; } | undefined; let messageLink: ApiMessage | undefined; + const previousSearchFlood = selectTabState(global, tabId).globalSearch.searchFlood; + if (peer) { const inChatResultRequest = callApi('searchMessagesInChat', { peer, @@ -256,7 +286,7 @@ async function searchMessagesGlobal(global: T, params: { } const currentSearchQuery = selectCurrentGlobalSearchQuery(global, tabId); if (!result || (query !== '' && query !== currentSearchQuery)) { - global = updateGlobalSearchFetchingStatus(global, { messages: false }, tabId); + global = updateGlobalSearchFetchingStatus(global, { messages: false, publicPosts: false }, tabId); setGlobal(global); return; } @@ -269,6 +299,8 @@ async function searchMessagesGlobal(global: T, params: { messages, userStatusesById, totalCount, nextOffsetRate, nextOffsetId, nextOffsetPeerId, } = result; + const searchFlood = result.searchFlood || previousSearchFlood; + if (userStatusesById) { global = addUserStatuses(global, userStatusesById); } @@ -285,6 +317,7 @@ async function searchMessagesGlobal(global: T, params: { nextOffsetRate, nextOffsetId, nextOffsetPeerId, + searchFlood, tabId, ); @@ -298,6 +331,20 @@ async function searchMessagesGlobal(global: T, params: { }, tabId); setGlobal(global); + + if (type === 'publicPosts' && searchFlood && !searchFlood.queryIsFree && !offsetId + && previousSearchFlood?.remains === 0) { + const lang = getTranslationFn(); + getActions().showNotification({ + icon: 'star', + message: { + key: 'NotificationPaidExtraSearch', + variables: { + stars: formatStarsAsText(lang, searchFlood.starsAmount), + }, + }, + }); + } } async function getMessageByPublicLink(global: GlobalState, link: { username: string; messageId: number }) { diff --git a/src/global/actions/api/middleSearch.ts b/src/global/actions/api/middleSearch.ts index 20ed21609..470ecc4f5 100644 --- a/src/global/actions/api/middleSearch.ts +++ b/src/global/actions/api/middleSearch.ts @@ -113,7 +113,7 @@ addActionHandler('performMiddleSearch', async (global, actions, payload): Promis } if (type === 'channels') { - result = await callApi('searchHashtagPosts', { + result = await callApi('searchPublicPosts', { hashtag: query!, limit: MESSAGE_SEARCH_SLICE, offsetId, diff --git a/src/global/actions/ui/globalSearch.ts b/src/global/actions/ui/globalSearch.ts index 12d44b42c..b4411091f 100644 --- a/src/global/actions/ui/globalSearch.ts +++ b/src/global/actions/ui/globalSearch.ts @@ -12,9 +12,12 @@ addActionHandler('setGlobalSearchQuery', (global, actions, payload): ActionRetur const { query, tabId = getCurrentTabId() } = payload; const { chatId, currentContent } = selectTabState(global, tabId).globalSearch; - const fetchingStatus = query && currentContent !== GlobalSearchContent.BotApps + const fetchingStatus = query + && currentContent !== GlobalSearchContent.BotApps && currentContent !== GlobalSearchContent.PublicPosts ? { chats: !chatId, messages: true } : undefined; + actions.checkSearchPostsFlood({ query, tabId }); + return updateGlobalSearch(global, { globalResults: {}, localResults: {}, diff --git a/src/global/helpers/payments.ts b/src/global/helpers/payments.ts index 4351398a7..3e5a31c3f 100644 --- a/src/global/helpers/payments.ts +++ b/src/global/helpers/payments.ts @@ -6,8 +6,6 @@ import type { ApiRequestInputSavedStarGift, ApiStarsAmount, ApiStarsTransaction, - ApiStarsTransactionPeer, - ApiStarsTransactionPeerPeer, ApiTypeCurrencyAmount, } from '../../api/types'; import type { CustomPeer } from '../../types'; @@ -259,10 +257,25 @@ export function getRequestInputSavedStarGift( return undefined; } +export function shouldUseCustomPeer(transaction: ApiStarsTransaction) { + return transaction.peer.type !== 'peer' || Boolean(transaction.isPostsSearch); +} + export function buildStarsTransactionCustomPeer( - peer: Exclude, - isForTon?: boolean, + transaction: ApiStarsTransaction, ): CustomPeer { + const { peer } = transaction; + const isForTon = transaction.amount.currency === TON_CURRENCY_CODE; + + if (transaction.isPostsSearch) { + return { + avatarIcon: 'search', + isCustomPeer: true, + title: '', + peerColorId: 5, + }; + } + if (peer.type === 'appStore') { return { avatarIcon: 'star', diff --git a/src/global/reducers/globalSearch.ts b/src/global/reducers/globalSearch.ts index 0fbfb4130..f8b9666ff 100644 --- a/src/global/reducers/globalSearch.ts +++ b/src/global/reducers/globalSearch.ts @@ -1,4 +1,4 @@ -import type { ApiGlobalMessageSearchType, ApiMessage } from '../../api/types'; +import type { ApiGlobalMessageSearchType, ApiMessage, ApiSearchPostsFlood } from '../../api/types'; import type { GlobalSearchContent } from '../../types'; import type { GlobalState, TabArgs, TabState } from '../types'; @@ -37,6 +37,7 @@ export function updateGlobalSearchResults( nextOffsetRate?: number, nextOffsetId?: number, nextOffsetPeerId?: string, + searchFlood?: ApiSearchPostsFlood, ...[tabId = getCurrentTabId()]: TabArgs ): T { const { resultsByType } = selectTabState(global, tabId).globalSearch || {}; @@ -52,8 +53,12 @@ export function updateGlobalSearchResults( (newId) => foundIdsForType.includes(getSearchResultKey(newFoundMessagesById[newId])), ) ) { - global = updateGlobalSearchFetchingStatus(global, { messages: false }, tabId); + global = updateGlobalSearchFetchingStatus(global, { + messages: false, + publicPosts: false, + }, tabId); return updateGlobalSearch(global, { + searchFlood, resultsByType: { ...(selectTabState(global, tabId).globalSearch || {}).resultsByType, [type]: { @@ -74,9 +79,13 @@ export function updateGlobalSearchResults( const foundIds = Array.prototype.concat(prevFoundIds, newFoundIds); const foundOrPrevFoundIds = areSortedArraysEqual(prevFoundIds, foundIds) ? prevFoundIds : foundIds; - global = updateGlobalSearchFetchingStatus(global, { messages: false }, tabId); + global = updateGlobalSearchFetchingStatus(global, { + messages: false, + publicPosts: false, + }, tabId); return updateGlobalSearch(global, { + searchFlood, resultsByType: { ...(selectTabState(global, tabId).globalSearch || {}).resultsByType, [type]: { @@ -91,7 +100,7 @@ export function updateGlobalSearchResults( } export function updateGlobalSearchFetchingStatus( - global: T, newState: { chats?: boolean; messages?: boolean; botApps?: boolean }, + global: T, newState: { chats?: boolean; messages?: boolean; botApps?: boolean; publicPosts?: boolean }, ...[tabId = getCurrentTabId()]: TabArgs ): T { return updateGlobalSearch(global, { diff --git a/src/global/types/actions.ts b/src/global/types/actions.ts index 3305628f8..5a76942d6 100644 --- a/src/global/types/actions.ts +++ b/src/global/types/actions.ts @@ -418,6 +418,9 @@ export interface ActionPayloads { shouldCheckFetchingMessagesStatus?: boolean; } & WithTabId; searchPopularBotApps: WithTabId | undefined; + checkSearchPostsFlood: { + query?: string; + } & WithTabId; addRecentlyFoundChatId: { id: string; }; diff --git a/src/global/types/tabState.ts b/src/global/types/tabState.ts index 061f4bb51..2dfb05f69 100644 --- a/src/global/types/tabState.ts +++ b/src/global/types/tabState.ts @@ -36,6 +36,7 @@ import type { ApiReceiptRegular, ApiSavedGifts, ApiSavedStarGift, + ApiSearchPostsFlood, ApiSponsoredPeer, ApiStarGift, ApiStarGiftAttribute, @@ -249,10 +250,12 @@ export type TabState = { chatId?: string; foundTopicIds?: number[]; sponsoredPeer?: ApiSponsoredPeer; + searchFlood?: ApiSearchPostsFlood; fetchingStatus?: { chats?: boolean; messages?: boolean; botApps?: boolean; + publicPosts?: boolean; }; isClosing?: boolean; localResults?: { diff --git a/src/lib/gramjs/tl/api.d.ts b/src/lib/gramjs/tl/api.d.ts index 7da54d461..cd2d7821b 100644 --- a/src/lib/gramjs/tl/api.d.ts +++ b/src/lib/gramjs/tl/api.d.ts @@ -16515,6 +16515,7 @@ namespace Api { stargiftUpgrade?: true; businessTransfer?: true; stargiftResale?: true; + postsSearch?: true; id: string; amount: Api.TypeStarsAmount; date: int; @@ -16548,6 +16549,7 @@ namespace Api { stargiftUpgrade?: true; businessTransfer?: true; stargiftResale?: true; + postsSearch?: true; id: string; amount: Api.TypeStarsAmount; date: int; diff --git a/src/lib/gramjs/tl/apiTl.ts b/src/lib/gramjs/tl/apiTl.ts index c51b28785..4a3061953 100644 --- a/src/lib/gramjs/tl/apiTl.ts +++ b/src/lib/gramjs/tl/apiTl.ts @@ -1368,7 +1368,7 @@ starsTransactionPeer#d80da15d peer:Peer = StarsTransactionPeer; starsTransactionPeerAds#60682812 = StarsTransactionPeer; starsTransactionPeerAPI#f9677aad = StarsTransactionPeer; starsTopupOption#bd915c0 flags:# extended:flags.1?true stars:long store_product:flags.0?string currency:string amount:long = StarsTopupOption; -starsTransaction#13659eb0 flags:# refund:flags.3?true pending:flags.4?true failed:flags.6?true gift:flags.10?true reaction:flags.11?true stargift_upgrade:flags.18?true business_transfer:flags.21?true stargift_resale:flags.22?true id:string amount:StarsAmount date:int peer:StarsTransactionPeer title:flags.0?string description:flags.1?string photo:flags.2?WebDocument transaction_date:flags.5?int transaction_url:flags.5?string bot_payload:flags.7?bytes msg_id:flags.8?int extended_media:flags.9?Vector subscription_period:flags.12?int giveaway_post_id:flags.13?int stargift:flags.14?StarGift floodskip_number:flags.15?int starref_commission_permille:flags.16?int starref_peer:flags.17?Peer starref_amount:flags.17?StarsAmount paid_messages:flags.19?int premium_gift_months:flags.20?int ads_proceeds_from_date:flags.23?int ads_proceeds_to_date:flags.23?int = StarsTransaction; +starsTransaction#13659eb0 flags:# refund:flags.3?true pending:flags.4?true failed:flags.6?true gift:flags.10?true reaction:flags.11?true stargift_upgrade:flags.18?true business_transfer:flags.21?true stargift_resale:flags.22?true posts_search:flags.24?true id:string amount:StarsAmount date:int peer:StarsTransactionPeer title:flags.0?string description:flags.1?string photo:flags.2?WebDocument transaction_date:flags.5?int transaction_url:flags.5?string bot_payload:flags.7?bytes msg_id:flags.8?int extended_media:flags.9?Vector subscription_period:flags.12?int giveaway_post_id:flags.13?int stargift:flags.14?StarGift floodskip_number:flags.15?int starref_commission_permille:flags.16?int starref_peer:flags.17?Peer starref_amount:flags.17?StarsAmount paid_messages:flags.19?int premium_gift_months:flags.20?int ads_proceeds_from_date:flags.23?int ads_proceeds_to_date:flags.23?int = StarsTransaction; payments.starsStatus#6c9ce8ed flags:# balance:StarsAmount subscriptions:flags.1?Vector subscriptions_next_offset:flags.2?string subscriptions_missing_balance:flags.4?long history:flags.3?Vector next_offset:flags.0?string chats:Vector users:Vector = payments.StarsStatus; foundStory#e87acbc0 peer:Peer story:StoryItem = FoundStory; stories.foundStories#e2de7737 flags:# count:int stories:Vector next_offset:flags.0?string chats:Vector users:Vector = stories.FoundStories; @@ -1751,6 +1751,7 @@ channels.getChannelRecommendations#25a71742 flags:# channel:flags.0?InputChannel channels.searchPosts#f2c4f24d flags:# hashtag:flags.0?string query:flags.1?string offset_rate:int offset_peer:InputPeer offset_id:int limit:int allow_paid_stars:flags.2?long = messages.Messages; channels.updatePaidMessagesPrice#4b12327b flags:# broadcast_messages_allowed:flags.0?true channel:InputChannel send_paid_messages_stars:long = Updates; channels.toggleAutotranslation#167fc0a1 channel:InputChannel enabled:Bool = Updates; +channels.checkSearchPostsFlood#22567115 flags:# query:flags.0?string = SearchPostsFlood; 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; diff --git a/src/lib/gramjs/tl/static/api.json b/src/lib/gramjs/tl/static/api.json index 5d760ca0b..9fb883c73 100644 --- a/src/lib/gramjs/tl/static/api.json +++ b/src/lib/gramjs/tl/static/api.json @@ -284,6 +284,7 @@ "channels.searchPosts", "channels.reportSpam", "channels.updatePaidMessagesPrice", + "channels.checkSearchPostsFlood", "channels.toggleAutotranslation", "bots.getBotRecommendations", "bots.canSendMessage", diff --git a/src/lib/gramjs/tl/static/api.tl b/src/lib/gramjs/tl/static/api.tl index 051f1ba62..cf284a780 100644 --- a/src/lib/gramjs/tl/static/api.tl +++ b/src/lib/gramjs/tl/static/api.tl @@ -1853,7 +1853,7 @@ starsTransactionPeerAPI#f9677aad = StarsTransactionPeer; starsTopupOption#bd915c0 flags:# extended:flags.1?true stars:long store_product:flags.0?string currency:string amount:long = StarsTopupOption; -starsTransaction#13659eb0 flags:# refund:flags.3?true pending:flags.4?true failed:flags.6?true gift:flags.10?true reaction:flags.11?true stargift_upgrade:flags.18?true business_transfer:flags.21?true stargift_resale:flags.22?true id:string amount:StarsAmount date:int peer:StarsTransactionPeer title:flags.0?string description:flags.1?string photo:flags.2?WebDocument transaction_date:flags.5?int transaction_url:flags.5?string bot_payload:flags.7?bytes msg_id:flags.8?int extended_media:flags.9?Vector subscription_period:flags.12?int giveaway_post_id:flags.13?int stargift:flags.14?StarGift floodskip_number:flags.15?int starref_commission_permille:flags.16?int starref_peer:flags.17?Peer starref_amount:flags.17?StarsAmount paid_messages:flags.19?int premium_gift_months:flags.20?int ads_proceeds_from_date:flags.23?int ads_proceeds_to_date:flags.23?int = StarsTransaction; +starsTransaction#13659eb0 flags:# refund:flags.3?true pending:flags.4?true failed:flags.6?true gift:flags.10?true reaction:flags.11?true stargift_upgrade:flags.18?true business_transfer:flags.21?true stargift_resale:flags.22?true posts_search:flags.24?true id:string amount:StarsAmount date:int peer:StarsTransactionPeer title:flags.0?string description:flags.1?string photo:flags.2?WebDocument transaction_date:flags.5?int transaction_url:flags.5?string bot_payload:flags.7?bytes msg_id:flags.8?int extended_media:flags.9?Vector subscription_period:flags.12?int giveaway_post_id:flags.13?int stargift:flags.14?StarGift floodskip_number:flags.15?int starref_commission_permille:flags.16?int starref_peer:flags.17?Peer starref_amount:flags.17?StarsAmount paid_messages:flags.19?int premium_gift_months:flags.20?int ads_proceeds_from_date:flags.23?int ads_proceeds_to_date:flags.23?int = StarsTransaction; payments.starsStatus#6c9ce8ed flags:# balance:StarsAmount subscriptions:flags.1?Vector subscriptions_next_offset:flags.2?string subscriptions_missing_balance:flags.4?long history:flags.3?Vector next_offset:flags.0?string chats:Vector users:Vector = payments.StarsStatus; diff --git a/src/types/index.ts b/src/types/index.ts index f785207a0..a2820aecf 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -313,6 +313,7 @@ export enum GlobalSearchContent { ChatList, ChannelList, BotApps, + PublicPosts, Media, Links, Files, diff --git a/src/types/language.d.ts b/src/types/language.d.ts index dff70377d..70839cd80 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -1333,6 +1333,7 @@ export interface LangPair { 'SearchTabMusic': undefined; 'SearchTabVoice': undefined; 'SearchTabMessages': undefined; + 'SearchTabPublicPosts': undefined; 'StarsTransactionsAll': undefined; 'StarsTransactionsIncoming': undefined; 'StarsTransactionsOutgoing': undefined; @@ -1351,6 +1352,7 @@ export interface LangPair { 'ProfileTabSharedGroups': undefined; 'ProfileTabSimilarChannels': undefined; 'ProfileTabSimilarBots': undefined; + 'ProfileTabPublicPosts': undefined; 'ActionUnsupportedTitle': undefined; 'ActionUnsupportedDescription': undefined; 'UnlockMoreSimilarBots': undefined; @@ -1620,6 +1622,14 @@ export interface LangPair { 'LabelPayInTON': undefined; 'PriceChanged': undefined; 'PayNewPrice': undefined; + 'GlobalSearch': undefined; + 'DescriptionPublicPostsSearch': undefined; + 'PublicPosts': undefined; + 'PublicPostsLimitReached': undefined; + 'PublicPostsPremiumFeatureDescription': undefined; + 'PublicPostsPremiumFeatureSubtitle': undefined; + 'PublicPostsSubscribeToPremium': undefined; + 'PostsSearchTransaction': undefined; } export interface LangPairWithVariables { @@ -2816,6 +2826,18 @@ export interface LangPairWithVariables { 'originalAmount': V; 'newAmount': V; }; + 'ButtonSearchPublicPosts': { + 'query': V; + }; + 'PublicPostsSearchForStars': { + 'stars': V; + }; + 'UnlockTimerPublicPostsSearch': { + 'time': V; + }; + 'NotificationPaidExtraSearch': { + 'stars': V; + }; } export interface LangPairPlural { @@ -3137,6 +3159,12 @@ export interface LangPairPluralWithVariables { 'TextAgeVerificationModal': { 'count': V; }; + 'RemainingPublicPostsSearch': { + 'count': V; + }; + 'HintPublicPostsSearchQuota': { + 'count': V; + }; } export type RegularLangKey = keyof LangPair; export type RegularLangKeyWithVariables = keyof LangPairWithVariables;