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={{ '--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.

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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;
color: var(--color-text-meta);
&.with-description {
&.with-description,
&.with-sticker {
flex-direction: column;
}
.sticker {
margin-bottom: 1rem;
}
.AnimatedSticker {
margin: 0 auto;
}

View File

@ -2,26 +2,45 @@ import type { FC } from '../../lib/teact/teact';
import { memo } from '../../lib/teact/teact';
import buildClassName from '../../util/buildClassName';
import { LOCAL_TGS_PREVIEW_URLS, LOCAL_TGS_URLS } from './helpers/animatedAssets';
import renderText from './helpers/renderText';
import useOldLang from '../../hooks/useOldLang';
import useShowTransitionDeprecated from '../../hooks/useShowTransitionDeprecated';
import AnimatedIconWithPreview from './AnimatedIconWithPreview';
import './NothingFound.scss';
interface OwnProps {
text?: string;
description?: string;
withSticker?: boolean;
}
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 { transitionClassNames } = useShowTransitionDeprecated(true);
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}
{description && <p className="description">{renderText(lang(description), ['br'])}</p>}
</div>

View File

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

View File

@ -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<string>('');
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<number>(0);
@ -375,7 +380,7 @@ function LeftColumn({
openLeftColumnContent({ contentKey: LeftColumnContent.GlobalSearch });
if (query !== searchQuery) {
setGlobalSearchQuery({ query });
debouncedSetGlobalSearchQuery(query);
}
});

View File

@ -114,6 +114,7 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
setGlobalSearchChatId,
lockScreen,
openSettingsScreen,
searchMessagesGlobal,
} = getActions();
const oldLang = useOldLang();
@ -193,6 +194,15 @@ const LeftMainHeader: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
onReset={onReset}
onFocus={handleSearchFocus}
onSpinnerClick={connectionStatusPosition === 'minimized' ? toggleConnectionStatus : undefined}
onEnter={handleSearchEnter}
>
{searchContent}
<StoryToggler
@ -344,7 +355,8 @@ export default memo(withGlobal<OwnProps>(
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),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
searchQuery={searchQuery}
/>
);
case GlobalSearchContent.PublicPosts:
return (
<PublicPostsResults
key="publicPosts"
searchQuery={searchQuery}
/>
);
default:
return undefined;
}

View File

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

View File

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

View File

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

View File

@ -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<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
|| (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 && (
<div className={buildClassName(styles.header, styles.uniqueGift)}>
@ -161,7 +162,7 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
{shouldDisplayAvatar && (
<Avatar peer={avatarPeer} webPhoto={photo} size="giant" />
)}
{!sticker && (
{!sticker && !transaction.isPostsSearch && (
<img
className={buildClassName(styles.starsBackground)}
src={StarsBackground}
@ -236,10 +237,12 @@ const StarsTransactionModal: FC<OwnProps & StateProps> = ({
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<OwnProps>(
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,

View File

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

View File

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

View File

@ -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<OwnProps> = ({ 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<OwnProps> = ({ 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 = (
<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 (
<span>
{lang(langKey, formattedTime)}
{oldLang(langKey, time)}
</span>
);
};

View File

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

View File

@ -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<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: {
query?: string;
type: ApiGlobalMessageSearchType;
@ -175,6 +196,12 @@ async function searchMessagesGlobal<T extends GlobalState>(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<number, ApiUserStatus>;
@ -184,10 +211,13 @@ async function searchMessagesGlobal<T extends GlobalState>(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<T extends GlobalState>(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<T extends GlobalState>(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<T extends GlobalState>(global: T, params: {
nextOffsetRate,
nextOffsetId,
nextOffsetPeerId,
searchFlood,
tabId,
);
@ -298,6 +331,20 @@ async function searchMessagesGlobal<T extends GlobalState>(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 }) {

View File

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

View File

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

View File

@ -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<T extends GlobalState>(
return undefined;
}
export function shouldUseCustomPeer(transaction: ApiStarsTransaction) {
return transaction.peer.type !== 'peer' || Boolean(transaction.isPostsSearch);
}
export function buildStarsTransactionCustomPeer(
peer: Exclude<ApiStarsTransactionPeer, ApiStarsTransactionPeerPeer>,
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',

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 { GlobalState, TabArgs, TabState } from '../types';
@ -37,6 +37,7 @@ export function updateGlobalSearchResults<T extends GlobalState>(
nextOffsetRate?: number,
nextOffsetId?: number,
nextOffsetPeerId?: string,
searchFlood?: ApiSearchPostsFlood,
...[tabId = getCurrentTabId()]: TabArgs<T>
): T {
const { resultsByType } = selectTabState(global, tabId).globalSearch || {};
@ -52,8 +53,12 @@ export function updateGlobalSearchResults<T extends GlobalState>(
(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<T extends GlobalState>(
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<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>
): T {
return updateGlobalSearch(global, {

View File

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

View File

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

View File

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

View File

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

View File

@ -284,6 +284,7 @@
"channels.searchPosts",
"channels.reportSpam",
"channels.updatePaidMessagesPrice",
"channels.checkSearchPostsFlood",
"channels.toggleAutotranslation",
"bots.getBotRecommendations",
"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;
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;

View File

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

View File

@ -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<V = LangVariable> {
@ -2816,6 +2826,18 @@ export interface LangPairWithVariables<V = LangVariable> {
'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<V = LangVariable> {
'TextAgeVerificationModal': {
'count': V;
};
'RemainingPublicPostsSearch': {
'count': V;
};
'HintPublicPostsSearchQuota': {
'count': V;
};
}
export type RegularLangKey = keyof LangPair;
export type RegularLangKeyWithVariables = keyof LangPairWithVariables;