TelegramPWA/src/components/middle/message/SponsoredMessage.tsx

215 lines
6.6 KiB
TypeScript

import type { RefObject } from 'react';
import type { FC } from '../../../lib/teact/teact';
import React, { memo, useEffect, useRef } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type { ApiSponsoredMessage } from '../../../api/types';
import { selectSponsoredMessage } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import { IS_ANDROID } from '../../../util/windowEnvironment';
import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities';
import { preventMessageInputBlur } from '../helpers/preventMessageInputBlur';
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
import useFlag from '../../../hooks/useFlag';
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import AboutAdsModal from '../../common/AboutAdsModal.async';
import Avatar from '../../common/Avatar';
import Icon from '../../common/icons/Icon';
import PeerColorWrapper from '../../common/PeerColorWrapper';
import Button from '../../ui/Button';
import MessageAppendix from './MessageAppendix';
import SponsoredMessageContextMenuContainer from './SponsoredMessageContextMenuContainer.async';
import './SponsoredMessage.scss';
type OwnProps = {
chatId: string;
containerRef: RefObject<HTMLDivElement>;
};
type StateProps = {
message?: ApiSponsoredMessage;
};
const INTERSECTION_DEBOUNCE_MS = 200;
const SponsoredMessage: FC<OwnProps & StateProps> = ({
chatId,
message,
containerRef,
}) => {
const {
viewSponsoredMessage,
openUrl,
hideSponsoredMessages,
clickSponsoredMessage,
reportSponsoredMessage,
} = getActions();
const lang = useOldLang();
// eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement>(null);
// eslint-disable-next-line no-null/no-null
const contentRef = useRef<HTMLDivElement>(null);
const shouldObserve = Boolean(message);
const {
observe: observeIntersection,
} = useIntersectionObserver({
rootRef: containerRef,
debounceMs: INTERSECTION_DEBOUNCE_MS,
threshold: 1,
});
const {
isContextMenuOpen, contextMenuAnchor,
handleBeforeContextMenu, handleContextMenu,
handleContextMenuClose, handleContextMenuHide,
} = useContextMenuHandlers(ref, undefined, true, IS_ANDROID);
const [isAboutAdsModalOpen, openAboutAdsModal, closeAboutAdsModal] = useFlag(false);
useEffect(() => {
return shouldObserve ? observeIntersection(contentRef.current!, (target) => {
if (target.isIntersecting) {
viewSponsoredMessage({ chatId });
}
}) : undefined;
}, [chatId, shouldObserve, observeIntersection, viewSponsoredMessage]);
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement, MouseEvent>) => {
preventMessageInputBlur(e);
handleBeforeContextMenu(e);
};
const handleReportSponsoredMessage = useLastCallback(() => {
reportSponsoredMessage({ chatId, randomId: message!.randomId });
});
const handleHideSponsoredMessage = useLastCallback(() => {
hideSponsoredMessages();
});
const handleClick = useLastCallback(() => {
if (!message) return;
clickSponsoredMessage({ chatId });
openUrl({ url: message!.url, shouldSkipModal: true });
});
if (!message) {
return undefined;
}
function renderContent() {
if (!message) return undefined;
return (
<>
<div className="message-title message-peer" dir="auto">{message.title}</div>
<div className="text-content with-meta" dir="auto" ref={contentRef}>
<span className="text-content-inner" dir="auto">
{renderTextWithEntities({
text: message!.text.text,
entities: message!.text.entities,
})}
</span>
</div>
<Button
className="SponsoredMessage__button"
size="tiny"
color="translucent"
isRectangular
onClick={handleClick}
>
{message.buttonText}
</Button>
</>
);
}
return (
<div
ref={ref}
key="sponsored-message"
className="SponsoredMessage Message open"
>
<div
className="message-content has-shadow has-solid-background has-appendix"
dir="auto"
onMouseDown={handleMouseDown}
onContextMenu={handleContextMenu}
>
<PeerColorWrapper peerColor={message.peerColor} className="content-inner" dir="auto">
{message.photo && (
<Avatar
size="large"
photo={message.photo}
className={buildClassName('channel-avatar', lang.isRtl && 'is-rtl')}
/>
)}
<span className="message-title message-type">
{message!.isRecommended ? lang('Message.RecommendedLabel') : lang('SponsoredMessage')}
<span onClick={openAboutAdsModal} className="ad-about">{lang('SponsoredMessageAdWhatIsThis')}</span>
</span>
{renderContent()}
</PeerColorWrapper>
<MessageAppendix />
<div className="message-action-buttons">
<Button
className="message-action-button"
color="translucent-white"
round
size="tiny"
ariaLabel={lang('Close')}
onClick={handleHideSponsoredMessage}
>
<Icon name="close" />
</Button>
{message.canReport && (
<Button
className="message-action-button"
color="translucent-white"
round
size="tiny"
ariaLabel={lang('More')}
onClick={handleContextMenu}
onContextMenu={handleContextMenu}
>
<Icon name="more" />
</Button>
)}
</div>
</div>
{contextMenuAnchor && (
<SponsoredMessageContextMenuContainer
isOpen={isContextMenuOpen}
anchor={contextMenuAnchor}
message={message!}
onAboutAdsClick={openAboutAdsModal}
onReportAd={handleReportSponsoredMessage}
onClose={handleContextMenuClose}
onCloseAnimationEnd={handleContextMenuHide}
/>
)}
<AboutAdsModal
isOpen={isAboutAdsModalOpen}
isMonetizationSharing={message.canReport}
onClose={closeAboutAdsModal}
/>
</div>
);
};
export default memo(withGlobal<OwnProps>(
(global, { chatId }): StateProps => {
const message = selectSponsoredMessage(global, chatId);
return {
message,
};
},
)(SponsoredMessage));