Global Search: Show sponsored results (#5844)

Co-authored-by: Dmitry Kabanov <dmitrykabanovdev@gmail.com>
Co-authored-by: Dmitry Kabanov <153344039+dmitrykabanovdev@users.noreply.github.com>
This commit is contained in:
zubiden 2025-04-23 18:59:25 +02:00 committed by Alexander Zinchuk
parent 3087fb85cc
commit 97bd26df00
26 changed files with 352 additions and 127 deletions

View File

@ -18,6 +18,7 @@ import type {
ApiRestrictionReason,
ApiSendAsPeerId,
ApiSponsoredMessageReportResult,
ApiSponsoredPeer,
ApiStarsSubscriptionPricing,
ApiTopic,
} from '../../types';
@ -711,3 +712,16 @@ export function buildApiStarsSubscriptionPricing(
amount: pricing.amount.toJSNumber(),
};
}
export function buildApiSponsoredPeer(sponsoredPeer: GramJs.SponsoredPeer): ApiSponsoredPeer {
const {
peer, randomId, additionalInfo, sponsorInfo,
} = sponsoredPeer;
return {
peerId: getApiChatIdFromMtpPeer(peer),
randomId: serializeBytes(randomId),
additionalInfo,
sponsorInfo,
};
}

View File

@ -45,6 +45,7 @@ import {
buildApiChatlistInvite,
buildApiChatReactions,
buildApiMissingInvitedUser,
buildApiSponsoredPeer,
buildApiTopic,
buildChatMember,
buildChatMembers,
@ -2020,3 +2021,9 @@ export async function fetchChannelRecommendations({ chat }: { chat?: ApiChat })
count: result instanceof GramJs.messages.ChatsSlice ? result.count : similarChannels.length,
};
}
export async function fetchSponsoredPeer({ query }: { query: string }) {
const result = await invokeRequest(new GramJs.contacts.GetSponsoredPeers({ q: query }));
if (!result || result instanceof GramJs.contacts.SponsoredPeersEmpty) return undefined;
return buildApiSponsoredPeer(result.peers[0]);
}

View File

@ -313,3 +313,10 @@ export type ApiDraft = {
effectId?: string;
isLocal?: boolean;
};
export type ApiSponsoredPeer = {
randomId: string;
peerId: string;
sponsorInfo?: string;
additionalInfo?: string;
};

View File

@ -782,6 +782,7 @@
"MessageRecommendedLabel" = "recommended";
"SponsoredMessageAd" = "Ad";
"SponsoredMessageAdWhatIsThis" = "what's this?";
"SponsoredPeerBadge" = "Ad";
"PremiumStickerTooltip" = "This set contains premium stickers like this one.";
"ViewAction" = "View";
"Loading" = "Loading...";
@ -1923,4 +1924,4 @@
"ApiMessageActionPaidMessagesRefundedOutgoing" = "You refunded **{stars}** to {user}";
"ApiMessageActionPaidMessagesRefundedIncoming" = "{user} refunded **{stars}** to you";
"NotificationTitleNotSupportedInFrozenAccount" = "Your account is frozen";
"NotificationMessageNotSupportedInFrozenAccount" = "This action is not available";
"NotificationMessageNotSupportedInFrozenAccount" = "This action is not available";

View File

@ -58,8 +58,7 @@ export { default as ChatFolderModal } from '../components/left/ChatFolderModal';
export { default as MuteChatModal } from '../components/left/MuteChatModal';
export { default as ContextMenuContainer } from '../components/middle/message/ContextMenuContainer';
export { default as SponsoredMessageContextMenuContainer }
from '../components/middle/message/SponsoredMessageContextMenuContainer';
export { default as SponsoredContextMenuContainer } from '../components/middle/message/SponsoredContextMenuContainer';
export { default as StickerSetModal } from '../components/common/StickerSetModal';
export { default as CustomEmojiSetsModal } from '../components/common/CustomEmojiSetsModal';
export { default as HeaderMenuContainer } from '../components/middle/HeaderMenuContainer';

View File

@ -8,15 +8,21 @@ type OwnProps = {
children: React.ReactNode;
className?: string;
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
onMouseDown?: (e: React.MouseEvent<HTMLDivElement>) => void;
};
const BadgeButton = ({
children,
className,
onClick,
onMouseDown,
}: OwnProps) => {
return (
<div className={buildClassName(styles.root, onClick && styles.clickable, className)} onClick={onClick}>
<div
className={buildClassName(styles.root, onClick && styles.clickable, className)}
onClick={onClick}
onMouseDown={onMouseDown}
>
{children}
</div>
);

View File

@ -5,7 +5,7 @@ import React, {
} from '../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../global';
import type { ApiMessage, ApiMessageSearchContext } from '../../../api/types';
import type { ApiMessage, ApiMessageSearchContext, ApiSponsoredPeer } from '../../../api/types';
import { LoadMoreDirection } from '../../../types';
import { ALL_FOLDER_ID, GLOBAL_SUGGESTED_CHANNELS_ID } from '../../../config';
@ -27,6 +27,7 @@ import useAppLayout from '../../../hooks/useAppLayout';
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
import useEffectOnce from '../../../hooks/useEffectOnce';
import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
@ -43,6 +44,7 @@ import Transition from '../../ui/Transition';
import ChatMessage from './ChatMessage';
import DateSuggest from './DateSuggest';
import LeftSearchResultChat from './LeftSearchResultChat';
import LeftSearchResultSponsored from './LeftSearchResultSponsored';
import RecentContacts from './RecentContacts';
import './ChatResults.scss';
@ -62,6 +64,7 @@ type StateProps = {
accountPeerIds?: string[];
globalPeerIds?: string[];
foundIds?: SearchResultKey[];
sponsoredPeer?: ApiSponsoredPeer;
globalMessagesByChatId?: Record<string, { byId: Record<number, ApiMessage> }>;
fetchingStatus?: { chats?: boolean; messages?: boolean };
suggestedChannelIds?: string[];
@ -69,6 +72,7 @@ type StateProps = {
const MIN_QUERY_LENGTH_FOR_GLOBAL_SEARCH = 4;
const LESS_LIST_ITEMS_AMOUNT = 5;
const INTERSECTION_THROTTLE = 200;
const runThrottled = throttle((cb) => cb(), 500, false);
@ -85,6 +89,7 @@ const ChatResults: FC<OwnProps & StateProps> = ({
globalMessagesByChatId,
fetchingStatus,
suggestedChannelIds,
sponsoredPeer,
onReset,
onSearchDateSelect,
}) => {
@ -93,6 +98,8 @@ const ChatResults: FC<OwnProps & StateProps> = ({
setGlobalSearchChatId, loadChannelRecommendations,
} = getActions();
// eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(null);
// eslint-disable-next-line no-null/no-null
const chatSelectionRef = useRef<HTMLDivElement>(null);
@ -335,7 +342,15 @@ const ChatResults: FC<OwnProps & StateProps> = ({
&& !localResults.length && !globalResults.length && !actualFoundIds.length;
const isMessagesFetching = fetchingStatus?.messages;
if (!searchQuery && !searchDate && !isChannelList) {
const shouldRenderTopPeers = !searchQuery && !searchDate && !isChannelList;
const { observe } = useIntersectionObserver({
rootRef: containerRef,
throttleMs: INTERSECTION_THROTTLE,
isDisabled: !shouldRenderTopPeers,
});
if (shouldRenderTopPeers) {
return <RecentContacts onReset={onReset} />;
}
@ -343,6 +358,7 @@ const ChatResults: FC<OwnProps & StateProps> = ({
return (
<InfiniteScroll
ref={containerRef}
className="LeftSearch--content custom-scroll"
items={actualFoundIds}
onLoadMore={handleLoadMore}
@ -415,6 +431,9 @@ const ChatResults: FC<OwnProps & StateProps> = ({
)}
{oldLang('DialogList.SearchSectionGlobal')}
</h3>
{sponsoredPeer && (
<LeftSearchResultSponsored sponsoredPeer={sponsoredPeer} observeIntersection={observe} />
)}
{globalResults.map((id, index) => {
if (!shouldShowMoreGlobal && index >= LESS_LIST_ITEMS_AMOUNT) {
return undefined;
@ -493,7 +512,7 @@ export default memo(withGlobal<OwnProps>(
}
const {
fetchingStatus, globalResults, localResults, resultsByType,
fetchingStatus, globalResults, localResults, resultsByType, sponsoredPeer,
} = selectTabState(global).globalSearch;
const { peerIds: globalPeerIds } = globalResults || {};
const { peerIds: accountPeerIds } = localResults || {};
@ -509,6 +528,7 @@ export default memo(withGlobal<OwnProps>(
foundIds,
globalMessagesByChatId,
fetchingStatus,
sponsoredPeer,
suggestedChannelIds: similarChannelIds,
};
},

View File

@ -183,6 +183,11 @@
text-overflow: ellipsis;
}
}
.search-sponsored-badge {
display: flex;
align-self: flex-start;
}
}
.search-section {

View File

@ -0,0 +1,116 @@
import type { FC } from '../../../lib/teact/teact';
import React, { memo, useRef } from '../../../lib/teact/teact';
import { getActions } from '../../../global';
import type { ApiSponsoredPeer } from '../../../api/types';
import { StoryViewerOrigin } from '../../../types';
import { isUserId } from '../../../global/helpers';
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
import { useFastClick } from '../../../hooks/useFastClick';
import { type ObserveFn, useOnIntersect } from '../../../hooks/useIntersectionObserver';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useSelectWithEnter from '../../../hooks/useSelectWithEnter';
import BadgeButton from '../../common/BadgeButton';
import GroupChatInfo from '../../common/GroupChatInfo';
import Icon from '../../common/icons/Icon';
import PrivateChatInfo from '../../common/PrivateChatInfo';
import SponsoredMessageContextMenuContainer from '../../middle/message/SponsoredContextMenuContainer';
import ListItem from '../../ui/ListItem';
type OwnProps = {
sponsoredPeer: ApiSponsoredPeer;
observeIntersection?: ObserveFn;
};
const LeftSearchResultSponsored: FC<OwnProps> = ({
sponsoredPeer,
observeIntersection,
}) => {
// eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement>(null);
const { clickSponsored, viewSponsored, openChat } = getActions();
const lang = useLang();
const {
peerId, randomId, additionalInfo, sponsorInfo,
} = sponsoredPeer;
useOnIntersect(ref, observeIntersection, (entry) => {
if (entry.intersectionRatio === 1) {
viewSponsored({ randomId });
}
});
const handleClick = useLastCallback(() => {
clickSponsored({ randomId });
openChat({ id: peerId });
});
const {
isContextMenuOpen, contextMenuAnchor,
handleBeforeContextMenu, handleContextMenu,
handleContextMenuClose, handleContextMenuHide,
} = useContextMenuHandlers(ref);
const {
handleClick: handleBadgeClick,
handleMouseDown: handleBadgeMouseDown,
} = useFastClick((e: React.MouseEvent) => {
e.stopPropagation();
handleContextMenu(e);
});
const buttonRef = useSelectWithEnter(handleClick);
return (
<ListItem
ref={ref}
className="chat-item-clickable search-result"
onClick={handleClick}
onMouseDown={handleBeforeContextMenu}
onContextMenu={handleContextMenu}
buttonRef={buttonRef}
>
{isUserId(peerId) ? (
<PrivateChatInfo
userId={peerId}
withUsername
withStory
avatarSize="medium"
storyViewerOrigin={StoryViewerOrigin.SearchResult}
/>
) : (
<GroupChatInfo
chatId={peerId}
withUsername
avatarSize="medium"
withStory
storyViewerOrigin={StoryViewerOrigin.SearchResult}
/>
)}
<BadgeButton className="search-sponsored-badge" onMouseDown={handleBadgeMouseDown} onClick={handleBadgeClick}>
{lang('SponsoredPeerBadge')}
<Icon name="more" />
</BadgeButton>
{contextMenuAnchor && (
<SponsoredMessageContextMenuContainer
isOpen={isContextMenuOpen}
anchor={contextMenuAnchor}
triggerRef={ref}
randomId={randomId}
additionalInfo={additionalInfo}
canReport
sponsorInfo={sponsorInfo}
onClose={handleContextMenuClose}
onCloseAnimationEnd={handleContextMenuHide}
/>
)}
</ListItem>
);
};
export default memo(LeftSearchResultSponsored);

View File

@ -122,7 +122,7 @@ const MediaViewer = ({
toggleChatInfo,
searchChatMediaMessages,
loadMoreProfilePhotos,
clickSponsoredMessage,
clickSponsored,
openUrl,
} = getActions();
@ -265,7 +265,7 @@ const MediaViewer = ({
const handleSponsoredClick = useLastCallback((isFromMedia?: boolean) => {
if (!sponsoredMessage || !chatId) return;
clickSponsoredMessage({ isMedia: isFromMedia, isFullscreen: true, peerId: chatId });
clickSponsored({ isMedia: isFromMedia, isFullscreen: true, randomId: sponsoredMessage.randomId });
openUrl({ url: sponsoredMessage!.url });
closeMediaViewer();
});

View File

@ -3,9 +3,6 @@ import React, {
memo, useRef,
} from '../../../lib/teact/teact';
import type {
ApiSponsoredMessage,
} from '../../../api/types';
import type { IAnchorPosition } from '../../../types';
import useLastCallback from '../../../hooks/useLastCallback';
@ -20,7 +17,8 @@ import './MessageContextMenu.scss';
type OwnProps = {
isOpen: boolean;
anchor: IAnchorPosition;
message: ApiSponsoredMessage;
sponsorInfo?: string;
canReport?: boolean;
triggerRef: React.RefObject<HTMLElement>;
shouldSkipAbout?: boolean;
onClose: NoneToVoidFunction;
@ -31,9 +29,10 @@ type OwnProps = {
onSponsoredReport?: NoneToVoidFunction;
};
const SponsoredMessageContextMenu: FC<OwnProps> = ({
const SponsoredContextMenu: FC<OwnProps> = ({
isOpen,
message,
sponsorInfo,
canReport,
anchor,
triggerRef,
shouldSkipAbout,
@ -53,7 +52,7 @@ const SponsoredMessageContextMenu: FC<OwnProps> = ({
const getMenuElement = useLastCallback(() => menuRef.current);
const getRootElement = useLastCallback(() => document.body);
const isSeparatorNeeded = message.sponsorInfo || !shouldSkipAbout || message.canReport;
const isSeparatorNeeded = sponsorInfo || !shouldSkipAbout || canReport;
return (
<Menu
@ -69,15 +68,15 @@ const SponsoredMessageContextMenu: FC<OwnProps> = ({
onClose={onClose}
onCloseAnimationEnd={onCloseAnimationEnd}
>
{message.sponsorInfo && onSponsorInfo && (
{sponsorInfo && onSponsorInfo && (
<MenuItem icon="channel" onClick={onSponsorInfo}>{lang('SponsoredMessageSponsor')}</MenuItem>
)}
{!shouldSkipAbout && (
<MenuItem icon="info" onClick={onAboutAdsClick}>
{lang(message.canReport ? 'AboutRevenueSharingAds' : 'SponsoredMessageInfo')}
{lang(canReport ? 'AboutRevenueSharingAds' : 'SponsoredMessageInfo')}
</MenuItem>
)}
{message.canReport && onSponsoredReport && (
{canReport && onSponsoredReport && (
<MenuItem icon="hand-stop" onClick={onSponsoredReport}>
{lang('ReportAd')}
</MenuItem>
@ -90,4 +89,4 @@ const SponsoredMessageContextMenu: FC<OwnProps> = ({
);
};
export default memo(SponsoredMessageContextMenu);
export default memo(SponsoredContextMenu);

View File

@ -0,0 +1,20 @@
import type { FC } from '../../../lib/teact/teact';
import React from '../../../lib/teact/teact';
import type { OwnProps } from './SponsoredContextMenuContainer';
import { Bundles } from '../../../util/moduleLoader';
import useModuleLoader from '../../../hooks/useModuleLoader';
const SponsoredContextMenuContainerAsync: FC<OwnProps> = (props) => {
const { isOpen } = props;
const SponsoredContextMenuContainer = useModuleLoader(
Bundles.Extra, 'SponsoredContextMenuContainer', !isOpen,
);
// eslint-disable-next-line react/jsx-props-no-spreading
return SponsoredContextMenuContainer ? <SponsoredContextMenuContainer {...props} /> : undefined;
};
export default SponsoredContextMenuContainerAsync;

View File

@ -2,17 +2,19 @@ import type { FC } from '../../../lib/teact/teact';
import React, { memo } from '../../../lib/teact/teact';
import { getActions } from '../../../global';
import type { ApiSponsoredMessage } from '../../../api/types';
import type { IAnchorPosition } from '../../../types';
import useLastCallback from '../../../hooks/useLastCallback';
import useShowTransition from '../../../hooks/useShowTransition';
import SponsoredMessageContextMenu from './SponsoredMessageContextMenu';
import SponsoredContextMenu from './SponsoredContextMenu';
export type OwnProps = {
isOpen: boolean;
message: ApiSponsoredMessage;
randomId: string;
sponsorInfo?: string;
additionalInfo?: string;
canReport?: boolean;
anchor: IAnchorPosition;
triggerRef: React.RefObject<HTMLElement>;
shouldSkipAbout?: boolean;
@ -23,7 +25,10 @@ export type OwnProps = {
const SponsoredMessageContextMenuContainer: FC<OwnProps> = ({
isOpen,
message,
randomId,
sponsorInfo,
additionalInfo,
canReport,
anchor,
triggerRef,
shouldSkipAbout,
@ -34,8 +39,8 @@ const SponsoredMessageContextMenuContainer: FC<OwnProps> = ({
const {
openAboutAdsModal,
showDialog,
reportSponsoredMessage,
hideSponsoredMessages,
reportSponsored,
hideSponsored,
} = getActions();
const { ref } = useShowTransition({
@ -49,26 +54,31 @@ const SponsoredMessageContextMenuContainer: FC<OwnProps> = ({
});
const handleAboutAdsOpen = useLastCallback(() => {
openAboutAdsModal({ chatId: message.chatId });
openAboutAdsModal({
randomId,
additionalInfo,
canReport,
sponsorInfo,
});
handleItemClick();
});
const handleSponsoredHide = useLastCallback(() => {
hideSponsoredMessages();
hideSponsored();
handleItemClick();
});
const handleSponsorInfo = useLastCallback(() => {
showDialog({
data: {
message: [message.sponsorInfo, message.additionalInfo].join('\n'),
message: [sponsorInfo, additionalInfo].filter(Boolean).join('\n'),
},
});
handleItemClick();
});
const handleReportSponsoredMessage = useLastCallback(() => {
reportSponsoredMessage({ peerId: message.chatId, randomId: message.randomId });
reportSponsored({ randomId });
handleItemClick();
});
@ -78,11 +88,12 @@ const SponsoredMessageContextMenuContainer: FC<OwnProps> = ({
return (
<div ref={ref} className="ContextMenuContainer">
<SponsoredMessageContextMenu
<SponsoredContextMenu
isOpen={isOpen}
anchor={anchor}
triggerRef={triggerRef}
message={message}
canReport={canReport}
sponsorInfo={sponsorInfo}
shouldSkipAbout={shouldSkipAbout}
onClose={onClose}
onCloseAnimationEnd={onClose}

View File

@ -38,7 +38,7 @@ import PeerColorWrapper from '../../common/PeerColorWrapper';
import Button from '../../ui/Button';
import MessageAppendix from './MessageAppendix';
import Photo from './Photo';
import SponsoredMessageContextMenuContainer from './SponsoredMessageContextMenuContainer.async';
import SponsoredContextMenuContainer from './SponsoredContextMenuContainer.async';
import Video from './Video';
import './SponsoredMessage.scss';
@ -72,10 +72,10 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
canAutoPlayMedia,
}) => {
const {
viewSponsoredMessage,
viewSponsored,
openUrl,
hideSponsoredMessages,
clickSponsoredMessage,
hideSponsored,
clickSponsored,
openMediaViewer,
openAboutAdsModal,
} = getActions();
@ -103,11 +103,11 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
useEffect(() => {
return shouldObserve ? observeIntersection(contentRef.current!, (target) => {
if (target.isIntersecting) {
viewSponsoredMessage({ peerId: chatId });
if (target.isIntersecting && message?.randomId) {
viewSponsored({ randomId: message.randomId });
}
}) : undefined;
}, [chatId, shouldObserve, observeIntersection, viewSponsoredMessage]);
}, [message?.randomId, shouldObserve, observeIntersection, viewSponsored]);
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
preventMessageInputBlur(e);
@ -115,7 +115,7 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
};
const handleHideSponsoredMessage = useLastCallback(() => {
hideSponsoredMessages();
hideSponsored();
});
const {
@ -128,12 +128,13 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
const handleClick = useLastCallback(() => {
if (!message) return;
clickSponsoredMessage({ isMedia: photo || isGif ? true : undefined, peerId: chatId });
clickSponsored({ randomId: message.randomId, isMedia: photo || isGif ? true : undefined });
openUrl({ url: message.url, shouldSkipModal: true });
});
const handleOpenMedia = useLastCallback(() => {
clickSponsoredMessage({ isMedia: true, peerId: chatId });
if (!message) return;
clickSponsored({ randomId: message.randomId, isMedia: true });
openMediaViewer({
origin: MediaViewerOrigin.SponsoredMessage,
chatId,
@ -142,7 +143,13 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
});
const handleOpenAboutAdsModal = useLastCallback(() => {
openAboutAdsModal({ chatId });
if (!message) return;
openAboutAdsModal({
randomId: message.randomId,
canReport: message.canReport,
additionalInfo: message.additionalInfo,
sponsorInfo: message.sponsorInfo,
});
});
const extraPadding = 0;
@ -315,11 +322,14 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
</div>
</div>
{contextMenuAnchor && (
<SponsoredMessageContextMenuContainer
<SponsoredContextMenuContainer
isOpen={isContextMenuOpen}
anchor={contextMenuAnchor}
triggerRef={ref}
message={message!}
randomId={message.randomId}
canReport={message.canReport}
sponsorInfo={message.sponsorInfo}
additionalInfo={message.additionalInfo}
onClose={handleContextMenuClose}
onCloseAnimationEnd={handleContextMenuHide}
/>

View File

@ -1,20 +0,0 @@
import type { FC } from '../../../lib/teact/teact';
import React from '../../../lib/teact/teact';
import type { OwnProps } from './SponsoredMessageContextMenuContainer';
import { Bundles } from '../../../util/moduleLoader';
import useModuleLoader from '../../../hooks/useModuleLoader';
const SponsoredMessageContextMenuContainerAsync: FC<OwnProps> = (props) => {
const { isOpen } = props;
const SponsoredMessageContextMenuContainer = useModuleLoader(
Bundles.Extra, 'SponsoredMessageContextMenuContainer', !isOpen,
);
// eslint-disable-next-line react/jsx-props-no-spreading
return SponsoredMessageContextMenuContainer ? <SponsoredMessageContextMenuContainer {...props} /> : undefined;
};
export default SponsoredMessageContextMenuContainerAsync;

View File

@ -17,7 +17,7 @@ import useHeaderPane, { type PaneState } from '../hooks/useHeaderPane';
import Avatar from '../../common/Avatar';
import BadgeButton from '../../common/BadgeButton';
import SponsoredMessageContextMenuContainer from '../message/SponsoredMessageContextMenuContainer';
import SponsoredMessageContextMenuContainer from '../message/SponsoredContextMenuContainer';
import styles from './BotAdPane.module.scss';
@ -40,9 +40,9 @@ const BotAdPane = ({
onPaneStateChange,
}: OwnProps & StateProps) => {
const {
viewSponsoredMessage,
viewSponsored,
openUrl,
clickSponsoredMessage,
clickSponsored,
openAboutAdsModal,
} = getActions();
@ -67,25 +67,38 @@ const BotAdPane = ({
const handleClick = useLastCallback(() => {
if (!renderingSponsoredMessage) return;
clickSponsoredMessage({ peerId: chatId });
clickSponsored({ randomId: renderingSponsoredMessage.randomId });
openUrl({ url: renderingSponsoredMessage.url, shouldSkipModal: true });
});
const handleAboutClick = useLastCallback((e: React.MouseEvent<HTMLDivElement>) => {
if (!renderingSponsoredMessage) return;
const {
randomId, additionalInfo, canReport, sponsorInfo,
} = renderingSponsoredMessage;
e.stopPropagation();
openAboutAdsModal({ chatId });
openAboutAdsModal({
randomId,
additionalInfo,
canReport,
sponsorInfo,
});
});
useEffect(() => {
if (shouldRender && sponsoredMessage) {
viewSponsoredMessage({ peerId: chatId });
if (shouldRender && renderingSponsoredMessage) {
viewSponsored({ randomId: renderingSponsoredMessage.randomId });
}
}, [shouldRender, sponsoredMessage, chatId]);
}, [shouldRender, renderingSponsoredMessage, chatId]);
if (!shouldRender || !renderingSponsoredMessage) {
return undefined;
}
const {
randomId, canReport, additionalInfo, sponsorInfo,
} = renderingSponsoredMessage;
const {
peerColor,
content,
@ -132,7 +145,10 @@ const BotAdPane = ({
isOpen={isContextMenuOpen}
anchor={contextMenuAnchor}
triggerRef={ref}
message={renderingSponsoredMessage}
randomId={randomId}
additionalInfo={additionalInfo}
canReport={canReport}
sponsorInfo={sponsorInfo}
onClose={handleContextMenuClose}
onCloseAnimationEnd={handleContextMenuHide}
/>

View File

@ -1,11 +1,9 @@
import React, { memo, useMemo, useRef } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type { ApiSponsoredMessage } from '../../../api/types';
import type { TabState } from '../../../global/types';
import type { TableAboutData } from '../common/TableAboutModal';
import { selectSponsoredMessage } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import renderText from '../../common/helpers/renderText';
@ -16,7 +14,7 @@ import useOldLang from '../../../hooks/useOldLang';
import Icon from '../../common/icons/Icon';
import SafeLink from '../../common/SafeLink';
import SponsoredMessageContextMenuContainer from '../../middle/message/SponsoredMessageContextMenuContainer';
import SponsoredMessageContextMenuContainer from '../../middle/message/SponsoredContextMenuContainer';
import Button from '../../ui/Button';
import Modal from '../../ui/Modal';
import TableAboutModal from '../common/TableAboutModal';
@ -29,18 +27,21 @@ export type OwnProps = {
};
type StateProps = {
message?: ApiSponsoredMessage;
minLevelToRestrictAds?: number;
};
const AboutAdsModal = ({ message, minLevelToRestrictAds }: OwnProps & StateProps) => {
const AboutAdsModal = ({ modal, minLevelToRestrictAds }: OwnProps & StateProps) => {
const { closeAboutAdsModal } = getActions();
// eslint-disable-next-line no-null/no-null
const moreMenuRef = useRef<HTMLButtonElement>(null);
const isOpen = Boolean(message);
const isMonetizationSharing = message?.canReport;
const isOpen = Boolean(modal);
const renderingModal = useCurrentOrPrev(modal);
const {
canReport, randomId, additionalInfo, sponsorInfo,
} = renderingModal || {};
const isMonetizationSharing = canReport;
const renderingIsNewDesign = useCurrentOrPrev(isMonetizationSharing);
@ -139,12 +140,15 @@ const AboutAdsModal = ({ message, minLevelToRestrictAds }: OwnProps & StateProps
buttonText={oldLang('RevenueSharingAdsUnderstood')}
onClose={handleClose}
/>
{contextMenuAnchor && message && (
{contextMenuAnchor && randomId && (
<SponsoredMessageContextMenuContainer
isOpen={isContextMenuOpen}
anchor={contextMenuAnchor}
triggerRef={moreMenuRef}
message={message}
randomId={randomId}
additionalInfo={additionalInfo}
canReport={canReport}
sponsorInfo={sponsorInfo}
shouldSkipAbout
onItemClick={handleClose}
onClose={handleContextMenuClose}
@ -174,12 +178,10 @@ const AboutAdsModal = ({ message, minLevelToRestrictAds }: OwnProps & StateProps
};
export default memo(withGlobal<OwnProps>(
(global, { modal }): StateProps => {
const message = modal?.chatId ? selectSponsoredMessage(global, modal.chatId) : undefined;
(global): StateProps => {
const minLevelToRestrictAds = global.appConfig?.channelRestrictAdsLevelMin;
return {
message,
minLevelToRestrictAds,
};
},

View File

@ -30,7 +30,7 @@ const ReportAdModal = ({
modal,
}: OwnProps) => {
const {
reportSponsoredMessage, closeReportAdModal, openPreviousReportAdModal,
reportSponsored, closeReportAdModal, openPreviousReportAdModal,
} = getActions();
const lang = useOldLang();
const isOpen = Boolean(modal);
@ -40,7 +40,7 @@ const ReportAdModal = ({
const handleOptionClick = useLastCallback((e, option: string) => {
const { chatId, randomId } = modal!;
reportSponsoredMessage({ peerId: chatId, randomId, option });
reportSponsored({ peerId: chatId, randomId, option });
});
const [renderingSection, renderingDepth] = useMemo(() => {

View File

@ -33,11 +33,14 @@ addActionHandler('setGlobalSearchQuery', (global, actions, payload): ActionRetur
if (query && !chatId) {
void searchThrottled(async () => {
const result = await callApi('searchChats', { query });
const [searchResult, sponsoredResult] = await Promise.all([
callApi('searchChats', { query }),
callApi('fetchSponsoredPeer', { query }),
]);
global = getGlobal();
const currentSearchQuery = selectCurrentGlobalSearchQuery(global, tabId);
if (!result || !currentSearchQuery || (query !== currentSearchQuery)) {
if (!searchResult || !currentSearchQuery || (query !== currentSearchQuery)) {
global = updateGlobalSearchFetchingStatus(global, { chats: false }, tabId);
setGlobal(global);
return;
@ -45,7 +48,7 @@ addActionHandler('setGlobalSearchQuery', (global, actions, payload): ActionRetur
const {
accountResultIds, globalResultIds,
} = result;
} = searchResult;
global = updateGlobalSearchFetchingStatus(global, { chats: false }, tabId);
global = updateGlobalSearch(global, {
@ -56,6 +59,7 @@ addActionHandler('setGlobalSearchQuery', (global, actions, payload): ActionRetur
...selectTabState(global, tabId).globalSearch.globalResults,
peerIds: globalResultIds,
},
sponsoredPeer: sponsoredResult,
}, tabId);
setGlobal(global);

View File

@ -84,6 +84,7 @@ import {
updateChat,
updateChatFullInfo,
updateChatMessage,
updateGlobalSearch,
updateListedIds,
updateMessageTranslation,
updateOutlyingLists,
@ -135,7 +136,6 @@ import {
selectReplyCanBeSentToChat,
selectScheduledMessage,
selectSendAs,
selectSponsoredMessage,
selectTabState,
selectThreadIdFromMessage,
selectTopic,
@ -1867,38 +1867,24 @@ addActionHandler('loadSponsoredMessages', async (global, actions, payload): Prom
setGlobal(global);
});
addActionHandler('viewSponsoredMessage', (global, actions, payload): ActionReturnType => {
const { peerId } = payload;
const peer = selectPeer(global, peerId);
const message = selectSponsoredMessage(global, peerId);
if (!peer || !message) {
return;
}
addActionHandler('viewSponsored', (global, actions, payload): ActionReturnType => {
const { randomId } = payload;
void callApi('viewSponsoredMessage', { random: message.randomId });
void callApi('viewSponsoredMessage', { random: randomId });
});
addActionHandler('clickSponsoredMessage', (global, actions, payload): ActionReturnType => {
const { peerId, isMedia, isFullscreen } = payload;
const peer = selectPeer(global, peerId);
const message = selectSponsoredMessage(global, peerId);
if (!peer || !message) {
return;
}
addActionHandler('clickSponsored', (global, actions, payload): ActionReturnType => {
const { randomId, isMedia, isFullscreen } = payload;
void callApi('clickSponsoredMessage', {
random: message.randomId, isMedia, isFullscreen,
random: randomId, isMedia, isFullscreen,
});
});
addActionHandler('reportSponsoredMessage', async (global, actions, payload): Promise<void> => {
addActionHandler('reportSponsored', async (global, actions, payload): Promise<void> => {
const {
peerId, randomId, option = '', tabId = getCurrentTabId(),
} = payload;
const peer = selectPeer(global, peerId);
if (!peer) {
return;
}
const result = await callApi('reportSponsoredMessage', { randomId, option });
@ -1918,7 +1904,13 @@ addActionHandler('reportSponsoredMessage', async (global, actions, payload): Pro
actions.closeReportAdModal({ tabId });
global = getGlobal();
global = deleteSponsoredMessage(global, peerId);
if (peerId) {
global = deleteSponsoredMessage(global, peerId);
} else {
global = updateGlobalSearch(global, {
sponsoredPeer: undefined,
}, tabId);
}
setGlobal(global);
return;
}
@ -1943,7 +1935,7 @@ addActionHandler('reportSponsoredMessage', async (global, actions, payload): Pro
}
});
addActionHandler('hideSponsoredMessages', async (global, actions, payload): Promise<void> => {
addActionHandler('hideSponsored', async (global, actions, payload): Promise<void> => {
const { tabId = getCurrentTabId() } = payload || {};
const isCurrentUserPremium = selectIsCurrentUserPremium(global);
if (!isCurrentUserPremium) {

View File

@ -1064,11 +1064,16 @@ addActionHandler('closeDeleteMessageModal', (global, actions, payload): ActionRe
});
addActionHandler('openAboutAdsModal', (global, actions, payload): ActionReturnType => {
const { chatId, tabId = getCurrentTabId() } = payload || {};
const {
randomId, additionalInfo, canReport, sponsorInfo, tabId = getCurrentTabId(),
} = payload || {};
return updateTabState(global, {
aboutAdsModal: {
chatId,
randomId,
canReport,
additionalInfo,
sponsorInfo,
},
}, tabId);
});

View File

@ -504,21 +504,24 @@ export interface ActionPayloads {
loadSponsoredMessages: {
peerId: string;
};
viewSponsoredMessage: {
peerId: string;
viewSponsored: {
randomId: string;
};
clickSponsoredMessage: {
peerId: string;
clickSponsored: {
randomId: string;
isMedia?: boolean;
isFullscreen?: boolean;
};
reportSponsoredMessage: {
peerId: string;
reportSponsored: {
peerId?: string;
randomId: string;
option?: string;
} & WithTabId;
openAboutAdsModal: {
chatId: string;
randomId: string;
canReport?: boolean;
sponsorInfo?: string;
additionalInfo?: string;
} & WithTabId;
closeAboutAdsModal: WithTabId | undefined;
openPreparedInlineMessageModal: {
@ -542,7 +545,7 @@ export interface ActionPayloads {
openPreviousReportModal: WithTabId | undefined;
closeReportAdModal: WithTabId | undefined;
closeReportModal: WithTabId | undefined;
hideSponsoredMessages: WithTabId | undefined;
hideSponsored: WithTabId | undefined;
loadSendAs: {
chatId: string;
};

View File

@ -35,6 +35,7 @@ import type {
ApiReceiptRegular,
ApiSavedGifts,
ApiSavedStarGift,
ApiSponsoredPeer,
ApiStarGift,
ApiStarGiftAttribute,
ApiStarGiveawayOption,
@ -186,7 +187,10 @@ export type TabState = {
};
aboutAdsModal?: {
chatId: string;
randomId: string;
canReport?: boolean;
sponsorInfo?: string;
additionalInfo?: string;
};
reactionPicker?: {
@ -220,6 +224,7 @@ export type TabState = {
currentContent?: GlobalSearchContent;
chatId?: string;
foundTopicIds?: number[];
sponsoredPeer?: ApiSponsoredPeer;
fetchingStatus?: {
chats?: boolean;
messages?: boolean;
@ -440,7 +445,7 @@ export type TabState = {
openedCustomEmojiSetIds?: string[];
reportAdModal?: {
chatId: string;
chatId?: string;
randomId: string;
sections: {
title: string;

View File

@ -1498,6 +1498,7 @@ contacts.getTopPeers#973478b6 flags:# correspondents:flags.0?true bots_pm:flags.
contacts.addContact#e8f463d0 flags:# add_phone_privacy_exception:flags.0?true id:InputUser first_name:string last_name:string phone:string = Updates;
contacts.resolvePhone#8af94344 phone:string = contacts.ResolvedPeer;
contacts.editCloseFriends#ba6705f0 id:Vector<long> = Bool;
contacts.getSponsoredPeers#b6c8c393 q:string = contacts.SponsoredPeers;
messages.getMessages#63c66506 id:Vector<InputMessage> = messages.Messages;
messages.getDialogs#a0f4cb4f flags:# exclude_pinned:flags.0?true folder_id:flags.1?int offset_date:int offset_id:int offset_peer:InputPeer limit:int hash:long = messages.Dialogs;
messages.getHistory#4423e6c5 peer:InputPeer offset_id:int offset_date:int add_offset:int limit:int max_id:int min_id:int hash:long = messages.Messages;

View File

@ -77,6 +77,7 @@
"contacts.addContact",
"contacts.resolvePhone",
"contacts.editCloseFriends",
"contacts.getSponsoredPeers",
"messages.getMessages",
"messages.getDialogs",
"messages.getHistory",

View File

@ -685,6 +685,7 @@ export interface LangPair {
'MessageRecommendedLabel': undefined;
'SponsoredMessageAd': undefined;
'SponsoredMessageAdWhatIsThis': undefined;
'SponsoredPeerBadge': undefined;
'PremiumStickerTooltip': undefined;
'ViewAction': undefined;
'Loading': undefined;