diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index 9499d0d0b..29ce536f1 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -704,7 +704,8 @@ "Answer_other" = "{count} answers"; "Vote" = "Vote"; "MessageRecommendedLabel" = "recommended"; -"SponsoredMessage" = "ad"; +"SponsoredMessageAd" = "Ad"; +"SponsoredMessageAdWhatIsThis" = "what's this?"; "PremiumStickerTooltip" = "This set contains premium stickers like this one."; "ViewAction" = "View"; "Loading" = "Loading..."; diff --git a/src/bundles/extra.ts b/src/bundles/extra.ts index 75274aca4..fdd3caaea 100644 --- a/src/bundles/extra.ts +++ b/src/bundles/extra.ts @@ -25,7 +25,7 @@ export { default as GiftCodeModal } from '../components/modals/giftcode/GiftCode export { default as ChatlistModal } from '../components/modals/chatlist/ChatlistModal'; export { default as ChatInviteModal } from '../components/modals/chatInvite/ChatInviteModal'; -export { default as AboutAdsModal } from '../components/common/AboutAdsModal'; +export { default as AboutAdsModal } from '../components/modals/aboutAds/AboutAdsModal'; export { default as AboutMonetizationModal } from '../components/common/AboutMonetizationModal'; export { default as VerificationMonetizationModal } from '../components/common/VerificationMonetizationModal'; export { default as ReportAdModal } from '../components/modals/reportAd/ReportAdModal'; diff --git a/src/components/common/AboutAdsModal.tsx b/src/components/common/AboutAdsModal.tsx deleted file mode 100644 index 5516a45a7..000000000 --- a/src/components/common/AboutAdsModal.tsx +++ /dev/null @@ -1,127 +0,0 @@ -import type { FC } from '../../lib/teact/teact'; -import React, { memo, useMemo } from '../../lib/teact/teact'; - -import type { TableAboutData } from '../modals/common/TableAboutModal'; - -import buildClassName from '../../util/buildClassName'; -import renderText from './helpers/renderText'; - -import useSelectorSignal from '../../hooks/data/useSelectorSignal'; -import useDerivedState from '../../hooks/useDerivedState'; -import useOldLang from '../../hooks/useOldLang'; - -import TableAboutModal from '../modals/common/TableAboutModal'; -import Button from '../ui/Button'; -import Modal from '../ui/Modal'; -import SafeLink from './SafeLink'; - -import styles from './AboutAdsModal.module.scss'; - -export type OwnProps = { - isOpen: boolean; - isMonetizationSharing?: boolean; - onClose: NoneToVoidFunction; -}; - -const AboutAdsModal: FC = ({ - isOpen, - isMonetizationSharing, - onClose, -}) => { - const oldLang = useOldLang(); - - const minLevelSignal = useSelectorSignal((global) => global.appConfig?.channelRestrictAdsLevelMin); - const minLevelToRestrictAds = useDerivedState(minLevelSignal); - - const regularAdContent = useMemo(() => { - return ( - <> -

{oldLang('SponsoredMessageInfoScreen.Title')}

-

{renderText(oldLang('SponsoredMessageInfoDescription1'), ['br'])}

-

{renderText(oldLang('SponsoredMessageInfoDescription2'), ['br'])}

-

{renderText(oldLang('SponsoredMessageInfoDescription3'), ['br'])}

-

- -

-

{renderText(oldLang('SponsoredMessageInfoDescription4'), ['br'])}

- - ); - }, [oldLang]); - - const modalData = useMemo(() => { - if (!isOpen) return undefined; - - const header = ( - <> -

{oldLang('AboutRevenueSharingAds')}

-

- {oldLang('RevenueSharingAdsAlertSubtitle')} -

- - ); - - const listItemData = [ - ['lock', oldLang('RevenueSharingAdsInfo1Title'), - renderText(oldLang('RevenueSharingAdsInfo1Subtitle'), ['simple_markdown'])], - ['revenue-split', oldLang('RevenueSharingAdsInfo2Title'), - renderText(oldLang('RevenueSharingAdsInfo2Subtitle'), ['simple_markdown'])], - ['nochannel', oldLang('RevenueSharingAdsInfo3Title'), - renderText(oldLang('RevenueSharingAdsInfo3Subtitle', minLevelToRestrictAds), ['simple_markdown'])], - ] satisfies TableAboutData; - - const footer = ( - <> -

{renderText(oldLang('RevenueSharingAdsInfo4Title'), ['simple_markdown'])}

-

- {renderText(oldLang('RevenueSharingAdsInfo4Subtitle2', ''), ['simple_markdown'])} - -

- - ); - - return { - header, - listItemData, - footer, - }; - }, [isOpen, oldLang, minLevelToRestrictAds]); - - if (isMonetizationSharing && modalData) { - return ( - - ); - } - - return ( - - {regularAdContent} - - - ); -}; - -export default memo(AboutAdsModal); diff --git a/src/components/common/BadgeButton.tsx b/src/components/common/BadgeButton.tsx index f308e2c5b..68cdf9696 100644 --- a/src/components/common/BadgeButton.tsx +++ b/src/components/common/BadgeButton.tsx @@ -7,7 +7,7 @@ import styles from './BadgeButton.module.scss'; type OwnProps = { children: React.ReactNode; className?: string; - onClick?: NoneToVoidFunction; + onClick?: (e: React.MouseEvent) => void; }; const BadgeButton = ({ diff --git a/src/components/middle/MessageList.tsx b/src/components/middle/MessageList.tsx index b8bac7dce..d457cdcee 100644 --- a/src/components/middle/MessageList.tsx +++ b/src/components/middle/MessageList.tsx @@ -227,10 +227,11 @@ const MessageList: FC = ({ }, [firstUnreadId]); useEffect(() => { - if (areAdsEnabled && isChannelChat && isSynced && isReady) { + const canHaveAds = isChannelChat || isBot; + if (areAdsEnabled && canHaveAds && isSynced && isReady) { loadSponsoredMessages({ peerId: chatId }); } - }, [chatId, isSynced, isReady, isChannelChat, areAdsEnabled]); + }, [chatId, isSynced, isReady, isChannelChat, isBot, areAdsEnabled]); // Updated only once when messages are loaded (as we want the unread divider to keep its position) useSyncEffect(() => { @@ -695,7 +696,7 @@ const MessageList: FC = ({ /> ) : hasMessages ? ( = ({ - areAdsEnabled, + canShowAds, chatId, threadId, messageIds, @@ -308,7 +308,7 @@ const MessageListContent: FC = ({ key="fab-trigger" className="fab-trigger" /> - {areAdsEnabled && isViewportNewest && ( + {canShowAds && isViewportNewest && ( (FALLBACK_PANE_STATE); const [getGroupCallState, setGroupCallState] = useSignal(FALLBACK_PANE_STATE); const [getChatReportState, setChatReportState] = useSignal(FALLBACK_PANE_STATE); + const [getBotAdState, setBotAdState] = useSignal(FALLBACK_PANE_STATE); const isPinnedMessagesFullWidth = isAudioPlayerRendered || !isDesktop; @@ -85,9 +87,10 @@ const MiddleHeaderPanes = ({ const pinnedState = getPinnedState(); const groupCallState = getGroupCallState(); const chatReportState = getChatReportState(); + const botAdState = getBotAdState(); // Keep in sync with the order of the panes in the DOM - const stateArray = [audioPlayerState, groupCallState, chatReportState, pinnedState]; + const stateArray = [audioPlayerState, groupCallState, chatReportState, pinnedState, botAdState]; const isFirstRender = isFirstRenderRef.current; const totalHeight = stateArray.reduce((acc, state) => acc + state.height, 0); @@ -100,7 +103,7 @@ const MiddleHeaderPanes = ({ setExtraStyles(middleColumn, { '--middle-header-panes-height': `${totalHeight}px`, }); - }, [getAudioPlayerState, getGroupCallState, getPinnedState, getChatReportState]); + }, [getAudioPlayerState, getGroupCallState, getPinnedState, getChatReportState, getBotAdState]); if (!shouldRender) return undefined; @@ -136,6 +139,11 @@ const MiddleHeaderPanes = ({ isFullWidth shouldHide={!isPinnedMessagesFullWidth} /> + ); }; diff --git a/src/components/middle/hooks/useHeaderPane.tsx b/src/components/middle/hooks/useHeaderPane.tsx index afd3e60c0..99e01bb1f 100644 --- a/src/components/middle/hooks/useHeaderPane.tsx +++ b/src/components/middle/hooks/useHeaderPane.tsx @@ -1,5 +1,6 @@ import { type RefObject, + useCallback, useEffect, useLayoutEffect, useRef, @@ -12,6 +13,8 @@ import { requestForcedReflow, requestNextMutation } from '../../../lib/fasterdom import useTimeout from '../../../hooks/schedulers/useTimeout'; import useLastCallback from '../../../hooks/useLastCallback'; +import useResizeObserver from '../../../hooks/useResizeObserver'; +import useThrottledCallback from '../../../hooks/useThrottledCallback'; export interface PaneState { element?: HTMLElement; @@ -21,16 +24,19 @@ export interface PaneState { // Max slide transition duration const CLOSE_DURATION = 450; +const RESIZE_THROTTLE = 100; export default function useHeaderPane({ ref: providedRef, isOpen, isDisabled, + withResizeObserver, onStateChange, } : { ref?: RefObject; isOpen?: boolean; isDisabled?: boolean; + withResizeObserver?: boolean; onStateChange?: (state: PaneState) => void; }) { const [shouldRender, setShouldRender] = useState(true); @@ -38,6 +44,8 @@ export default function useHeaderPane(null); const ref = providedRef || localRef; + const lastHeightRef = useRef(0); + const reset = useLastCallback(() => { setShouldRender(true); onStateChange?.({ @@ -65,7 +73,8 @@ export default function useHeaderPane { + // Should be `useCallback` to trigger effect on deps change + const handleUpdate = useCallback(() => { const element = ref.current; if (isDisabled || !element || !shouldRender) return; @@ -80,6 +89,7 @@ export default function useHeaderPane { const currentHeight = element.offsetHeight; + lastHeightRef.current = currentHeight; return () => { onStateChange?.({ element, @@ -90,6 +100,22 @@ export default function useHeaderPane { + const element = ref.current; + if (!element) return; + + const newHeight = element.offsetHeight; + if (newHeight === lastHeightRef.current) { + return; + } + + handleUpdate(); + }, [handleUpdate, ref], RESIZE_THROTTLE, true); + + useLayoutEffect(handleUpdate, [handleUpdate]); + + useResizeObserver(ref, handleResize, !withResizeObserver || !shouldRender); + return { shouldRender, ref, diff --git a/src/components/middle/message/MessageContextMenu.tsx b/src/components/middle/message/MessageContextMenu.tsx index f809dd8a5..733ce75a0 100644 --- a/src/components/middle/message/MessageContextMenu.tsx +++ b/src/components/middle/message/MessageContextMenu.tsx @@ -12,7 +12,6 @@ import type { ApiPeer, ApiPoll, ApiReaction, - ApiSponsoredMessage, ApiStickerSet, ApiThreadInfo, ApiTypeStory, @@ -51,7 +50,7 @@ type OwnProps = { isOpen: boolean; anchor: IAnchorPosition; targetHref?: string; - message: ApiMessage | ApiSponsoredMessage; + message: ApiMessage; poll?: ApiPoll; story?: ApiTypeStory; canSendNow?: boolean; @@ -119,10 +118,6 @@ type OwnProps = { onClosePoll?: NoneToVoidFunction; onShowSeenBy?: NoneToVoidFunction; onShowReactors?: NoneToVoidFunction; - onAboutAdsClick?: NoneToVoidFunction; - onSponsoredHide?: NoneToVoidFunction; - onSponsorInfo?: NoneToVoidFunction; - onSponsoredReport?: NoneToVoidFunction; onTranslate?: NoneToVoidFunction; onShowOriginal?: NoneToVoidFunction; onSelectLanguage?: NoneToVoidFunction; @@ -215,10 +210,6 @@ const MessageContextMenu: FC = ({ onSendPaidReaction, onShowPaidReactionModal, onCopyMessages, - onAboutAdsClick, - onSponsoredHide, - onSponsorInfo, - onSponsoredReport, onReactionPickerOpen, onTranslate, onShowOriginal, @@ -234,10 +225,8 @@ const MessageContextMenu: FC = ({ const lang = useOldLang(); const noReactions = !isPrivate && !enabledReactions; const withReactions = canShowReactionList && !noReactions; - const isSponsoredMessage = !('id' in message); const isEdited = ('isEdited' in message) && message.isEdited; - const messageId = !isSponsoredMessage ? message.id : ''; - const seenByDates = !isSponsoredMessage ? message.seenByDates : undefined; + const seenByDates = message.seenByDates; const [areItemsHidden, hideItems] = useFlag(); const [isReady, markIsReady, unmarkIsReady] = useFlag(); @@ -286,23 +275,19 @@ const MessageContextMenu: FC = ({ onClose(); }); - const copyOptions = isSponsoredMessage - ? [] - : getMessageCopyOptions( - message, - groupStatetefulContent({ poll, story }), - targetHref, - canCopy, - handleAfterCopy, - canCopyLink ? onCopyLink : undefined, - onCopyMessages, - onCopyNumber, - ); + const copyOptions = getMessageCopyOptions( + message, + groupStatetefulContent({ poll, story }), + targetHref, + canCopy, + handleAfterCopy, + canCopyLink ? onCopyLink : undefined, + onCopyMessages, + onCopyNumber, + ); const getTriggerElement = useLastCallback(() => { - return isSponsoredMessage - ? document.querySelector('.Transition_slide-active > .MessageList .SponsoredMessage') - : document.querySelector(`.Transition_slide-active > .MessageList div[data-message-id="${messageId}"]`); + return document.querySelector(`.Transition_slide-active > .MessageList div[data-message-id="${message.id}"]`); }); const getRootElement = useLastCallback(() => document.querySelector('.Transition_slide-active > .MessageList')); @@ -368,7 +353,7 @@ const MessageContextMenu: FC = ({ topReactions={topReactions} allAvailableReactions={availableReactions} defaultTagReactions={defaultTagReactions} - currentReactions={!isSponsoredMessage ? message.reactions?.results : undefined} + currentReactions={message.reactions?.results} reactionsLimit={reactionsLimit} onToggleReaction={onToggleReaction!} onSendPaidReaction={onSendPaidReaction} @@ -464,26 +449,7 @@ const MessageContextMenu: FC = ({ )} )} - {isSponsoredMessage && message.sponsorInfo && ( - {lang('SponsoredMessageSponsor')} - )} - {isSponsoredMessage && ( - - {lang(message.canReport ? 'AboutRevenueSharingAds' : 'SponsoredMessageInfo')} - - )} - {isSponsoredMessage && message.canReport && ( - - {lang('ReportAd')} - - )} - {isSponsoredMessage && onSponsoredHide && ( - <> - - {lang('HideAd')} - - )} - {(canShowSeenBy || canShowReactionsCount) && !isSponsoredMessage && ( + {(canShowSeenBy || canShowReactionsCount) && ( <> = ({ )} - {((!isSponsoredMessage && (canLoadReadDate || shouldRenderShowWhen)) || isEdited) && ( + {(canLoadReadDate || shouldRenderShowWhen || isEdited) && ( )} - {!isSponsoredMessage && (canLoadReadDate || shouldRenderShowWhen) && ( + {(canLoadReadDate || shouldRenderShowWhen) && ( = ({ openUrl, hideSponsoredMessages, clickSponsoredMessage, - reportSponsoredMessage, openMediaViewer, + openAboutAdsModal, } = getActions(); const lang = useOldLang(); @@ -101,7 +100,6 @@ const SponsoredMessage: FC = ({ handleBeforeContextMenu, handleContextMenu, handleContextMenuClose, handleContextMenuHide, } = useContextMenuHandlers(ref, undefined, true, IS_ANDROID); - const [isAboutAdsModalOpen, openAboutAdsModal, closeAboutAdsModal] = useFlag(false); useEffect(() => { return shouldObserve ? observeIntersection(contentRef.current!, (target) => { @@ -116,10 +114,6 @@ const SponsoredMessage: FC = ({ handleBeforeContextMenu(e); }; - const handleReportSponsoredMessage = useLastCallback(() => { - reportSponsoredMessage({ peerId: chatId, randomId: message!.randomId }); - }); - const handleHideSponsoredMessage = useLastCallback(() => { hideSponsoredMessages(); }); @@ -135,7 +129,7 @@ const SponsoredMessage: FC = ({ if (!message) return; clickSponsoredMessage({ isMedia: photo || isGif ? true : undefined, peerId: chatId }); - openUrl({ url: message!.url, shouldSkipModal: true }); + openUrl({ url: message.url, shouldSkipModal: true }); }); const handleOpenMedia = useLastCallback(() => { @@ -147,6 +141,10 @@ const SponsoredMessage: FC = ({ }); }); + const handleOpenAboutAdsModal = useLastCallback(() => { + openAboutAdsModal({ chatId }); + }); + const extraPadding = 0; const sizeCalculations = useMemo(() => { @@ -283,7 +281,9 @@ const SponsoredMessage: FC = ({ )} {message!.isRecommended ? lang('Message.RecommendedLabel') : lang('SponsoredMessage')} - {lang('SponsoredMessageAdWhatIsThis')} + + {lang('SponsoredMessageAdWhatIsThis')} + {renderContent()} @@ -318,18 +318,12 @@ const SponsoredMessage: FC = ({ )} - ); }; diff --git a/src/components/middle/message/SponsoredMessageContextMenu.tsx b/src/components/middle/message/SponsoredMessageContextMenu.tsx new file mode 100644 index 000000000..bd036db19 --- /dev/null +++ b/src/components/middle/message/SponsoredMessageContextMenu.tsx @@ -0,0 +1,93 @@ +import type { FC } from '../../../lib/teact/teact'; +import React, { + memo, useRef, +} from '../../../lib/teact/teact'; + +import type { + ApiSponsoredMessage, +} from '../../../api/types'; +import type { IAnchorPosition } from '../../../types'; + +import useLastCallback from '../../../hooks/useLastCallback'; +import useOldLang from '../../../hooks/useOldLang'; + +import Menu from '../../ui/Menu'; +import MenuItem from '../../ui/MenuItem'; +import MenuSeparator from '../../ui/MenuSeparator'; + +import './MessageContextMenu.scss'; + +type OwnProps = { + isOpen: boolean; + anchor: IAnchorPosition; + message: ApiSponsoredMessage; + triggerRef: React.RefObject; + shouldSkipAbout?: boolean; + onClose: NoneToVoidFunction; + onCloseAnimationEnd?: NoneToVoidFunction; + onAboutAdsClick: NoneToVoidFunction; + onSponsoredHide: NoneToVoidFunction; + onSponsorInfo?: NoneToVoidFunction; + onSponsoredReport?: NoneToVoidFunction; +}; + +const SponsoredMessageContextMenu: FC = ({ + isOpen, + message, + anchor, + triggerRef, + shouldSkipAbout, + onClose, + onCloseAnimationEnd, + onAboutAdsClick, + onSponsoredHide, + onSponsorInfo, + onSponsoredReport, +}) => { + // eslint-disable-next-line no-null/no-null + const menuRef = useRef(null); + const lang = useOldLang(); + + const getTriggerElement = useLastCallback(() => triggerRef.current); + const getLayout = useLastCallback(() => ({ withPortal: true })); + const getMenuElement = useLastCallback(() => menuRef.current); + const getRootElement = useLastCallback(() => document.body); + + const isSeparatorNeeded = message.sponsorInfo || !shouldSkipAbout || message.canReport; + + return ( + + {message.sponsorInfo && onSponsorInfo && ( + {lang('SponsoredMessageSponsor')} + )} + {!shouldSkipAbout && ( + + {lang(message.canReport ? 'AboutRevenueSharingAds' : 'SponsoredMessageInfo')} + + )} + {message.canReport && onSponsoredReport && ( + + {lang('ReportAd')} + + )} + {isSeparatorNeeded && } + + {lang('HideAd')} + + + ); +}; + +export default memo(SponsoredMessageContextMenu); diff --git a/src/components/middle/message/SponsoredMessageContextMenuContainer.tsx b/src/components/middle/message/SponsoredMessageContextMenuContainer.tsx index 8682f7bce..2d26613b6 100644 --- a/src/components/middle/message/SponsoredMessageContextMenuContainer.tsx +++ b/src/components/middle/message/SponsoredMessageContextMenuContainer.tsx @@ -5,60 +5,71 @@ import { getActions } from '../../../global'; import type { ApiSponsoredMessage } from '../../../api/types'; import type { IAnchorPosition } from '../../../types'; -import buildClassName from '../../../util/buildClassName'; - -import useFlag from '../../../hooks/useFlag'; import useLastCallback from '../../../hooks/useLastCallback'; -import useShowTransitionDeprecated from '../../../hooks/useShowTransitionDeprecated'; +import useShowTransition from '../../../hooks/useShowTransition'; -import MessageContextMenu from './MessageContextMenu'; +import SponsoredMessageContextMenu from './SponsoredMessageContextMenu'; export type OwnProps = { isOpen: boolean; message: ApiSponsoredMessage; anchor: IAnchorPosition; - onAboutAdsClick: NoneToVoidFunction; - onReportAd: NoneToVoidFunction; + triggerRef: React.RefObject; + shouldSkipAbout?: boolean; + onItemClick?: NoneToVoidFunction; onClose: NoneToVoidFunction; onCloseAnimationEnd: NoneToVoidFunction; }; const SponsoredMessageContextMenuContainer: FC = ({ + isOpen, message, anchor, - onAboutAdsClick, - onReportAd, + triggerRef, + shouldSkipAbout, + onItemClick, onClose, onCloseAnimationEnd, }) => { - const { openPremiumModal, showDialog } = getActions(); + const { + openAboutAdsModal, + showDialog, + reportSponsoredMessage, + hideSponsoredMessages, + } = getActions(); - const [isMenuOpen, , closeMenu] = useFlag(true); - const { transitionClassNames } = useShowTransitionDeprecated(isMenuOpen, onCloseAnimationEnd, undefined, false); - - const handleAboutAdsOpen = useLastCallback(() => { - onAboutAdsClick(); - closeMenu(); + const { ref } = useShowTransition({ + isOpen, + onCloseAnimationEnd, }); - const handleSponsoredHide = useLastCallback(() => { - closeMenu(); - openPremiumModal(); + const handleItemClick = useLastCallback(() => { + onItemClick?.(); onClose(); }); + const handleAboutAdsOpen = useLastCallback(() => { + openAboutAdsModal({ chatId: message.chatId }); + handleItemClick(); + }); + + const handleSponsoredHide = useLastCallback(() => { + hideSponsoredMessages(); + handleItemClick(); + }); + const handleSponsorInfo = useLastCallback(() => { - closeMenu(); showDialog({ data: { message: [message.sponsorInfo, message.additionalInfo].join('\n'), }, }); + handleItemClick(); }); const handleReportSponsoredMessage = useLastCallback(() => { - closeMenu(); - onReportAd(); + reportSponsoredMessage({ peerId: message.chatId, randomId: message.randomId }); + handleItemClick(); }); if (!anchor) { @@ -66,13 +77,15 @@ const SponsoredMessageContextMenuContainer: FC = ({ } return ( -
- + void; +}; + +type StateProps = { + isBot?: boolean; + sponsoredMessage?: ApiSponsoredMessage; +}; + +const BotAdPane = ({ + chatId, + isBot, + messageListType, + sponsoredMessage, + onPaneStateChange, +}: OwnProps & StateProps) => { + const { + viewSponsoredMessage, + openUrl, + clickSponsoredMessage, + openAboutAdsModal, + } = getActions(); + + const lang = useLang(); + + const isOpen = Boolean(isBot && sponsoredMessage && messageListType === 'thread'); + + const renderingSponsoredMessage = useCurrentOrPrev(sponsoredMessage); + + const { ref, shouldRender } = useHeaderPane({ + isOpen, + withResizeObserver: true, + onStateChange: onPaneStateChange, + }); + + const { + isContextMenuOpen, contextMenuAnchor, + handleBeforeContextMenu, handleContextMenu, + handleContextMenuClose, handleContextMenuHide, + } = useContextMenuHandlers(ref, !shouldRender, true); + + const handleClick = useLastCallback(() => { + if (!renderingSponsoredMessage) return; + + clickSponsoredMessage({ peerId: chatId }); + openUrl({ url: renderingSponsoredMessage.url, shouldSkipModal: true }); + }); + + const handleAboutClick = useLastCallback((e: React.MouseEvent) => { + e.stopPropagation(); + openAboutAdsModal({ chatId }); + }); + + useEffect(() => { + if (shouldRender && sponsoredMessage) { + viewSponsoredMessage({ peerId: chatId }); + } + }, [shouldRender, sponsoredMessage, chatId]); + + if (!shouldRender || !renderingSponsoredMessage) { + return undefined; + } + + const { + peerColor, + content, + photo, + title, + } = renderingSponsoredMessage; + + return ( + <> +
+
+ + {lang('SponsoredMessageAd')} + + {lang('SponsoredMessageAdWhatIsThis')} + + +
{title}
+ {content.text && ( +
+ {renderTextWithEntities({ + text: content.text.text, + entities: content.text.entities, + })} +
+ )} +
+ {photo && ( + + )} +
+ {contextMenuAnchor && ( + + )} + + ); +}; + +export default memo(withGlobal( + (global, { chatId }): StateProps => { + const bot = selectBot(global, chatId); + const sponsoredMessage = selectSponsoredMessage(global, chatId); + return { + isBot: Boolean(bot), + sponsoredMessage, + }; + }, +)(BotAdPane)); diff --git a/src/components/modals/ModalContainer.tsx b/src/components/modals/ModalContainer.tsx index 86ec90c47..fd3cc0ca6 100644 --- a/src/components/modals/ModalContainer.tsx +++ b/src/components/modals/ModalContainer.tsx @@ -7,6 +7,7 @@ import { selectTabState } from '../../global/selectors'; import { pick } from '../../util/iteratees'; import WebAppsCloseConfirmationModal from '../main/WebAppsCloseConfirmationModal.async'; +import AboutAdsModal from './aboutAds/AboutAdsModal.async'; import AttachBotInstallModal from './attachBotInstall/AttachBotInstallModal.async'; import BoostModal from './boost/BoostModal.async'; import ChatInviteModal from './chatInvite/ChatInviteModal.async'; @@ -55,7 +56,8 @@ type ModalKey = keyof Pick; type StateProps = { @@ -94,6 +96,7 @@ const MODALS: ModalRegistry = { isGiftRecipientPickerOpen: GiftRecipientPicker, isWebAppsCloseConfirmationModalOpen: WebAppsCloseConfirmationModal, giftInfoModal: GiftInfoModal, + aboutAdsModal: AboutAdsModal, }; const MODAL_KEYS = Object.keys(MODALS) as ModalKey[]; const MODAL_ENTRIES = Object.entries(MODALS) as Entries; diff --git a/src/components/common/AboutAdsModal.async.tsx b/src/components/modals/aboutAds/AboutAdsModal.async.tsx similarity index 56% rename from src/components/common/AboutAdsModal.async.tsx rename to src/components/modals/aboutAds/AboutAdsModal.async.tsx index ff15e3a9d..884ed54c8 100644 --- a/src/components/common/AboutAdsModal.async.tsx +++ b/src/components/modals/aboutAds/AboutAdsModal.async.tsx @@ -1,15 +1,15 @@ -import type { FC } from '../../lib/teact/teact'; -import React from '../../lib/teact/teact'; +import type { FC } from '../../../lib/teact/teact'; +import React from '../../../lib/teact/teact'; import type { OwnProps } from './AboutAdsModal'; -import { Bundles } from '../../util/moduleLoader'; +import { Bundles } from '../../../util/moduleLoader'; -import useModuleLoader from '../../hooks/useModuleLoader'; +import useModuleLoader from '../../../hooks/useModuleLoader'; const AboutAdsModalAsync: FC = (props) => { - const { isOpen } = props; - const AboutAdsModal = useModuleLoader(Bundles.Extra, 'AboutAdsModal', !isOpen); + const { modal } = props; + const AboutAdsModal = useModuleLoader(Bundles.Extra, 'AboutAdsModal', !modal); // eslint-disable-next-line react/jsx-props-no-spreading return AboutAdsModal ? : undefined; diff --git a/src/components/common/AboutAdsModal.module.scss b/src/components/modals/aboutAds/AboutAdsModal.module.scss similarity index 76% rename from src/components/common/AboutAdsModal.module.scss rename to src/components/modals/aboutAds/AboutAdsModal.module.scss index 88ba2f0be..0085e9390 100644 --- a/src/components/common/AboutAdsModal.module.scss +++ b/src/components/modals/aboutAds/AboutAdsModal.module.scss @@ -8,6 +8,12 @@ padding-inline: 1.5rem; } +.moreButton { + position: absolute; + top: 0.5rem; + right: 0.5rem; +} + .secondary { color: var(--color-text-secondary); } @@ -16,4 +22,5 @@ display: flex; flex-direction: column; align-items: center; + line-height: 1.25; } diff --git a/src/components/modals/aboutAds/AboutAdsModal.tsx b/src/components/modals/aboutAds/AboutAdsModal.tsx new file mode 100644 index 000000000..e64a8c72f --- /dev/null +++ b/src/components/modals/aboutAds/AboutAdsModal.tsx @@ -0,0 +1,185 @@ +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'; + +import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers'; +import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev'; +import useLastCallback from '../../../hooks/useLastCallback'; +import useOldLang from '../../../hooks/useOldLang'; + +import Icon from '../../common/icons/Icon'; +import SafeLink from '../../common/SafeLink'; +import SponsoredMessageContextMenuContainer from '../../middle/message/SponsoredMessageContextMenuContainer'; +import Button from '../../ui/Button'; +import Modal from '../../ui/Modal'; +import TableAboutModal from '../common/TableAboutModal'; + +import styles from './AboutAdsModal.module.scss'; + +export type OwnProps = { + // eslint-disable-next-line react/no-unused-prop-types + modal: TabState['aboutAdsModal']; +}; + +type StateProps = { + message?: ApiSponsoredMessage; + minLevelToRestrictAds?: number; +}; + +const AboutAdsModal = ({ message, 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 renderingIsNewDesign = useCurrentOrPrev(isMonetizationSharing); + + const oldLang = useOldLang(); + + const regularAdContent = useMemo(() => { + return ( + <> +

{oldLang('SponsoredMessageInfoScreen.Title')}

+

{renderText(oldLang('SponsoredMessageInfoDescription1'), ['br'])}

+

{renderText(oldLang('SponsoredMessageInfoDescription2'), ['br'])}

+

{renderText(oldLang('SponsoredMessageInfoDescription3'), ['br'])}

+

+ +

+

{renderText(oldLang('SponsoredMessageInfoDescription4'), ['br'])}

+ + ); + }, [oldLang]); + + const { + isContextMenuOpen, contextMenuAnchor, + handleContextMenu, handleContextMenuClose, handleContextMenuHide, + } = useContextMenuHandlers(moreMenuRef, !renderingIsNewDesign); + + const handleClose = useLastCallback(() => { + closeAboutAdsModal(); + handleContextMenuClose(); + handleContextMenuHide(); + }); + + const modalData = useMemo(() => { + if (!isOpen) return undefined; + + const header = ( + <> +

{oldLang('AboutRevenueSharingAds')}

+

+ {oldLang('RevenueSharingAdsAlertSubtitle')} +

+ + + ); + + const listItemData = [ + ['lock', oldLang('RevenueSharingAdsInfo1Title'), + renderText(oldLang('RevenueSharingAdsInfo1Subtitle'), ['simple_markdown'])], + ['revenue-split', oldLang('RevenueSharingAdsInfo2Title'), + renderText(oldLang('RevenueSharingAdsInfo2Subtitle'), ['simple_markdown'])], + ['nochannel', oldLang('RevenueSharingAdsInfo3Title'), + renderText(oldLang('RevenueSharingAdsInfo3Subtitle', minLevelToRestrictAds), ['simple_markdown'])], + ] satisfies TableAboutData; + + const footer = ( + <> +

{renderText(oldLang('RevenueSharingAdsInfo4Title'), ['simple_markdown'])}

+

+ {renderText(oldLang('RevenueSharingAdsInfo4Subtitle2', ''), ['simple_markdown'])} + +

+ + ); + + return { + header, + listItemData, + footer, + }; + }, [isOpen, oldLang, handleContextMenu, minLevelToRestrictAds]); + + if (renderingIsNewDesign) { + return ( + <> + + {contextMenuAnchor && message && ( + + )} + + ); + } + + return ( + + {regularAdContent} + + + ); +}; + +export default memo(withGlobal( + (global, { modal }): StateProps => { + const message = modal?.chatId ? selectSponsoredMessage(global, modal.chatId) : undefined; + const minLevelToRestrictAds = global.appConfig?.channelRestrictAdsLevelMin; + + return { + message, + minLevelToRestrictAds, + }; + }, +)(AboutAdsModal)); diff --git a/src/components/modals/common/TableAboutModal.module.scss b/src/components/modals/common/TableAboutModal.module.scss index df6bbad6b..d9a65797d 100644 --- a/src/components/modals/common/TableAboutModal.module.scss +++ b/src/components/modals/common/TableAboutModal.module.scss @@ -31,9 +31,10 @@ display: flex; flex-direction: column; align-items: center; + overflow-x: hidden; } .separator { margin-block: 1rem; - width: 110%; + width: calc(100% + 2rem); // Hack to cover modal paddings } diff --git a/src/components/modals/common/TableAboutModal.tsx b/src/components/modals/common/TableAboutModal.tsx index 165fb4382..3e9bc30bd 100644 --- a/src/components/modals/common/TableAboutModal.tsx +++ b/src/components/modals/common/TableAboutModal.tsx @@ -59,7 +59,7 @@ const TableAboutModal = ({ {footer} {buttonText && ( - + )} ); diff --git a/src/components/modals/reportAd/ReportAdModal.module.scss b/src/components/modals/reportAd/ReportAdModal.module.scss index 73ab87944..e05987c9e 100644 --- a/src/components/modals/reportAd/ReportAdModal.module.scss +++ b/src/components/modals/reportAd/ReportAdModal.module.scss @@ -2,6 +2,12 @@ overflow-x: hidden; } +.root { + :global(.modal-dialog) { + max-width: 23rem; + } +} + .modalTitle { display: flex; flex-direction: column; @@ -15,7 +21,7 @@ } .option { - margin-bottom: 0.25rem !important; + margin-bottom: 0rem !important; } .optionButton { diff --git a/src/components/modals/reportAd/ReportAdModal.tsx b/src/components/modals/reportAd/ReportAdModal.tsx index d28c9e25c..c57ff0b1e 100644 --- a/src/components/modals/reportAd/ReportAdModal.tsx +++ b/src/components/modals/reportAd/ReportAdModal.tsx @@ -20,7 +20,7 @@ import Transition, { ACTIVE_SLIDE_CLASS_NAME, TO_SLIDE_CLASS_NAME } from '../../ import styles from './ReportAdModal.module.scss'; -const ADDED_PADDING = 40; +const ADDED_PADDING = 56; export type OwnProps = { modal: TabState['reportAdModal']; @@ -64,7 +64,11 @@ const ReportAdModal = ({ const parts = template.split('{link}'); return [ parts[0], - , + , parts[1], ]; }, [lang, modal]); @@ -123,6 +127,7 @@ const ReportAdModal = ({ diff --git a/src/components/ui/ListItem.scss b/src/components/ui/ListItem.scss index 832fb8930..c10816260 100644 --- a/src/components/ui/ListItem.scss +++ b/src/components/ui/ListItem.scss @@ -453,7 +453,7 @@ .subtitle { font-size: 0.875rem; - line-height: 1.5rem; + line-height: 1.25rem; color: var(--color-text-secondary); & + .subtitle { diff --git a/src/global/actions/api/messages.ts b/src/global/actions/api/messages.ts index 9adb07dc5..be746ba62 100644 --- a/src/global/actions/api/messages.ts +++ b/src/global/actions/api/messages.ts @@ -62,6 +62,7 @@ import { isServiceNotificationMessage, isUserBot, } from '../../helpers'; +import { isApiPeerUser } from '../../helpers/peers'; import { addActionHandler, getActions, getGlobal, setGlobal, } from '../../index'; @@ -112,6 +113,7 @@ import { selectFocusedMessageId, selectForwardsCanBeSentToChat, selectForwardsContainVoiceMessages, + selectIsChatBotNotStarted, selectIsChatWithSelf, selectIsCurrentUserPremium, selectLanguageCode, @@ -1631,6 +1633,10 @@ addActionHandler('loadSponsoredMessages', async (global, actions, payload): Prom return; } + if (isApiPeerUser(peer) && selectIsChatBotNotStarted(global, peer.id)) { + return; + } + const result = await callApi('fetchSponsoredMessages', { peer }); if (!result) { return; diff --git a/src/global/actions/ui/messages.ts b/src/global/actions/ui/messages.ts index 54dd1fa62..4209634c0 100644 --- a/src/global/actions/ui/messages.ts +++ b/src/global/actions/ui/messages.ts @@ -1050,3 +1050,21 @@ addActionHandler('closeDeleteMessageModal', (global, actions, payload): ActionRe deleteMessageModal: undefined, }, tabId); }); + +addActionHandler('openAboutAdsModal', (global, actions, payload): ActionReturnType => { + const { chatId, tabId = getCurrentTabId() } = payload || {}; + + return updateTabState(global, { + aboutAdsModal: { + chatId, + }, + }, tabId); +}); + +addActionHandler('closeAboutAdsModal', (global, actions, payload): ActionReturnType => { + const { tabId = getCurrentTabId() } = payload || {}; + + return updateTabState(global, { + aboutAdsModal: undefined, + }, tabId); +}); diff --git a/src/global/selectors/messages.ts b/src/global/selectors/messages.ts index 9ee98a57d..fcc70094e 100644 --- a/src/global/selectors/messages.ts +++ b/src/global/selectors/messages.ts @@ -1251,8 +1251,7 @@ export function selectCanForwardMessages(global: T, chatI } export function selectSponsoredMessage(global: T, chatId: string) { - const chat = selectChat(global, chatId); - const message = chat && isChatChannel(chat) ? global.messages.sponsoredByChatId[chatId] : undefined; + const message = global.messages.sponsoredByChatId[chatId]; return message && message.expiresAt >= Math.round(Date.now() / 1000) ? message : undefined; } diff --git a/src/global/types.ts b/src/global/types.ts index b933aac4e..65f91db3c 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -388,6 +388,10 @@ export type TabState = { messageId: number; }; + aboutAdsModal?: { + chatId: string; + }; + reactionPicker?: { chatId?: string; messageId?: number; @@ -1750,6 +1754,10 @@ export interface ActionPayloads { randomId: string; option?: string; } & WithTabId; + openAboutAdsModal: { + chatId: string; + } & WithTabId; + closeAboutAdsModal: WithTabId | undefined; openPreviousReportAdModal: WithTabId | undefined; openPreviousReportModal: WithTabId | undefined; closeReportAdModal: WithTabId | undefined; diff --git a/src/types/language.d.ts b/src/types/language.d.ts index d798b7c4e..769eb2618 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -610,7 +610,8 @@ export interface LangPair { 'ChatPollTotalVotesResultEmpty': undefined; 'Vote': undefined; 'MessageRecommendedLabel': undefined; - 'SponsoredMessage': undefined; + 'SponsoredMessageAd': undefined; + 'SponsoredMessageAdWhatIsThis': undefined; 'PremiumStickerTooltip': undefined; 'ViewAction': undefined; 'Loading': undefined; diff --git a/src/util/localization/types.ts b/src/util/localization/types.ts index b38c00da8..039ad43b3 100644 --- a/src/util/localization/types.ts +++ b/src/util/localization/types.ts @@ -116,10 +116,10 @@ type AdvancedLangFnParameters = export type LangFnParameters = RegularLangFnParameters | AdvancedLangFnParameters; export type LangFn = { - ( + ( key: K, variables?: undefined, options?: LangFnOptions, ): string; - ( + ( key: K, variables: undefined, options: LangFnOptionsWithPlural, ): string; ( @@ -129,10 +129,10 @@ export type LangFn = { key: K, variables: V, options: LangFnOptionsWithPlural, ): string; - ( + ( key: K, variables?: undefined, options?: AdvancedLangFnOptions, ): TeactNode; - ( + ( key: K, variables: undefined, options: AdvancedLangFnOptionsWithPlural, ): TeactNode; (