From 97bd26df004d5588060d27215b8f69481a069d4e Mon Sep 17 00:00:00 2001 From: zubiden <19638254+zubiden@users.noreply.github.com> Date: Wed, 23 Apr 2025 18:59:25 +0200 Subject: [PATCH] Global Search: Show sponsored results (#5844) Co-authored-by: Dmitry Kabanov Co-authored-by: Dmitry Kabanov <153344039+dmitrykabanovdev@users.noreply.github.com> --- src/api/gramjs/apiBuilders/chats.ts | 14 +++ src/api/gramjs/methods/chats.ts | 7 ++ src/api/types/chats.ts | 7 ++ src/assets/localization/fallback.strings | 3 +- src/bundles/extra.ts | 3 +- src/components/common/BadgeButton.tsx | 8 +- src/components/left/search/ChatResults.tsx | 26 +++- src/components/left/search/LeftSearch.scss | 5 + .../left/search/LeftSearchResultSponsored.tsx | 116 ++++++++++++++++++ src/components/mediaViewer/MediaViewer.tsx | 4 +- ...ntextMenu.tsx => SponsoredContextMenu.tsx} | 21 ++-- .../SponsoredContextMenuContainer.async.tsx | 20 +++ ....tsx => SponsoredContextMenuContainer.tsx} | 35 ++++-- .../middle/message/SponsoredMessage.tsx | 36 ++++-- ...soredMessageContextMenuContainer.async.tsx | 20 --- src/components/middle/panes/BotAdPane.tsx | 34 +++-- .../modals/aboutAds/AboutAdsModal.tsx | 26 ++-- .../modals/reportAd/ReportAdModal.tsx | 4 +- src/global/actions/api/globalSearch.ts | 10 +- src/global/actions/api/messages.ts | 40 +++--- src/global/actions/ui/messages.ts | 9 +- src/global/types/actions.ts | 19 +-- src/global/types/tabState.ts | 9 +- src/lib/gramjs/tl/apiTl.ts | 1 + src/lib/gramjs/tl/static/api.json | 1 + src/types/language.d.ts | 1 + 26 files changed, 352 insertions(+), 127 deletions(-) create mode 100644 src/components/left/search/LeftSearchResultSponsored.tsx rename src/components/middle/message/{SponsoredMessageContextMenu.tsx => SponsoredContextMenu.tsx} (82%) create mode 100644 src/components/middle/message/SponsoredContextMenuContainer.async.tsx rename src/components/middle/message/{SponsoredMessageContextMenuContainer.tsx => SponsoredContextMenuContainer.tsx} (77%) delete mode 100644 src/components/middle/message/SponsoredMessageContextMenuContainer.async.tsx diff --git a/src/api/gramjs/apiBuilders/chats.ts b/src/api/gramjs/apiBuilders/chats.ts index 53756ad92..59432881f 100644 --- a/src/api/gramjs/apiBuilders/chats.ts +++ b/src/api/gramjs/apiBuilders/chats.ts @@ -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, + }; +} diff --git a/src/api/gramjs/methods/chats.ts b/src/api/gramjs/methods/chats.ts index 6a185f16a..eb4151e45 100644 --- a/src/api/gramjs/methods/chats.ts +++ b/src/api/gramjs/methods/chats.ts @@ -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]); +} diff --git a/src/api/types/chats.ts b/src/api/types/chats.ts index e5d1fbe5e..37de636a3 100644 --- a/src/api/types/chats.ts +++ b/src/api/types/chats.ts @@ -313,3 +313,10 @@ export type ApiDraft = { effectId?: string; isLocal?: boolean; }; + +export type ApiSponsoredPeer = { + randomId: string; + peerId: string; + sponsorInfo?: string; + additionalInfo?: string; +}; diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index 4401d5a6f..23eccd90e 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -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"; \ No newline at end of file +"NotificationMessageNotSupportedInFrozenAccount" = "This action is not available"; diff --git a/src/bundles/extra.ts b/src/bundles/extra.ts index 93122c1b0..7abed7263 100644 --- a/src/bundles/extra.ts +++ b/src/bundles/extra.ts @@ -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'; diff --git a/src/components/common/BadgeButton.tsx b/src/components/common/BadgeButton.tsx index e2e179f9c..0478b8a11 100644 --- a/src/components/common/BadgeButton.tsx +++ b/src/components/common/BadgeButton.tsx @@ -8,15 +8,21 @@ type OwnProps = { children: React.ReactNode; className?: string; onClick?: (e: React.MouseEvent) => void; + onMouseDown?: (e: React.MouseEvent) => void; }; const BadgeButton = ({ children, className, onClick, + onMouseDown, }: OwnProps) => { return ( -
+
{children}
); diff --git a/src/components/left/search/ChatResults.tsx b/src/components/left/search/ChatResults.tsx index c625103df..dd16904b6 100644 --- a/src/components/left/search/ChatResults.tsx +++ b/src/components/left/search/ChatResults.tsx @@ -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 }>; 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 = ({ globalMessagesByChatId, fetchingStatus, suggestedChannelIds, + sponsoredPeer, onReset, onSearchDateSelect, }) => { @@ -93,6 +98,8 @@ const ChatResults: FC = ({ setGlobalSearchChatId, loadChannelRecommendations, } = getActions(); + // eslint-disable-next-line no-null/no-null + const containerRef = useRef(null); // eslint-disable-next-line no-null/no-null const chatSelectionRef = useRef(null); @@ -335,7 +342,15 @@ const ChatResults: FC = ({ && !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 ; } @@ -343,6 +358,7 @@ const ChatResults: FC = ({ return ( = ({ )} {oldLang('DialogList.SearchSectionGlobal')} + {sponsoredPeer && ( + + )} {globalResults.map((id, index) => { if (!shouldShowMoreGlobal && index >= LESS_LIST_ITEMS_AMOUNT) { return undefined; @@ -493,7 +512,7 @@ export default memo(withGlobal( } 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( foundIds, globalMessagesByChatId, fetchingStatus, + sponsoredPeer, suggestedChannelIds: similarChannelIds, }; }, diff --git a/src/components/left/search/LeftSearch.scss b/src/components/left/search/LeftSearch.scss index 2239eea32..fd9e6ad2f 100644 --- a/src/components/left/search/LeftSearch.scss +++ b/src/components/left/search/LeftSearch.scss @@ -183,6 +183,11 @@ text-overflow: ellipsis; } } + + .search-sponsored-badge { + display: flex; + align-self: flex-start; + } } .search-section { diff --git a/src/components/left/search/LeftSearchResultSponsored.tsx b/src/components/left/search/LeftSearchResultSponsored.tsx new file mode 100644 index 000000000..83cb16616 --- /dev/null +++ b/src/components/left/search/LeftSearchResultSponsored.tsx @@ -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 = ({ + sponsoredPeer, + observeIntersection, +}) => { + // eslint-disable-next-line no-null/no-null + const ref = useRef(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 ( + + {isUserId(peerId) ? ( + + ) : ( + + )} + + {lang('SponsoredPeerBadge')} + + + {contextMenuAnchor && ( + + )} + + ); +}; + +export default memo(LeftSearchResultSponsored); diff --git a/src/components/mediaViewer/MediaViewer.tsx b/src/components/mediaViewer/MediaViewer.tsx index bd6500df6..f39748f91 100644 --- a/src/components/mediaViewer/MediaViewer.tsx +++ b/src/components/mediaViewer/MediaViewer.tsx @@ -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(); }); diff --git a/src/components/middle/message/SponsoredMessageContextMenu.tsx b/src/components/middle/message/SponsoredContextMenu.tsx similarity index 82% rename from src/components/middle/message/SponsoredMessageContextMenu.tsx rename to src/components/middle/message/SponsoredContextMenu.tsx index bd036db19..f35328941 100644 --- a/src/components/middle/message/SponsoredMessageContextMenu.tsx +++ b/src/components/middle/message/SponsoredContextMenu.tsx @@ -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; shouldSkipAbout?: boolean; onClose: NoneToVoidFunction; @@ -31,9 +29,10 @@ type OwnProps = { onSponsoredReport?: NoneToVoidFunction; }; -const SponsoredMessageContextMenu: FC = ({ +const SponsoredContextMenu: FC = ({ isOpen, - message, + sponsorInfo, + canReport, anchor, triggerRef, shouldSkipAbout, @@ -53,7 +52,7 @@ const SponsoredMessageContextMenu: FC = ({ const getMenuElement = useLastCallback(() => menuRef.current); const getRootElement = useLastCallback(() => document.body); - const isSeparatorNeeded = message.sponsorInfo || !shouldSkipAbout || message.canReport; + const isSeparatorNeeded = sponsorInfo || !shouldSkipAbout || canReport; return ( = ({ onClose={onClose} onCloseAnimationEnd={onCloseAnimationEnd} > - {message.sponsorInfo && onSponsorInfo && ( + {sponsorInfo && onSponsorInfo && ( {lang('SponsoredMessageSponsor')} )} {!shouldSkipAbout && ( - {lang(message.canReport ? 'AboutRevenueSharingAds' : 'SponsoredMessageInfo')} + {lang(canReport ? 'AboutRevenueSharingAds' : 'SponsoredMessageInfo')} )} - {message.canReport && onSponsoredReport && ( + {canReport && onSponsoredReport && ( {lang('ReportAd')} @@ -90,4 +89,4 @@ const SponsoredMessageContextMenu: FC = ({ ); }; -export default memo(SponsoredMessageContextMenu); +export default memo(SponsoredContextMenu); diff --git a/src/components/middle/message/SponsoredContextMenuContainer.async.tsx b/src/components/middle/message/SponsoredContextMenuContainer.async.tsx new file mode 100644 index 000000000..af23ca122 --- /dev/null +++ b/src/components/middle/message/SponsoredContextMenuContainer.async.tsx @@ -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 = (props) => { + const { isOpen } = props; + const SponsoredContextMenuContainer = useModuleLoader( + Bundles.Extra, 'SponsoredContextMenuContainer', !isOpen, + ); + + // eslint-disable-next-line react/jsx-props-no-spreading + return SponsoredContextMenuContainer ? : undefined; +}; + +export default SponsoredContextMenuContainerAsync; diff --git a/src/components/middle/message/SponsoredMessageContextMenuContainer.tsx b/src/components/middle/message/SponsoredContextMenuContainer.tsx similarity index 77% rename from src/components/middle/message/SponsoredMessageContextMenuContainer.tsx rename to src/components/middle/message/SponsoredContextMenuContainer.tsx index 2d26613b6..0fbaa7ac2 100644 --- a/src/components/middle/message/SponsoredMessageContextMenuContainer.tsx +++ b/src/components/middle/message/SponsoredContextMenuContainer.tsx @@ -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; shouldSkipAbout?: boolean; @@ -23,7 +25,10 @@ export type OwnProps = { const SponsoredMessageContextMenuContainer: FC = ({ isOpen, - message, + randomId, + sponsorInfo, + additionalInfo, + canReport, anchor, triggerRef, shouldSkipAbout, @@ -34,8 +39,8 @@ const SponsoredMessageContextMenuContainer: FC = ({ const { openAboutAdsModal, showDialog, - reportSponsoredMessage, - hideSponsoredMessages, + reportSponsored, + hideSponsored, } = getActions(); const { ref } = useShowTransition({ @@ -49,26 +54,31 @@ const SponsoredMessageContextMenuContainer: FC = ({ }); 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 = ({ return (
- = ({ canAutoPlayMedia, }) => { const { - viewSponsoredMessage, + viewSponsored, openUrl, - hideSponsoredMessages, - clickSponsoredMessage, + hideSponsored, + clickSponsored, openMediaViewer, openAboutAdsModal, } = getActions(); @@ -103,11 +103,11 @@ const SponsoredMessage: FC = ({ 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) => { preventMessageInputBlur(e); @@ -115,7 +115,7 @@ const SponsoredMessage: FC = ({ }; const handleHideSponsoredMessage = useLastCallback(() => { - hideSponsoredMessages(); + hideSponsored(); }); const { @@ -128,12 +128,13 @@ const SponsoredMessage: FC = ({ 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 = ({ }); 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 = ({
{contextMenuAnchor && ( - diff --git a/src/components/middle/message/SponsoredMessageContextMenuContainer.async.tsx b/src/components/middle/message/SponsoredMessageContextMenuContainer.async.tsx deleted file mode 100644 index d4c9f7f84..000000000 --- a/src/components/middle/message/SponsoredMessageContextMenuContainer.async.tsx +++ /dev/null @@ -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 = (props) => { - const { isOpen } = props; - const SponsoredMessageContextMenuContainer = useModuleLoader( - Bundles.Extra, 'SponsoredMessageContextMenuContainer', !isOpen, - ); - - // eslint-disable-next-line react/jsx-props-no-spreading - return SponsoredMessageContextMenuContainer ? : undefined; -}; - -export default SponsoredMessageContextMenuContainerAsync; diff --git a/src/components/middle/panes/BotAdPane.tsx b/src/components/middle/panes/BotAdPane.tsx index 3cf88ea07..55c9cc5da 100644 --- a/src/components/middle/panes/BotAdPane.tsx +++ b/src/components/middle/panes/BotAdPane.tsx @@ -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) => { + 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} /> diff --git a/src/components/modals/aboutAds/AboutAdsModal.tsx b/src/components/modals/aboutAds/AboutAdsModal.tsx index 0360c308c..101e44287 100644 --- a/src/components/modals/aboutAds/AboutAdsModal.tsx +++ b/src/components/modals/aboutAds/AboutAdsModal.tsx @@ -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(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 && ( ( - (global, { modal }): StateProps => { - const message = modal?.chatId ? selectSponsoredMessage(global, modal.chatId) : undefined; + (global): StateProps => { const minLevelToRestrictAds = global.appConfig?.channelRestrictAdsLevelMin; return { - message, minLevelToRestrictAds, }; }, diff --git a/src/components/modals/reportAd/ReportAdModal.tsx b/src/components/modals/reportAd/ReportAdModal.tsx index a922111d9..1167ff93c 100644 --- a/src/components/modals/reportAd/ReportAdModal.tsx +++ b/src/components/modals/reportAd/ReportAdModal.tsx @@ -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(() => { diff --git a/src/global/actions/api/globalSearch.ts b/src/global/actions/api/globalSearch.ts index 4c4b0289a..5154eb521 100644 --- a/src/global/actions/api/globalSearch.ts +++ b/src/global/actions/api/globalSearch.ts @@ -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); diff --git a/src/global/actions/api/messages.ts b/src/global/actions/api/messages.ts index 8ab9a8cfd..994b367e9 100644 --- a/src/global/actions/api/messages.ts +++ b/src/global/actions/api/messages.ts @@ -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 => { +addActionHandler('reportSponsored', async (global, actions, payload): Promise => { 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 => { +addActionHandler('hideSponsored', async (global, actions, payload): Promise => { const { tabId = getCurrentTabId() } = payload || {}; const isCurrentUserPremium = selectIsCurrentUserPremium(global); if (!isCurrentUserPremium) { diff --git a/src/global/actions/ui/messages.ts b/src/global/actions/ui/messages.ts index df461cc0c..6476a39a2 100644 --- a/src/global/actions/ui/messages.ts +++ b/src/global/actions/ui/messages.ts @@ -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); }); diff --git a/src/global/types/actions.ts b/src/global/types/actions.ts index 605c1bfe5..23a479782 100644 --- a/src/global/types/actions.ts +++ b/src/global/types/actions.ts @@ -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; }; diff --git a/src/global/types/tabState.ts b/src/global/types/tabState.ts index 0e121a98c..5e9e3e554 100644 --- a/src/global/types/tabState.ts +++ b/src/global/types/tabState.ts @@ -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; diff --git a/src/lib/gramjs/tl/apiTl.ts b/src/lib/gramjs/tl/apiTl.ts index e8d4ee7df..052a8f1c0 100644 --- a/src/lib/gramjs/tl/apiTl.ts +++ b/src/lib/gramjs/tl/apiTl.ts @@ -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 = Bool; +contacts.getSponsoredPeers#b6c8c393 q:string = contacts.SponsoredPeers; messages.getMessages#63c66506 id:Vector = 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; diff --git a/src/lib/gramjs/tl/static/api.json b/src/lib/gramjs/tl/static/api.json index 30abe2149..69021c00a 100644 --- a/src/lib/gramjs/tl/static/api.json +++ b/src/lib/gramjs/tl/static/api.json @@ -77,6 +77,7 @@ "contacts.addContact", "contacts.resolvePhone", "contacts.editCloseFriends", + "contacts.getSponsoredPeers", "messages.getMessages", "messages.getDialogs", "messages.getHistory", diff --git a/src/types/language.d.ts b/src/types/language.d.ts index e31839fe8..611e964a9 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -685,6 +685,7 @@ export interface LangPair { 'MessageRecommendedLabel': undefined; 'SponsoredMessageAd': undefined; 'SponsoredMessageAdWhatIsThis': undefined; + 'SponsoredPeerBadge': undefined; 'PremiumStickerTooltip': undefined; 'ViewAction': undefined; 'Loading': undefined;