Global Search: Support search public posts (#6114)

This commit is contained in:
Alexander Zinchuk 2025-08-15 18:25:31 +02:00
parent 6204fadc0f
commit 09323114d5
47 changed files with 918 additions and 50 deletions

View File

@ -51,6 +51,16 @@ You are an expert in TypeScript, JavaScript, HTML, SCSS and Teact with deep expe
style={{ transform: `translateX(${value}%)` }} style={{ transform: `translateX(${value}%)` }}
style={{ '--custom-prop': value } as React.CSSProperties} 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:** - **Localization & Text Rules:**
- **ALWAYS** use `lang()` for all text content - never hardcode strings. - **ALWAYS** use `lang()` for all text content - never hardcode strings.

View File

@ -22,6 +22,7 @@ import type {
ApiPreparedInlineMessage, ApiPreparedInlineMessage,
ApiQuickReply, ApiQuickReply,
ApiReplyInfo, ApiReplyInfo,
ApiSearchPostsFlood,
ApiSponsoredMessage, ApiSponsoredMessage,
ApiSticker, ApiSticker,
ApiStory, ApiStory,
@ -820,3 +821,17 @@ export function buildPreparedInlineMessage(
cacheTime: result.cacheTime, 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(),
};
}

View File

@ -549,7 +549,7 @@ export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction):
const { const {
date, id, peer, amount, description, photo, title, refund, extendedMedia, failed, msgId, pending, gift, reaction, date, id, peer, amount, description, photo, title, refund, extendedMedia, failed, msgId, pending, gift, reaction,
subscriptionPeriod, stargift, giveawayPostId, starrefCommissionPermille, stargiftUpgrade, paidMessages, subscriptionPeriod, stargift, giveawayPostId, starrefCommissionPermille, stargiftUpgrade, paidMessages,
stargiftResale, stargiftResale, postsSearch,
} = transaction; } = transaction;
if (photo) { if (photo) {
@ -588,6 +588,7 @@ export function buildApiStarsTransaction(transaction: GramJs.StarsTransaction):
isGiftUpgrade: stargiftUpgrade, isGiftUpgrade: stargiftUpgrade,
isGiftResale: stargiftResale, isGiftResale: stargiftResale,
paidMessages, paidMessages,
isPostsSearch: postsSearch,
}; };
} }

View File

@ -24,6 +24,7 @@ import type {
ApiPeer, ApiPeer,
ApiPoll, ApiPoll,
ApiReaction, ApiReaction,
ApiSearchPostsFlood,
ApiSendMessageAction, ApiSendMessageAction,
ApiTodoItem, ApiTodoItem,
ApiUser, ApiUser,
@ -66,6 +67,7 @@ import {
buildApiMessage, buildApiMessage,
buildApiQuickReply, buildApiQuickReply,
buildApiReportResult, buildApiReportResult,
buildApiSearchPostsFlood,
buildApiSponsoredMessage, buildApiSponsoredMessage,
buildApiThreadInfo, buildApiThreadInfo,
buildLocalForwardedMessage, buildLocalForwardedMessage,
@ -128,6 +130,7 @@ type SearchResults = {
nextOffsetRate?: number; nextOffsetRate?: number;
nextOffsetPeerId?: string; nextOffsetPeerId?: string;
nextOffsetId?: number; nextOffsetId?: number;
searchFlood?: ApiSearchPostsFlood;
}; };
export async function fetchMessages({ export async function fetchMessages({
@ -1537,6 +1540,16 @@ export async function searchMessagesGlobal({
minDate?: number; minDate?: number;
maxDate?: number; maxDate?: number;
}): Promise<SearchResults | undefined> { }): Promise<SearchResults | undefined> {
if (type === 'publicPosts') {
return searchPublicPosts({
query,
offsetRate,
offsetPeer,
offsetId,
limit,
});
}
let filter; let filter;
switch (type) { switch (type) {
case 'media': case 'media':
@ -1613,22 +1626,32 @@ export async function searchMessagesGlobal({
}; };
} }
export async function searchHashtagPosts({ export async function searchPublicPosts({
hashtag, offsetRate, offsetPeer, offsetId, limit, hashtag, query, offsetRate, offsetPeer, offsetId, limit,
}: { }: {
hashtag: string; hashtag?: string;
query?: string;
offsetRate?: number; offsetRate?: number;
offsetPeer?: ApiPeer; offsetPeer?: ApiPeer;
offsetId?: number; offsetId?: number;
limit?: number; limit?: number;
}): Promise<SearchResults | undefined> { }): Promise<SearchResults | undefined> {
const peer = (offsetPeer && buildInputPeer(offsetPeer.id, offsetPeer.accessHash)) || new GramJs.InputPeerEmpty(); 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({ const result = await invokeRequest(new GramJs.channels.SearchPosts({
hashtag, hashtag,
query,
offsetRate: offsetRate ?? DEFAULT_PRIMITIVES.INT, offsetRate: offsetRate ?? DEFAULT_PRIMITIVES.INT,
offsetId: offsetId ?? DEFAULT_PRIMITIVES.INT, offsetId: offsetId ?? DEFAULT_PRIMITIVES.INT,
offsetPeer: peer, offsetPeer: peer,
limit: limit ?? DEFAULT_PRIMITIVES.INT, limit: limit ?? DEFAULT_PRIMITIVES.INT,
allowPaidStars: BigInt(resultFlood.starsAmount),
})); }));
if (!result || result instanceof GramJs.messages.MessagesNotModified) { 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 nextOffsetRate = 'nextRate' in result && result.nextRate ? result.nextRate : undefined;
const nextOffsetId = lastMessage?.id; const nextOffsetId = lastMessage?.id;
const searchFlood = result instanceof GramJs.messages.MessagesSlice && result.searchFlood
? buildApiSearchPostsFlood(result.searchFlood, query)
: undefined;
return { return {
messages, messages,
userStatusesById, userStatusesById,
@ -1657,9 +1684,20 @@ export async function searchHashtagPosts({
nextOffsetRate, nextOffsetRate,
nextOffsetPeerId, nextOffsetPeerId,
nextOffsetId, 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({ export async function fetchWebPagePreview({
text, text,
}: { }: {

View File

@ -919,7 +919,8 @@ export type ApiTranscription = {
}; };
export type ApiMessageSearchType = 'text' | 'media' | 'documents' | 'links' | 'audio' | 'voice' | 'profilePhoto'; 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 ApiMessageSearchContext = 'all' | 'users' | 'groups' | 'channels';
export type ApiReportReason = 'spam' | 'violence' | 'pornography' | 'childAbuse' export type ApiReportReason = 'spam' | 'violence' | 'pornography' | 'childAbuse'
@ -992,6 +993,15 @@ export type ApiPreparedInlineMessage = {
cacheTime: number; cacheTime: number;
}; };
export type ApiSearchPostsFlood = {
query?: string;
queryIsFree?: boolean;
totalDaily: number;
remains: number;
waitTill?: number;
starsAmount: number;
};
export const MAIN_THREAD_ID = -1; export const MAIN_THREAD_ID = -1;
// `Symbol` can not be transferred from worker // `Symbol` can not be transferred from worker

View File

@ -239,6 +239,7 @@ export interface ApiStarsTransaction {
isGiftUpgrade?: true; isGiftUpgrade?: true;
isGiftResale?: true; isGiftResale?: true;
paidMessages?: number; paidMessages?: number;
isPostsSearch?: true;
} }
export interface ApiStarsSubscription { export interface ApiStarsSubscription {

View File

@ -1639,6 +1639,7 @@
"SearchTabMusic" = "Music"; "SearchTabMusic" = "Music";
"SearchTabVoice" = "Voice"; "SearchTabVoice" = "Voice";
"SearchTabMessages" = "Messages"; "SearchTabMessages" = "Messages";
"SearchTabPublicPosts" = "Posts";
"StarsTransactionsAll" = "All Transactions"; "StarsTransactionsAll" = "All Transactions";
"StarsTransactionsIncoming" = "Incoming"; "StarsTransactionsIncoming" = "Incoming";
"StarsTransactionsOutgoing" = "Outgoing"; "StarsTransactionsOutgoing" = "Outgoing";
@ -1657,6 +1658,7 @@
"ProfileTabSharedGroups" = "Groups"; "ProfileTabSharedGroups" = "Groups";
"ProfileTabSimilarChannels" = "Similar Channels"; "ProfileTabSimilarChannels" = "Similar Channels";
"ProfileTabSimilarBots" = "Similar Bots"; "ProfileTabSimilarBots" = "Similar Bots";
"ProfileTabPublicPosts" = "Public Posts";
"ActionUnsupportedTitle" = "Action not supported yet"; "ActionUnsupportedTitle" = "Action not supported yet";
"ActionUnsupportedDescription" = "Please use one of our apps to complete this action."; "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}**."; "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"; "PriceChanged" = "Price Changed";
"PayNewPrice" = "Pay New Price"; "PayNewPrice" = "Pay New Price";
"PriceChangedText" = "The price has already changed from **{originalAmount}** to **{newAmount}**. Do you want to pay the 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";

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 8.7 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 14 KiB

Binary file not shown.

BIN
src/assets/tgs/Search.tgs Normal file

Binary file not shown.

View File

@ -4,10 +4,15 @@
justify-content: center; justify-content: center;
color: var(--color-text-meta); color: var(--color-text-meta);
&.with-description { &.with-description,
&.with-sticker {
flex-direction: column; flex-direction: column;
} }
.sticker {
margin-bottom: 1rem;
}
.AnimatedSticker { .AnimatedSticker {
margin: 0 auto; margin: 0 auto;
} }

View File

@ -2,26 +2,45 @@ import type { FC } from '../../lib/teact/teact';
import { memo } from '../../lib/teact/teact'; import { memo } from '../../lib/teact/teact';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import { LOCAL_TGS_PREVIEW_URLS, LOCAL_TGS_URLS } from './helpers/animatedAssets';
import renderText from './helpers/renderText'; import renderText from './helpers/renderText';
import useOldLang from '../../hooks/useOldLang'; import useOldLang from '../../hooks/useOldLang';
import useShowTransitionDeprecated from '../../hooks/useShowTransitionDeprecated'; import useShowTransitionDeprecated from '../../hooks/useShowTransitionDeprecated';
import AnimatedIconWithPreview from './AnimatedIconWithPreview';
import './NothingFound.scss'; import './NothingFound.scss';
interface OwnProps { interface OwnProps {
text?: string; text?: string;
description?: string; description?: string;
withSticker?: boolean;
} }
const DEFAULT_TEXT = 'Nothing found.'; const DEFAULT_TEXT = 'Nothing found.';
const NothingFound: FC<OwnProps> = ({ text = DEFAULT_TEXT, description }) => { const NothingFound: FC<OwnProps> = ({ text = DEFAULT_TEXT, description, withSticker }) => {
const lang = useOldLang(); const lang = useOldLang();
const { transitionClassNames } = useShowTransitionDeprecated(true); const { transitionClassNames } = useShowTransitionDeprecated(true);
return ( return (
<div className={buildClassName('NothingFound', transitionClassNames, description && 'with-description')}> <div className={buildClassName(
'NothingFound',
transitionClassNames,
description && 'with-description',
withSticker && 'with-sticker')}
>
{withSticker && (
<AnimatedIconWithPreview
className="sticker"
size={120}
tgsUrl={LOCAL_TGS_URLS.DuckNothingFound}
previewUrl={LOCAL_TGS_PREVIEW_URLS.DuckNothingFound}
nonInteractive
noLoop={false}
/>
)}
{text} {text}
{description && <p className="description">{renderText(lang(description), ['br'])}</p>} {description && <p className="description">{renderText(lang(description), ['br'])}</p>}
</div> </div>

View File

@ -9,6 +9,7 @@ import VoiceMini from '../../../assets/tgs/calls/VoiceMini.tgs';
import VoiceMuted from '../../../assets/tgs/calls/VoiceMuted.tgs'; import VoiceMuted from '../../../assets/tgs/calls/VoiceMuted.tgs';
import VoiceOutlined from '../../../assets/tgs/calls/VoiceOutlined.tgs'; import VoiceOutlined from '../../../assets/tgs/calls/VoiceOutlined.tgs';
import Diamond from '../../../assets/tgs/Diamond.tgs'; import Diamond from '../../../assets/tgs/Diamond.tgs';
import DuckNothingFound from '../../../assets/tgs/DuckNothingFound.tgs';
import Flame from '../../../assets/tgs/general/Flame.tgs'; import Flame from '../../../assets/tgs/general/Flame.tgs';
import Fragment from '../../../assets/tgs/general/Fragment.tgs'; import Fragment from '../../../assets/tgs/general/Fragment.tgs';
import Mention from '../../../assets/tgs/general/Mention.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 MonkeyTracking from '../../../assets/tgs/monkeys/TwoFactorSetupMonkeyTracking.tgs';
import ReadTime from '../../../assets/tgs/ReadTime.tgs'; import ReadTime from '../../../assets/tgs/ReadTime.tgs';
import Report from '../../../assets/tgs/Report.tgs'; import Report from '../../../assets/tgs/Report.tgs';
import Search from '../../../assets/tgs/Search.tgs';
import SearchingDuck from '../../../assets/tgs/SearchingDuck.tgs'; import SearchingDuck from '../../../assets/tgs/SearchingDuck.tgs';
import Congratulations from '../../../assets/tgs/settings/Congratulations.tgs'; import Congratulations from '../../../assets/tgs/settings/Congratulations.tgs';
import DiscussionGroups from '../../../assets/tgs/settings/DiscussionGroupsDucks.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 StarReaction from '../../../assets/tgs/stars/StarReaction.tgs';
import StarReactionEffect from '../../../assets/tgs/stars/StarReactionEffect.tgs'; import StarReactionEffect from '../../../assets/tgs/stars/StarReactionEffect.tgs';
import Unlock from '../../../assets/tgs/Unlock.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 = { export const LOCAL_TGS_URLS = {
MonkeyIdle, MonkeyIdle,
@ -70,4 +79,6 @@ export const LOCAL_TGS_URLS = {
SearchingDuck, SearchingDuck,
BannedDuck, BannedDuck,
Diamond, Diamond,
Search,
DuckNothingFound,
}; };

View File

@ -17,6 +17,7 @@ import {
IS_APP, IS_FIREFOX, IS_MAC_OS, IS_TOUCH_ENV, LAYERS_ANIMATION_NAME, IS_APP, IS_FIREFOX, IS_MAC_OS, IS_TOUCH_ENV, LAYERS_ANIMATION_NAME,
} from '../../util/browser/windowEnvironment'; } from '../../util/browser/windowEnvironment';
import captureEscKeyListener from '../../util/captureEscKeyListener'; import captureEscKeyListener from '../../util/captureEscKeyListener';
import { debounce } from '../../util/schedulers';
import { captureControlledSwipe } from '../../util/swipeController'; import { captureControlledSwipe } from '../../util/swipeController';
import useFoldersReducer from '../../hooks/reducers/useFoldersReducer'; import useFoldersReducer from '../../hooks/reducers/useFoldersReducer';
@ -110,6 +111,10 @@ function LeftColumn({
const [contactsFilter, setContactsFilter] = useState<string>(''); const [contactsFilter, setContactsFilter] = useState<string>('');
const [foldersState, foldersDispatch] = useFoldersReducer(); const [foldersState, foldersDispatch] = useFoldersReducer();
const debouncedSetGlobalSearchQuery = useMemo(() => debounce((query: string) => {
setGlobalSearchQuery({ query });
}, 200, false, true), [setGlobalSearchQuery]);
// Used to reset child components in background. // Used to reset child components in background.
const [lastResetTime, setLastResetTime] = useState<number>(0); const [lastResetTime, setLastResetTime] = useState<number>(0);
@ -375,7 +380,7 @@ function LeftColumn({
openLeftColumnContent({ contentKey: LeftColumnContent.GlobalSearch }); openLeftColumnContent({ contentKey: LeftColumnContent.GlobalSearch });
if (query !== searchQuery) { if (query !== searchQuery) {
setGlobalSearchQuery({ query }); debouncedSetGlobalSearchQuery(query);
} }
}); });

View File

@ -114,6 +114,7 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
setGlobalSearchChatId, setGlobalSearchChatId,
lockScreen, lockScreen,
openSettingsScreen, openSettingsScreen,
searchMessagesGlobal,
} = getActions(); } = getActions();
const oldLang = useOldLang(); const oldLang = useOldLang();
@ -193,6 +194,15 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
lockScreen(); lockScreen();
}); });
const handleSearchEnter = useLastCallback(() => {
if (searchQuery && content === LeftColumnContent.GlobalSearch) {
searchMessagesGlobal({
type: 'publicPosts',
shouldResetResultsByType: true,
});
}
});
const isSearchRelevant = Boolean(globalSearchChatId) const isSearchRelevant = Boolean(globalSearchChatId)
|| content === LeftColumnContent.GlobalSearch || content === LeftColumnContent.GlobalSearch
|| content === LeftColumnContent.Contacts; || content === LeftColumnContent.Contacts;
@ -295,6 +305,7 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
onReset={onReset} onReset={onReset}
onFocus={handleSearchFocus} onFocus={handleSearchFocus}
onSpinnerClick={connectionStatusPosition === 'minimized' ? toggleConnectionStatus : undefined} onSpinnerClick={connectionStatusPosition === 'minimized' ? toggleConnectionStatus : undefined}
onEnter={handleSearchEnter}
> >
{searchContent} {searchContent}
<StoryToggler <StoryToggler
@ -344,7 +355,8 @@ export default memo(withGlobal<OwnProps>(
return { return {
searchQuery, searchQuery,
isLoading: fetchingStatus ? Boolean(fetchingStatus.chats || fetchingStatus.messages) : false, isLoading: fetchingStatus ? Boolean(fetchingStatus.chats
|| fetchingStatus.messages || fetchingStatus.publicPosts) : false,
globalSearchChatId: chatId, globalSearchChatId: chatId,
searchDate: minDate, searchDate: minDate,
theme: selectTheme(global), theme: selectTheme(global),

View File

@ -134,6 +134,7 @@ const AudioResults: FC<OwnProps & StateProps> = ({
{!canRenderContents && <Loading />} {!canRenderContents && <Loading />}
{canRenderContents && (!foundIds || foundIds.length === 0) && ( {canRenderContents && (!foundIds || foundIds.length === 0) && (
<NothingFound <NothingFound
withSticker
text={lang('ChatList.Search.NoResults')} text={lang('ChatList.Search.NoResults')}
description={lang('ChatList.Search.NoResultsDescription')} description={lang('ChatList.Search.NoResultsDescription')}
/> />

View File

@ -89,6 +89,7 @@ const BotAppResults: FC<OwnProps & StateProps> = ({
{!canRenderContents && <Loading />} {!canRenderContents && <Loading />}
{canRenderContents && !filteredFoundIds?.length && ( {canRenderContents && !filteredFoundIds?.length && (
<NothingFound <NothingFound
withSticker
text={lang('ChatList.Search.NoResults')} text={lang('ChatList.Search.NoResults')}
description={lang('ChatList.Search.NoResultsDescription')} description={lang('ChatList.Search.NoResultsDescription')}
/> />

View File

@ -132,6 +132,7 @@ const ChatMessageResults: FC<OwnProps & StateProps> = ({
)} )}
{nothingFound && ( {nothingFound && (
<NothingFound <NothingFound
withSticker
text={lang('ChatList.Search.NoResults')} text={lang('ChatList.Search.NoResults')}
description={lang('ChatList.Search.NoResultsDescription')} description={lang('ChatList.Search.NoResultsDescription')}
/> />

View File

@ -374,6 +374,7 @@ const ChatResults: FC<OwnProps & StateProps> = ({
)} )}
{nothingFound && ( {nothingFound && (
<NothingFound <NothingFound
withSticker
text={oldLang('ChatList.Search.NoResults')} text={oldLang('ChatList.Search.NoResults')}
description={oldLang('ChatList.Search.NoResultsDescription')} description={oldLang('ChatList.Search.NoResultsDescription')}
/> />

View File

@ -139,6 +139,7 @@ const FileResults: FC<OwnProps & StateProps> = ({
{!canRenderContents && <Loading />} {!canRenderContents && <Loading />}
{canRenderContents && (!foundIds || foundIds.length === 0) && ( {canRenderContents && (!foundIds || foundIds.length === 0) && (
<NothingFound <NothingFound
withSticker
text={lang('ChatList.Search.NoResults')} text={lang('ChatList.Search.NoResults')}
description={lang('ChatList.Search.NoResultsDescription')} description={lang('ChatList.Search.NoResultsDescription')}
/> />

View File

@ -1,6 +1,7 @@
import type { FC } from '../../../lib/teact/teact'; import type { FC } from '../../../lib/teact/teact';
import { import {
memo, memo,
useEffect,
useMemo, useMemo,
useRef, useRef,
useState, useState,
@ -27,6 +28,7 @@ import ChatResults from './ChatResults';
import FileResults from './FileResults'; import FileResults from './FileResults';
import LinkResults from './LinkResults'; import LinkResults from './LinkResults';
import MediaResults from './MediaResults'; import MediaResults from './MediaResults';
import PublicPostsResults from './PublicPostsResults';
import './LeftSearch.scss'; import './LeftSearch.scss';
@ -51,6 +53,7 @@ const TABS: TabInfo[] = [
{ type: GlobalSearchContent.ChatList, key: 'SearchTabChats' }, { type: GlobalSearchContent.ChatList, key: 'SearchTabChats' },
{ type: GlobalSearchContent.ChannelList, key: 'SearchTabChannels' }, { type: GlobalSearchContent.ChannelList, key: 'SearchTabChannels' },
{ type: GlobalSearchContent.BotApps, key: 'SearchTabApps' }, { type: GlobalSearchContent.BotApps, key: 'SearchTabApps' },
{ type: GlobalSearchContent.PublicPosts, key: 'SearchTabPublicPosts' },
{ type: GlobalSearchContent.Media, key: 'SearchTabMedia' }, { type: GlobalSearchContent.Media, key: 'SearchTabMedia' },
{ type: GlobalSearchContent.Links, key: 'SearchTabLinks' }, { type: GlobalSearchContent.Links, key: 'SearchTabLinks' },
{ type: GlobalSearchContent.Files, key: 'SearchTabFiles' }, { type: GlobalSearchContent.Files, key: 'SearchTabFiles' },
@ -74,12 +77,19 @@ const LeftSearch: FC<OwnProps & StateProps> = ({
const { const {
setGlobalSearchContent, setGlobalSearchContent,
setGlobalSearchDate, setGlobalSearchDate,
checkSearchPostsFlood,
} = getActions(); } = getActions();
const lang = useLang(); const lang = useLang();
const [activeTab, setActiveTab] = useState(currentContent); const [activeTab, setActiveTab] = useState(currentContent);
const dateSearchQuery = useMemo(() => parseDateString(searchQuery), [searchQuery]); const dateSearchQuery = useMemo(() => parseDateString(searchQuery), [searchQuery]);
useEffect(() => {
if (isActive) {
checkSearchPostsFlood({});
}
}, [isActive]);
const tabs = useMemo(() => { const tabs = useMemo(() => {
const arr = chatId ? CHAT_TABS : TABS; const arr = chatId ? CHAT_TABS : TABS;
return arr.map((tab) => ({ return arr.map((tab) => ({
@ -166,6 +176,13 @@ const LeftSearch: FC<OwnProps & StateProps> = ({
searchQuery={searchQuery} searchQuery={searchQuery}
/> />
); );
case GlobalSearchContent.PublicPosts:
return (
<PublicPostsResults
key="publicPosts"
searchQuery={searchQuery}
/>
);
default: default:
return undefined; return undefined;
} }

View File

@ -132,6 +132,7 @@ const LinkResults: FC<OwnProps & StateProps> = ({
{!canRenderContents && <Loading />} {!canRenderContents && <Loading />}
{canRenderContents && (!foundIds || foundIds.length === 0) && ( {canRenderContents && (!foundIds || foundIds.length === 0) && (
<NothingFound <NothingFound
withSticker
text={lang('ChatList.Search.NoResults')} text={lang('ChatList.Search.NoResults')}
description={lang('ChatList.Search.NoResultsDescription')} description={lang('ChatList.Search.NoResultsDescription')}
/> />

View File

@ -133,6 +133,7 @@ const MediaResults: FC<OwnProps & StateProps> = ({
{!canRenderContents && <Loading />} {!canRenderContents && <Loading />}
{canRenderContents && (!foundIds || foundIds.length === 0) && ( {canRenderContents && (!foundIds || foundIds.length === 0) && (
<NothingFound <NothingFound
withSticker
text={lang('ChatList.Search.NoResults')} text={lang('ChatList.Search.NoResults')}
description={lang('ChatList.Search.NoResultsDescription')} description={lang('ChatList.Search.NoResultsDescription')}
/> />

View File

@ -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<string, { byId: Record<number, ApiMessage> }>;
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 (
<ChatMessage
key={`${message.chatId}-${message.id}`}
chatId={message.chatId}
message={message}
searchQuery={searchQuery}
/>
);
}
return (
<Transition
name={lang.isRtl ? 'slideOptimizedRtl' : 'slideOptimized'}
activeKey={shouldShowSearchLauncher ? 0 : 1}
>
{shouldShowSearchLauncher ? (
<PublicPostsSearchLauncher
searchQuery={searchQuery}
searchFlood={searchFlood}
onSearch={handleSearch}
/>
) : (
<div className="LeftSearch--content">
<InfiniteScroll
key={searchQuery}
className="search-content custom-scroll chat-list"
items={foundMessages}
onLoadMore={handleLoadMore}
noFastList
>
{isNothingFound && (
<NothingFound
text={oldLang('ChatList.Search.NoResults')}
description={oldLang('ChatList.Search.NoResultsDescription')}
withSticker
/>
)}
{Boolean(foundMessages.length) && (
<div className="pb-2">
<h3 className="section-heading" dir={lang.isRtl ? 'auto' : undefined}>
{lang('PublicPosts')}
</h3>
{foundMessages.map(renderFoundMessage)}
</div>
)}
</InfiniteScroll>
</div>
)}
</Transition>
);
};
export default memo(withGlobal<OwnProps>(
(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));

View File

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

View File

@ -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 (
<div className={styles.container}>
<div className={styles.content}>
<AnimatedIconWithPreview
className={styles.sticker}
size={120}
tgsUrl={LOCAL_TGS_URLS.Search}
previewUrl={LOCAL_TGS_PREVIEW_URLS.Search}
nonInteractive
noLoop={false}
/>
<div className={styles.limitTitle}>
{lang('PublicPostsLimitReached')}
</div>
<div className={styles.limitDescription}>
{lang('HintPublicPostsSearchQuota', { count: totalDaily }, { pluralValue: totalDaily })}
</div>
<Button
className={styles.paidSearchButton}
color="primary"
size="smaller"
disabled={!searchQuery}
noForcedUpperCase
onClick={handlePaidSearchClick}
>
{lang('PublicPostsSearchForStars', {
stars: formatStarsAsIcon(lang, starsAmount, { asFont: true }),
}, { withNodes: true })}
</Button>
{Boolean(waitTill) && (
<div className={styles.freeSearchUnlock}>
<TextTimer
langKey="UnlockTimerPublicPostsSearch"
endsAt={waitTill + WAIT_DELAY}
onEnd={onCheckFlood}
/>
</div>
)}
</div>
</div>
);
};
const renderSearchButton = () => {
const remainingSearches = searchFlood?.remains || 0;
return (
<div className={styles.container}>
<div className={styles.content}>
<AnimatedIconWithPreview
className={styles.sticker}
size={120}
tgsUrl={LOCAL_TGS_URLS.Search}
previewUrl={LOCAL_TGS_PREVIEW_URLS.Search}
nonInteractive
noLoop={false}
/>
<div className={styles.title}>
{lang('GlobalSearch')}
</div>
<div className={styles.description}>
{lang('DescriptionPublicPostsSearch')}
</div>
<Button
className={styles.searchButton}
color="primary"
size="smaller"
noForcedUpperCase
disabled={!searchQuery}
onClick={handleSearchClick}
>
<Transition
name="fade"
activeKey={searchButtonActiveKey}
>
<div className={styles.searchButtonContent}>
<Icon name="search" className={styles.searchIcon} />
{lang('ButtonSearchPublicPosts', {
query: searchQuery ? <span className={styles.searchQuery}>{searchQuery}</span> : '',
}, { withNodes: true })}
{searchQuery && <Icon name="next" className={styles.nextIcon} />}
</div>
</Transition>
</Button>
<div className={styles.remainingSearches}>
{lang('RemainingPublicPostsSearch', { count: remainingSearches }, { pluralValue: remainingSearches })}
</div>
</div>
</div>
);
};
const renderPremiumRequired = () => {
return (
<div className={styles.container}>
<div className={styles.content}>
<div className={styles.premiumTitle}>
{lang('GlobalSearch')}
</div>
<div className={styles.premiumDescription}>
{lang('PublicPostsPremiumFeatureDescription')}
</div>
<Button
className={styles.subscribePremiumButton}
color="primary"
size="smaller"
noForcedUpperCase
onClick={handleSubscribePremiumClick}
>
{lang('PublicPostsSubscribeToPremium')}
</Button>
<div className={styles.premiumSubtitle}>
{lang('PublicPostsPremiumFeatureSubtitle')}
</div>
</div>
</div>
);
};
if (!isCurrentUserPremium) {
return renderPremiumRequired();
}
const serverTime = getServerTime();
const shouldRenderPaidScreen = searchFlood?.remains === 0
|| (searchFlood?.waitTill && searchFlood.waitTill > serverTime);
return (
<Transition
name="fade"
activeKey={shouldRenderPaidScreen ? 0 : 1}
>
{shouldRenderPaidScreen ? renderLimitReached() : renderSearchButton()}
</Transition>
);
};
export default memo(withGlobal<OwnProps>((global): StateProps => ({
isCurrentUserPremium: selectIsCurrentUserPremium(global),
starsBalance: global.stars?.balance?.amount || 0,
}))(PublicPostsSearchLauncher));

View File

@ -2,7 +2,7 @@ import type { ApiStarsAmount, ApiStarsTransaction, ApiTypeCurrencyAmount } from
import type { OldLangFn } from '../../../../hooks/useOldLang'; import type { OldLangFn } from '../../../../hooks/useOldLang';
import { STARS_CURRENCY_CODE, TON_CURRENCY_CODE } from '../../../../config'; import { STARS_CURRENCY_CODE, TON_CURRENCY_CODE } from '../../../../config';
import { buildStarsTransactionCustomPeer } from '../../../../global/helpers/payments'; import { buildStarsTransactionCustomPeer, shouldUseCustomPeer } from '../../../../global/helpers/payments';
import { import {
type LangFn, type LangFn,
} from '../../../../util/localization'; } from '../../../../util/localization';
@ -25,6 +25,9 @@ export function getTransactionTitle(oldLang: OldLangFn, lang: LangFn, transactio
? lang('StarGiftSaleTransaction') ? lang('StarGiftSaleTransaction')
: lang('StarGiftPurchaseTransaction'); : lang('StarGiftPurchaseTransaction');
} }
if (transaction.isPostsSearch) {
return lang('PostsSearchTransaction');
}
if (transaction.starRefCommision) { if (transaction.starRefCommision) {
return oldLang('StarTransactionCommission', formatPercent(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'); return isNegativeAmount(transaction.amount) ? oldLang('Gift2TransactionSent') : oldLang('Gift2ConvertedTitle');
} }
const customPeer = (transaction.peer && transaction.peer.type !== 'peer' const customPeer = (transaction.peer && shouldUseCustomPeer(transaction)
&& buildStarsTransactionCustomPeer(transaction.peer)) || undefined; && buildStarsTransactionCustomPeer(transaction)) || undefined;
if (customPeer) return customPeer.title || oldLang(customPeer.titleKey!); if (customPeer) return customPeer.title || oldLang(customPeer.titleKey!);

View File

@ -9,7 +9,9 @@ import type { GlobalState } from '../../../../global/types';
import type { CustomPeer } from '../../../../types'; import type { CustomPeer } from '../../../../types';
import { STARS_CURRENCY_CODE, TON_CURRENCY_CODE } from '../../../../config'; 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 { getPeerTitle } from '../../../../global/helpers/peers';
import { selectPeer } from '../../../../global/selectors'; import { selectPeer } from '../../../../global/selectors';
import buildClassName from '../../../../util/buildClassName'; import buildClassName from '../../../../util/buildClassName';
@ -71,14 +73,11 @@ const StarsTransactionItem = ({ transaction, className }: OwnProps) => {
let status: string | undefined; let status: string | undefined;
let avatarPeer: ApiPeer | CustomPeer | undefined; let avatarPeer: ApiPeer | CustomPeer | undefined;
if (transaction.peer.type === 'peer') { if (!shouldUseCustomPeer(transaction)) {
description = peer && getPeerTitle(oldLang, peer); description = peer && getPeerTitle(oldLang, peer);
avatarPeer = peer || CUSTOM_PEER_PREMIUM; avatarPeer = peer || CUSTOM_PEER_PREMIUM;
} else { } else {
const customPeer = buildStarsTransactionCustomPeer( const customPeer = buildStarsTransactionCustomPeer(transaction);
transaction.peer,
transaction.amount.currency === TON_CURRENCY_CODE,
);
title = customPeer.title || oldLang(customPeer.titleKey!); title = customPeer.title || oldLang(customPeer.titleKey!);
description = oldLang(customPeer.subtitleKey!); description = oldLang(customPeer.subtitleKey!);
avatarPeer = customPeer; avatarPeer = customPeer;
@ -92,6 +91,11 @@ const StarsTransactionItem = ({ transaction, className }: OwnProps) => {
description = lang('GiftUnique', { title: transaction.starGift.title, number: transaction.starGift.number }); description = lang('GiftUnique', { title: transaction.starGift.title, number: transaction.starGift.number });
} }
if (transaction.isPostsSearch) {
title = getTransactionTitle(oldLang, lang, transaction);
description = undefined;
}
if (transaction.photo) { if (transaction.photo) {
avatarPeer = undefined; avatarPeer = undefined;
} }

View File

@ -14,6 +14,7 @@ import { getMessageLink } from '../../../../global/helpers';
import { import {
buildStarsTransactionCustomPeer, buildStarsTransactionCustomPeer,
formatStarsTransactionAmount, formatStarsTransactionAmount,
shouldUseCustomPeer,
} from '../../../../global/helpers/payments'; } from '../../../../global/helpers/payments';
import { import {
selectCanPlayAnimatedEmojis, selectCanPlayAnimatedEmojis,
@ -97,8 +98,8 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
const giftAttributes = isUniqueGift ? getGiftAttributes(gift) : undefined; const giftAttributes = isUniqueGift ? getGiftAttributes(gift) : undefined;
const customPeer = (transaction.peer && transaction.peer.type !== 'peer' const customPeer = (transaction.peer && shouldUseCustomPeer(transaction)
&& buildStarsTransactionCustomPeer(transaction.peer)) || undefined; && buildStarsTransactionCustomPeer(transaction)) || undefined;
const peerId = transaction.peer?.type === 'peer' ? transaction.peer.id : undefined; const peerId = transaction.peer?.type === 'peer' ? transaction.peer.id : undefined;
const toName = transaction.peer && oldLang(getStarsPeerTitleKey(transaction.peer)); const toName = transaction.peer && oldLang(getStarsPeerTitleKey(transaction.peer));
@ -123,8 +124,8 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
|| (isGiftUpgrade && starGift?.type === 'starGiftUnique' ? starGift.title : undefined) || (isGiftUpgrade && starGift?.type === 'starGiftUnique' ? starGift.title : undefined)
|| (media ? mediaText : undefined); || (media ? mediaText : undefined);
const shouldDisplayAvatar = !media && !sticker; const shouldDisplayAvatar = !media && !sticker && !transaction.isPostsSearch;
const avatarPeer = !photo ? (peer || customPeer) : undefined; const avatarPeer = !photo ? ((!shouldUseCustomPeer(transaction) && peer) || customPeer) : undefined;
const uniqueGiftHeader = isUniqueGift && ( const uniqueGiftHeader = isUniqueGift && (
<div className={buildClassName(styles.header, styles.uniqueGift)}> <div className={buildClassName(styles.header, styles.uniqueGift)}>
@ -161,7 +162,7 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
{shouldDisplayAvatar && ( {shouldDisplayAvatar && (
<Avatar peer={avatarPeer} webPhoto={photo} size="giant" /> <Avatar peer={avatarPeer} webPhoto={photo} size="giant" />
)} )}
{!sticker && ( {!sticker && !transaction.isPostsSearch && (
<img <img
className={buildClassName(styles.starsBackground)} className={buildClassName(styles.starsBackground)}
src={StarsBackground} src={StarsBackground}
@ -236,10 +237,12 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
peerLabel = oldLang('Stars.Transaction.Via'); peerLabel = oldLang('Stars.Transaction.Via');
} }
tableData.push([ if (!transaction.isPostsSearch) {
peerLabel, tableData.push([
peerId ? { chatId: peerId } : toName || '', peerLabel,
]); peerId ? { chatId: peerId } : toName || '',
]);
}
if (transaction.starRefCommision && transaction.paidMessages) { if (transaction.starRefCommision && transaction.paidMessages) {
tableData.push([ tableData.push([
@ -329,8 +332,8 @@ export default memo(withGlobal<OwnProps>(
const currencyAmount = modal?.transaction.amount; const currencyAmount = modal?.transaction.amount;
const starsGiftSticker = modal?.transaction.isGift const starsGiftSticker = modal?.transaction.isGift
&& currencyAmount?.currency === STARS_CURRENCY_CODE ? selectGiftStickerForStars(global, currencyAmount?.amount) ? (currencyAmount?.currency === STARS_CURRENCY_CODE ? selectGiftStickerForStars(global, currencyAmount?.amount)
: selectGiftStickerForTon(global, currencyAmount?.amount); : selectGiftStickerForTon(global, currencyAmount?.amount)) : undefined;
return { return {
peer, peer,

View File

@ -53,7 +53,7 @@
background-size: cover; background-size: cover;
outline: none !important; outline: none !important;
transition: background-color 0.15s, color 0.15s; transition: background-color 0.15s, color 0.15s, opacity 0.15s;
// @optimization // @optimization
&:active, &:active,

View File

@ -49,6 +49,7 @@ type OwnProps = {
onUpClick?: (event: React.MouseEvent<HTMLButtonElement>) => void; onUpClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
onDownClick?: (event: React.MouseEvent<HTMLButtonElement>) => void; onDownClick?: (event: React.MouseEvent<HTMLButtonElement>) => void;
onSpinnerClick?: NoneToVoidFunction; onSpinnerClick?: NoneToVoidFunction;
onEnter?: NoneToVoidFunction;
}; };
const SearchInput: FC<OwnProps> = ({ const SearchInput: FC<OwnProps> = ({
@ -80,6 +81,7 @@ const SearchInput: FC<OwnProps> = ({
onUpClick, onUpClick,
onDownClick, onDownClick,
onSpinnerClick, onSpinnerClick,
onEnter,
}) => { }) => {
let inputRef = useRef<HTMLInputElement>(); let inputRef = useRef<HTMLInputElement>();
if (ref) { if (ref) {
@ -125,8 +127,22 @@ const SearchInput: FC<OwnProps> = ({
} }
const handleKeyDown = useLastCallback((e: React.KeyboardEvent<HTMLInputElement>) => { const handleKeyDown = useLastCallback((e: React.KeyboardEvent<HTMLInputElement>) => {
if (!resultsItemSelector) return; if (e.key === 'Enter') {
if (e.key === 'ArrowDown' || 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; const element = document.querySelector(resultsItemSelector) as HTMLElement;
if (element) { if (element) {
element.focus(); element.focus();

View File

@ -5,8 +5,11 @@ import { getServerTime } from '../../util/serverTime';
import useInterval from '../../hooks/schedulers/useInterval'; import useInterval from '../../hooks/schedulers/useInterval';
import useForceUpdate from '../../hooks/useForceUpdate'; import useForceUpdate from '../../hooks/useForceUpdate';
import useLang from '../../hooks/useLang';
import useOldLang from '../../hooks/useOldLang'; import useOldLang from '../../hooks/useOldLang';
import AnimatedCounter from '../common/AnimatedCounter';
type OwnProps = { type OwnProps = {
langKey: string; langKey: string;
endsAt: number; endsAt: number;
@ -16,7 +19,8 @@ type OwnProps = {
const UPDATE_FREQUENCY = 500; // Sometimes second gets skipped if using 1000 const UPDATE_FREQUENCY = 500; // Sometimes second gets skipped if using 1000
const TextTimer: FC<OwnProps> = ({ langKey, endsAt, onEnd }) => { const TextTimer: FC<OwnProps> = ({ langKey, endsAt, onEnd }) => {
const lang = useOldLang(); const lang = useLang();
const oldLang = useOldLang();
const forceUpdate = useForceUpdate(); const forceUpdate = useForceUpdate();
const serverTime = getServerTime(); const serverTime = getServerTime();
@ -32,11 +36,33 @@ const TextTimer: FC<OwnProps> = ({ langKey, endsAt, onEnd }) => {
if (!isActive) return undefined; if (!isActive) return undefined;
const timeLeft = endsAt - serverTime; const timeLeft = endsAt - serverTime;
const formattedTime = formatMediaDuration(timeLeft); const time = formatMediaDuration(timeLeft);
const timeParts = time.split(':');
const timeCounter = (
<span style="font-variant-numeric: tabular-nums;">
{timeParts.map((part, index) => (
<>
{index > 0 && ':'}
<AnimatedCounter key={index} text={part} />
</>
))}
</span>
);
const isTypedKey = langKey === 'UnlockTimerPublicPostsSearch';
if (isTypedKey) {
return (
<span>
{lang(langKey, { time: timeCounter }, { withNodes: true })}
</span>
);
}
return ( return (
<span> <span>
{lang(langKey, formattedTime)} {oldLang(langKey, time)}
</span> </span>
); );
}; };

View File

@ -110,6 +110,10 @@ export const TODO_ITEMS_LIMIT = 30;
export const TODO_TITLE_LENGTH_LIMIT = 32; export const TODO_TITLE_LENGTH_LIMIT = 32;
export const TODO_ITEM_LENGTH_LIMIT = 64; 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 // Suggested Posts defaults
export const STARS_SUGGESTED_POST_AMOUNT_MAX = 100000; export const STARS_SUGGESTED_POST_AMOUNT_MAX = 100000;
export const STARS_SUGGESTED_POST_AMOUNT_MIN = 5; export const STARS_SUGGESTED_POST_AMOUNT_MIN = 5;

View File

@ -1,5 +1,7 @@
import { getActions } from '../../../global';
import type { import type {
ApiChat, ApiGlobalMessageSearchType, ApiMessage, ApiMessageSearchContext, ApiPeer, ApiTopic, ApiChat, ApiGlobalMessageSearchType, ApiMessage, ApiMessageSearchContext, ApiPeer, ApiSearchPostsFlood, ApiTopic,
ApiUserStatus, ApiUserStatus,
} from '../../../api/types'; } from '../../../api/types';
import type { ActionReturnType, GlobalState, TabArgs } from '../../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 { isDeepLink, tryParseDeepLink } from '../../../util/deepLinkParser';
import { toChannelId } from '../../../util/entities/ids'; import { toChannelId } from '../../../util/entities/ids';
import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { getTranslationFn } from '../../../util/localization';
import { formatStarsAsText } from '../../../util/localization/format';
import { throttle } from '../../../util/schedulers'; import { throttle } from '../../../util/schedulers';
import { callApi } from '../../../api/gramjs'; import { callApi } from '../../../api/gramjs';
import { isChatChannel, isChatGroup } from '../../helpers/chats'; import { isChatChannel, isChatGroup } from '../../helpers/chats';
@ -158,6 +162,23 @@ addActionHandler('searchPopularBotApps', async (global, actions, payload): Promi
setGlobal(global); setGlobal(global);
}); });
addActionHandler('checkSearchPostsFlood', async (global, actions, payload): Promise<void> => {
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<T extends GlobalState>(global: T, params: { async function searchMessagesGlobal<T extends GlobalState>(global: T, params: {
query?: string; query?: string;
type: ApiGlobalMessageSearchType; type: ApiGlobalMessageSearchType;
@ -175,6 +196,12 @@ async function searchMessagesGlobal<T extends GlobalState>(global: T, params: {
query = '', type, context, offsetRate, offsetId, offsetPeer, query = '', type, context, offsetRate, offsetId, offsetPeer,
peer, maxDate, minDate, shouldResetResultsByType, tabId = getCurrentTabId(), peer, maxDate, minDate, shouldResetResultsByType, tabId = getCurrentTabId(),
} = params; } = params;
if (type === 'publicPosts') {
global = updateGlobalSearchFetchingStatus(global, { publicPosts: true }, tabId);
setGlobal(global);
}
let result: { let result: {
messages: ApiMessage[]; messages: ApiMessage[];
userStatusesById?: Record<number, ApiUserStatus>; userStatusesById?: Record<number, ApiUserStatus>;
@ -184,10 +211,13 @@ async function searchMessagesGlobal<T extends GlobalState>(global: T, params: {
nextOffsetRate?: number; nextOffsetRate?: number;
nextOffsetId?: number; nextOffsetId?: number;
nextOffsetPeerId?: string; nextOffsetPeerId?: string;
searchFlood?: ApiSearchPostsFlood;
} | undefined; } | undefined;
let messageLink: ApiMessage | undefined; let messageLink: ApiMessage | undefined;
const previousSearchFlood = selectTabState(global, tabId).globalSearch.searchFlood;
if (peer) { if (peer) {
const inChatResultRequest = callApi('searchMessagesInChat', { const inChatResultRequest = callApi('searchMessagesInChat', {
peer, peer,
@ -256,7 +286,7 @@ async function searchMessagesGlobal<T extends GlobalState>(global: T, params: {
} }
const currentSearchQuery = selectCurrentGlobalSearchQuery(global, tabId); const currentSearchQuery = selectCurrentGlobalSearchQuery(global, tabId);
if (!result || (query !== '' && query !== currentSearchQuery)) { if (!result || (query !== '' && query !== currentSearchQuery)) {
global = updateGlobalSearchFetchingStatus(global, { messages: false }, tabId); global = updateGlobalSearchFetchingStatus(global, { messages: false, publicPosts: false }, tabId);
setGlobal(global); setGlobal(global);
return; return;
} }
@ -269,6 +299,8 @@ async function searchMessagesGlobal<T extends GlobalState>(global: T, params: {
messages, userStatusesById, totalCount, nextOffsetRate, nextOffsetId, nextOffsetPeerId, messages, userStatusesById, totalCount, nextOffsetRate, nextOffsetId, nextOffsetPeerId,
} = result; } = result;
const searchFlood = result.searchFlood || previousSearchFlood;
if (userStatusesById) { if (userStatusesById) {
global = addUserStatuses(global, userStatusesById); global = addUserStatuses(global, userStatusesById);
} }
@ -285,6 +317,7 @@ async function searchMessagesGlobal<T extends GlobalState>(global: T, params: {
nextOffsetRate, nextOffsetRate,
nextOffsetId, nextOffsetId,
nextOffsetPeerId, nextOffsetPeerId,
searchFlood,
tabId, tabId,
); );
@ -298,6 +331,20 @@ async function searchMessagesGlobal<T extends GlobalState>(global: T, params: {
}, tabId); }, tabId);
setGlobal(global); 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 }) { async function getMessageByPublicLink(global: GlobalState, link: { username: string; messageId: number }) {

View File

@ -113,7 +113,7 @@ addActionHandler('performMiddleSearch', async (global, actions, payload): Promis
} }
if (type === 'channels') { if (type === 'channels') {
result = await callApi('searchHashtagPosts', { result = await callApi('searchPublicPosts', {
hashtag: query!, hashtag: query!,
limit: MESSAGE_SEARCH_SLICE, limit: MESSAGE_SEARCH_SLICE,
offsetId, offsetId,

View File

@ -12,9 +12,12 @@ addActionHandler('setGlobalSearchQuery', (global, actions, payload): ActionRetur
const { query, tabId = getCurrentTabId() } = payload; const { query, tabId = getCurrentTabId() } = payload;
const { chatId, currentContent } = selectTabState(global, tabId).globalSearch; 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; ? { chats: !chatId, messages: true } : undefined;
actions.checkSearchPostsFlood({ query, tabId });
return updateGlobalSearch(global, { return updateGlobalSearch(global, {
globalResults: {}, globalResults: {},
localResults: {}, localResults: {},

View File

@ -6,8 +6,6 @@ import type {
ApiRequestInputSavedStarGift, ApiRequestInputSavedStarGift,
ApiStarsAmount, ApiStarsAmount,
ApiStarsTransaction, ApiStarsTransaction,
ApiStarsTransactionPeer,
ApiStarsTransactionPeerPeer,
ApiTypeCurrencyAmount, ApiTypeCurrencyAmount,
} from '../../api/types'; } from '../../api/types';
import type { CustomPeer } from '../../types'; import type { CustomPeer } from '../../types';
@ -259,10 +257,25 @@ export function getRequestInputSavedStarGift<T extends GlobalState>(
return undefined; return undefined;
} }
export function shouldUseCustomPeer(transaction: ApiStarsTransaction) {
return transaction.peer.type !== 'peer' || Boolean(transaction.isPostsSearch);
}
export function buildStarsTransactionCustomPeer( export function buildStarsTransactionCustomPeer(
peer: Exclude<ApiStarsTransactionPeer, ApiStarsTransactionPeerPeer>, transaction: ApiStarsTransaction,
isForTon?: boolean,
): CustomPeer { ): 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') { if (peer.type === 'appStore') {
return { return {
avatarIcon: 'star', avatarIcon: 'star',

View File

@ -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 { GlobalSearchContent } from '../../types';
import type { GlobalState, TabArgs, TabState } from '../types'; import type { GlobalState, TabArgs, TabState } from '../types';
@ -37,6 +37,7 @@ export function updateGlobalSearchResults<T extends GlobalState>(
nextOffsetRate?: number, nextOffsetRate?: number,
nextOffsetId?: number, nextOffsetId?: number,
nextOffsetPeerId?: string, nextOffsetPeerId?: string,
searchFlood?: ApiSearchPostsFlood,
...[tabId = getCurrentTabId()]: TabArgs<T> ...[tabId = getCurrentTabId()]: TabArgs<T>
): T { ): T {
const { resultsByType } = selectTabState(global, tabId).globalSearch || {}; const { resultsByType } = selectTabState(global, tabId).globalSearch || {};
@ -52,8 +53,12 @@ export function updateGlobalSearchResults<T extends GlobalState>(
(newId) => foundIdsForType.includes(getSearchResultKey(newFoundMessagesById[newId])), (newId) => foundIdsForType.includes(getSearchResultKey(newFoundMessagesById[newId])),
) )
) { ) {
global = updateGlobalSearchFetchingStatus(global, { messages: false }, tabId); global = updateGlobalSearchFetchingStatus(global, {
messages: false,
publicPosts: false,
}, tabId);
return updateGlobalSearch(global, { return updateGlobalSearch(global, {
searchFlood,
resultsByType: { resultsByType: {
...(selectTabState(global, tabId).globalSearch || {}).resultsByType, ...(selectTabState(global, tabId).globalSearch || {}).resultsByType,
[type]: { [type]: {
@ -74,9 +79,13 @@ export function updateGlobalSearchResults<T extends GlobalState>(
const foundIds = Array.prototype.concat(prevFoundIds, newFoundIds); const foundIds = Array.prototype.concat(prevFoundIds, newFoundIds);
const foundOrPrevFoundIds = areSortedArraysEqual(prevFoundIds, foundIds) ? prevFoundIds : foundIds; const foundOrPrevFoundIds = areSortedArraysEqual(prevFoundIds, foundIds) ? prevFoundIds : foundIds;
global = updateGlobalSearchFetchingStatus(global, { messages: false }, tabId); global = updateGlobalSearchFetchingStatus(global, {
messages: false,
publicPosts: false,
}, tabId);
return updateGlobalSearch(global, { return updateGlobalSearch(global, {
searchFlood,
resultsByType: { resultsByType: {
...(selectTabState(global, tabId).globalSearch || {}).resultsByType, ...(selectTabState(global, tabId).globalSearch || {}).resultsByType,
[type]: { [type]: {
@ -91,7 +100,7 @@ export function updateGlobalSearchResults<T extends GlobalState>(
} }
export function updateGlobalSearchFetchingStatus<T extends GlobalState>( export function updateGlobalSearchFetchingStatus<T extends GlobalState>(
global: T, newState: { chats?: boolean; messages?: boolean; botApps?: boolean }, global: T, newState: { chats?: boolean; messages?: boolean; botApps?: boolean; publicPosts?: boolean },
...[tabId = getCurrentTabId()]: TabArgs<T> ...[tabId = getCurrentTabId()]: TabArgs<T>
): T { ): T {
return updateGlobalSearch(global, { return updateGlobalSearch(global, {

View File

@ -418,6 +418,9 @@ export interface ActionPayloads {
shouldCheckFetchingMessagesStatus?: boolean; shouldCheckFetchingMessagesStatus?: boolean;
} & WithTabId; } & WithTabId;
searchPopularBotApps: WithTabId | undefined; searchPopularBotApps: WithTabId | undefined;
checkSearchPostsFlood: {
query?: string;
} & WithTabId;
addRecentlyFoundChatId: { addRecentlyFoundChatId: {
id: string; id: string;
}; };

View File

@ -36,6 +36,7 @@ import type {
ApiReceiptRegular, ApiReceiptRegular,
ApiSavedGifts, ApiSavedGifts,
ApiSavedStarGift, ApiSavedStarGift,
ApiSearchPostsFlood,
ApiSponsoredPeer, ApiSponsoredPeer,
ApiStarGift, ApiStarGift,
ApiStarGiftAttribute, ApiStarGiftAttribute,
@ -249,10 +250,12 @@ export type TabState = {
chatId?: string; chatId?: string;
foundTopicIds?: number[]; foundTopicIds?: number[];
sponsoredPeer?: ApiSponsoredPeer; sponsoredPeer?: ApiSponsoredPeer;
searchFlood?: ApiSearchPostsFlood;
fetchingStatus?: { fetchingStatus?: {
chats?: boolean; chats?: boolean;
messages?: boolean; messages?: boolean;
botApps?: boolean; botApps?: boolean;
publicPosts?: boolean;
}; };
isClosing?: boolean; isClosing?: boolean;
localResults?: { localResults?: {

View File

@ -16515,6 +16515,7 @@ namespace Api {
stargiftUpgrade?: true; stargiftUpgrade?: true;
businessTransfer?: true; businessTransfer?: true;
stargiftResale?: true; stargiftResale?: true;
postsSearch?: true;
id: string; id: string;
amount: Api.TypeStarsAmount; amount: Api.TypeStarsAmount;
date: int; date: int;
@ -16548,6 +16549,7 @@ namespace Api {
stargiftUpgrade?: true; stargiftUpgrade?: true;
businessTransfer?: true; businessTransfer?: true;
stargiftResale?: true; stargiftResale?: true;
postsSearch?: true;
id: string; id: string;
amount: Api.TypeStarsAmount; amount: Api.TypeStarsAmount;
date: int; date: int;

View File

@ -1368,7 +1368,7 @@ starsTransactionPeer#d80da15d peer:Peer = StarsTransactionPeer;
starsTransactionPeerAds#60682812 = StarsTransactionPeer; starsTransactionPeerAds#60682812 = StarsTransactionPeer;
starsTransactionPeerAPI#f9677aad = StarsTransactionPeer; starsTransactionPeerAPI#f9677aad = StarsTransactionPeer;
starsTopupOption#bd915c0 flags:# extended:flags.1?true stars:long store_product:flags.0?string currency:string amount:long = StarsTopupOption; 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<MessageMedia> 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<MessageMedia> 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<StarsSubscription> subscriptions_next_offset:flags.2?string subscriptions_missing_balance:flags.4?long history:flags.3?Vector<StarsTransaction> next_offset:flags.0?string chats:Vector<Chat> users:Vector<User> = payments.StarsStatus; payments.starsStatus#6c9ce8ed flags:# balance:StarsAmount subscriptions:flags.1?Vector<StarsSubscription> subscriptions_next_offset:flags.2?string subscriptions_missing_balance:flags.4?long history:flags.3?Vector<StarsTransaction> next_offset:flags.0?string chats:Vector<Chat> users:Vector<User> = payments.StarsStatus;
foundStory#e87acbc0 peer:Peer story:StoryItem = FoundStory; foundStory#e87acbc0 peer:Peer story:StoryItem = FoundStory;
stories.foundStories#e2de7737 flags:# count:int stories:Vector<FoundStory> next_offset:flags.0?string chats:Vector<Chat> users:Vector<User> = stories.FoundStories; stories.foundStories#e2de7737 flags:# count:int stories:Vector<FoundStory> next_offset:flags.0?string chats:Vector<Chat> users:Vector<User> = 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.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.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.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.setBotInfo#10cf3123 flags:# bot:flags.2?InputUser lang_code:string name:flags.3?string about:flags.0?string description:flags.1?string = Bool;
bots.canSendMessage#1359f4e6 bot:InputUser = Bool; bots.canSendMessage#1359f4e6 bot:InputUser = Bool;
bots.allowSendMessage#f132e3ef bot:InputUser = Updates; bots.allowSendMessage#f132e3ef bot:InputUser = Updates;

View File

@ -284,6 +284,7 @@
"channels.searchPosts", "channels.searchPosts",
"channels.reportSpam", "channels.reportSpam",
"channels.updatePaidMessagesPrice", "channels.updatePaidMessagesPrice",
"channels.checkSearchPostsFlood",
"channels.toggleAutotranslation", "channels.toggleAutotranslation",
"bots.getBotRecommendations", "bots.getBotRecommendations",
"bots.canSendMessage", "bots.canSendMessage",

View File

@ -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; 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<MessageMedia> 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<MessageMedia> 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<StarsSubscription> subscriptions_next_offset:flags.2?string subscriptions_missing_balance:flags.4?long history:flags.3?Vector<StarsTransaction> next_offset:flags.0?string chats:Vector<Chat> users:Vector<User> = payments.StarsStatus; payments.starsStatus#6c9ce8ed flags:# balance:StarsAmount subscriptions:flags.1?Vector<StarsSubscription> subscriptions_next_offset:flags.2?string subscriptions_missing_balance:flags.4?long history:flags.3?Vector<StarsTransaction> next_offset:flags.0?string chats:Vector<Chat> users:Vector<User> = payments.StarsStatus;

View File

@ -313,6 +313,7 @@ export enum GlobalSearchContent {
ChatList, ChatList,
ChannelList, ChannelList,
BotApps, BotApps,
PublicPosts,
Media, Media,
Links, Links,
Files, Files,

View File

@ -1333,6 +1333,7 @@ export interface LangPair {
'SearchTabMusic': undefined; 'SearchTabMusic': undefined;
'SearchTabVoice': undefined; 'SearchTabVoice': undefined;
'SearchTabMessages': undefined; 'SearchTabMessages': undefined;
'SearchTabPublicPosts': undefined;
'StarsTransactionsAll': undefined; 'StarsTransactionsAll': undefined;
'StarsTransactionsIncoming': undefined; 'StarsTransactionsIncoming': undefined;
'StarsTransactionsOutgoing': undefined; 'StarsTransactionsOutgoing': undefined;
@ -1351,6 +1352,7 @@ export interface LangPair {
'ProfileTabSharedGroups': undefined; 'ProfileTabSharedGroups': undefined;
'ProfileTabSimilarChannels': undefined; 'ProfileTabSimilarChannels': undefined;
'ProfileTabSimilarBots': undefined; 'ProfileTabSimilarBots': undefined;
'ProfileTabPublicPosts': undefined;
'ActionUnsupportedTitle': undefined; 'ActionUnsupportedTitle': undefined;
'ActionUnsupportedDescription': undefined; 'ActionUnsupportedDescription': undefined;
'UnlockMoreSimilarBots': undefined; 'UnlockMoreSimilarBots': undefined;
@ -1620,6 +1622,14 @@ export interface LangPair {
'LabelPayInTON': undefined; 'LabelPayInTON': undefined;
'PriceChanged': undefined; 'PriceChanged': undefined;
'PayNewPrice': undefined; 'PayNewPrice': undefined;
'GlobalSearch': undefined;
'DescriptionPublicPostsSearch': undefined;
'PublicPosts': undefined;
'PublicPostsLimitReached': undefined;
'PublicPostsPremiumFeatureDescription': undefined;
'PublicPostsPremiumFeatureSubtitle': undefined;
'PublicPostsSubscribeToPremium': undefined;
'PostsSearchTransaction': undefined;
} }
export interface LangPairWithVariables<V = LangVariable> { export interface LangPairWithVariables<V = LangVariable> {
@ -2816,6 +2826,18 @@ export interface LangPairWithVariables<V = LangVariable> {
'originalAmount': V; 'originalAmount': V;
'newAmount': V; 'newAmount': V;
}; };
'ButtonSearchPublicPosts': {
'query': V;
};
'PublicPostsSearchForStars': {
'stars': V;
};
'UnlockTimerPublicPostsSearch': {
'time': V;
};
'NotificationPaidExtraSearch': {
'stars': V;
};
} }
export interface LangPairPlural { export interface LangPairPlural {
@ -3137,6 +3159,12 @@ export interface LangPairPluralWithVariables<V = LangVariable> {
'TextAgeVerificationModal': { 'TextAgeVerificationModal': {
'count': V; 'count': V;
}; };
'RemainingPublicPostsSearch': {
'count': V;
};
'HintPublicPostsSearchQuota': {
'count': V;
};
} }
export type RegularLangKey = keyof LangPair; export type RegularLangKey = keyof LangPair;
export type RegularLangKeyWithVariables = keyof LangPairWithVariables; export type RegularLangKeyWithVariables = keyof LangPairWithVariables;