Middle Header: Support bot ads (#5220)
This commit is contained in:
parent
5a647a0a83
commit
228cd56091
@ -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...";
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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<OwnProps> = ({
|
||||
isOpen,
|
||||
isMonetizationSharing,
|
||||
onClose,
|
||||
}) => {
|
||||
const oldLang = useOldLang();
|
||||
|
||||
const minLevelSignal = useSelectorSignal((global) => global.appConfig?.channelRestrictAdsLevelMin);
|
||||
const minLevelToRestrictAds = useDerivedState(minLevelSignal);
|
||||
|
||||
const regularAdContent = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
<h3>{oldLang('SponsoredMessageInfoScreen.Title')}</h3>
|
||||
<p>{renderText(oldLang('SponsoredMessageInfoDescription1'), ['br'])}</p>
|
||||
<p>{renderText(oldLang('SponsoredMessageInfoDescription2'), ['br'])}</p>
|
||||
<p>{renderText(oldLang('SponsoredMessageInfoDescription3'), ['br'])}</p>
|
||||
<p>
|
||||
<SafeLink
|
||||
url={oldLang('SponsoredMessageAlertLearnMoreUrl')}
|
||||
text={oldLang('SponsoredMessageAlertLearnMoreUrl')}
|
||||
/>
|
||||
</p>
|
||||
<p>{renderText(oldLang('SponsoredMessageInfoDescription4'), ['br'])}</p>
|
||||
</>
|
||||
);
|
||||
}, [oldLang]);
|
||||
|
||||
const modalData = useMemo(() => {
|
||||
if (!isOpen) return undefined;
|
||||
|
||||
const header = (
|
||||
<>
|
||||
<h3 className={styles.title}>{oldLang('AboutRevenueSharingAds')}</h3>
|
||||
<p className={buildClassName(styles.description, styles.secondary)}>
|
||||
{oldLang('RevenueSharingAdsAlertSubtitle')}
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
|
||||
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 = (
|
||||
<>
|
||||
<h3 className={styles.title}>{renderText(oldLang('RevenueSharingAdsInfo4Title'), ['simple_markdown'])}</h3>
|
||||
<p className={styles.description}>
|
||||
{renderText(oldLang('RevenueSharingAdsInfo4Subtitle2', ''), ['simple_markdown'])}
|
||||
<SafeLink
|
||||
url={oldLang('PromoteUrl')}
|
||||
text={oldLang('LearnMoreArrow')}
|
||||
/>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
|
||||
return {
|
||||
header,
|
||||
listItemData,
|
||||
footer,
|
||||
};
|
||||
}, [isOpen, oldLang, minLevelToRestrictAds]);
|
||||
|
||||
if (isMonetizationSharing && modalData) {
|
||||
return (
|
||||
<TableAboutModal
|
||||
isOpen={isOpen}
|
||||
listItemData={modalData.listItemData}
|
||||
headerIconName="channel"
|
||||
header={modalData.header}
|
||||
footer={modalData.footer}
|
||||
buttonText={oldLang('RevenueSharingAdsUnderstood')}
|
||||
onClose={onClose}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
className={styles.root}
|
||||
contentClassName={styles.content}
|
||||
onClose={onClose}
|
||||
>
|
||||
{regularAdContent}
|
||||
<Button
|
||||
size="smaller"
|
||||
onClick={onClose}
|
||||
>
|
||||
{oldLang('RevenueSharingAdsUnderstood')}
|
||||
</Button>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(AboutAdsModal);
|
||||
@ -7,7 +7,7 @@ import styles from './BadgeButton.module.scss';
|
||||
type OwnProps = {
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
onClick?: NoneToVoidFunction;
|
||||
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
|
||||
};
|
||||
|
||||
const BadgeButton = ({
|
||||
|
||||
@ -227,10 +227,11 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
||||
}, [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<OwnProps & StateProps> = ({
|
||||
/>
|
||||
) : hasMessages ? (
|
||||
<MessageListContent
|
||||
areAdsEnabled={areAdsEnabled}
|
||||
canShowAds={areAdsEnabled && isChannelChat}
|
||||
chatId={chatId}
|
||||
isComments={isComments}
|
||||
isChannelChat={isChannelChat}
|
||||
|
||||
@ -36,7 +36,7 @@ import SponsoredMessage from './message/SponsoredMessage';
|
||||
import MessageListBotInfo from './MessageListBotInfo';
|
||||
|
||||
interface OwnProps {
|
||||
areAdsEnabled?: boolean;
|
||||
canShowAds?: boolean;
|
||||
chatId: string;
|
||||
threadId: ThreadId;
|
||||
messageIds: number[];
|
||||
@ -68,7 +68,7 @@ interface OwnProps {
|
||||
const UNREAD_DIVIDER_CLASS = 'unread-divider';
|
||||
|
||||
const MessageListContent: FC<OwnProps> = ({
|
||||
areAdsEnabled,
|
||||
canShowAds,
|
||||
chatId,
|
||||
threadId,
|
||||
messageIds,
|
||||
@ -308,7 +308,7 @@ const MessageListContent: FC<OwnProps> = ({
|
||||
key="fab-trigger"
|
||||
className="fab-trigger"
|
||||
/>
|
||||
{areAdsEnabled && isViewportNewest && (
|
||||
{canShowAds && isViewportNewest && (
|
||||
<SponsoredMessage
|
||||
key={chatId}
|
||||
chatId={chatId}
|
||||
|
||||
@ -22,6 +22,7 @@ import { applyAnimationState, type PaneState } from './hooks/useHeaderPane';
|
||||
|
||||
import GroupCallTopPane from '../calls/group/GroupCallTopPane';
|
||||
import AudioPlayer from './panes/AudioPlayer';
|
||||
import BotAdPane from './panes/BotAdPane';
|
||||
import ChatReportPane from './panes/ChatReportPane';
|
||||
import HeaderPinnedMessage from './panes/HeaderPinnedMessage';
|
||||
|
||||
@ -64,6 +65,7 @@ const MiddleHeaderPanes = ({
|
||||
const [getPinnedState, setPinnedState] = useSignal<PaneState>(FALLBACK_PANE_STATE);
|
||||
const [getGroupCallState, setGroupCallState] = useSignal<PaneState>(FALLBACK_PANE_STATE);
|
||||
const [getChatReportState, setChatReportState] = useSignal<PaneState>(FALLBACK_PANE_STATE);
|
||||
const [getBotAdState, setBotAdState] = useSignal<PaneState>(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}
|
||||
/>
|
||||
<BotAdPane
|
||||
chatId={chatId}
|
||||
messageListType={messageListType}
|
||||
onPaneStateChange={setBotAdState}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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<RefType extends HTMLElement = HTMLDivElement>({
|
||||
ref: providedRef,
|
||||
isOpen,
|
||||
isDisabled,
|
||||
withResizeObserver,
|
||||
onStateChange,
|
||||
} : {
|
||||
ref?: RefObject<RefType | null>;
|
||||
isOpen?: boolean;
|
||||
isDisabled?: boolean;
|
||||
withResizeObserver?: boolean;
|
||||
onStateChange?: (state: PaneState) => void;
|
||||
}) {
|
||||
const [shouldRender, setShouldRender] = useState(true);
|
||||
@ -38,6 +44,8 @@ export default function useHeaderPane<RefType extends HTMLElement = HTMLDivEleme
|
||||
const localRef = useRef<RefType>(null);
|
||||
const ref = providedRef || localRef;
|
||||
|
||||
const lastHeightRef = useRef(0);
|
||||
|
||||
const reset = useLastCallback(() => {
|
||||
setShouldRender(true);
|
||||
onStateChange?.({
|
||||
@ -65,7 +73,8 @@ export default function useHeaderPane<RefType extends HTMLElement = HTMLDivEleme
|
||||
setShouldRender(false);
|
||||
}, !isOpen ? CLOSE_DURATION : undefined);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
// 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<RefType extends HTMLElement = HTMLDivEleme
|
||||
|
||||
requestForcedReflow(() => {
|
||||
const currentHeight = element.offsetHeight;
|
||||
lastHeightRef.current = currentHeight;
|
||||
return () => {
|
||||
onStateChange?.({
|
||||
element,
|
||||
@ -90,6 +100,22 @@ export default function useHeaderPane<RefType extends HTMLElement = HTMLDivEleme
|
||||
});
|
||||
}, [isOpen, shouldRender, isDisabled, ref, onStateChange]);
|
||||
|
||||
const handleResize = useThrottledCallback(() => {
|
||||
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,
|
||||
|
||||
@ -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<OwnProps> = ({
|
||||
onSendPaidReaction,
|
||||
onShowPaidReactionModal,
|
||||
onCopyMessages,
|
||||
onAboutAdsClick,
|
||||
onSponsoredHide,
|
||||
onSponsorInfo,
|
||||
onSponsoredReport,
|
||||
onReactionPickerOpen,
|
||||
onTranslate,
|
||||
onShowOriginal,
|
||||
@ -234,10 +225,8 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
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<OwnProps> = ({
|
||||
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<OwnProps> = ({
|
||||
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<OwnProps> = ({
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{isSponsoredMessage && message.sponsorInfo && (
|
||||
<MenuItem icon="channel" onClick={onSponsorInfo}>{lang('SponsoredMessageSponsor')}</MenuItem>
|
||||
)}
|
||||
{isSponsoredMessage && (
|
||||
<MenuItem icon="info" onClick={onAboutAdsClick}>
|
||||
{lang(message.canReport ? 'AboutRevenueSharingAds' : 'SponsoredMessageInfo')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{isSponsoredMessage && message.canReport && (
|
||||
<MenuItem icon="hand-stop" onClick={onSponsoredReport}>
|
||||
{lang('ReportAd')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{isSponsoredMessage && onSponsoredHide && (
|
||||
<>
|
||||
<MenuSeparator />
|
||||
<MenuItem icon="close-circle" onClick={onSponsoredHide}>{lang('HideAd')}</MenuItem>
|
||||
</>
|
||||
)}
|
||||
{(canShowSeenBy || canShowReactionsCount) && !isSponsoredMessage && (
|
||||
{(canShowSeenBy || canShowReactionsCount) && (
|
||||
<>
|
||||
<MenuSeparator size={hasCustomEmoji ? 'thin' : 'thick'} />
|
||||
<MenuItem
|
||||
@ -518,10 +484,10 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
</MenuItem>
|
||||
</>
|
||||
)}
|
||||
{((!isSponsoredMessage && (canLoadReadDate || shouldRenderShowWhen)) || isEdited) && (
|
||||
{(canLoadReadDate || shouldRenderShowWhen || isEdited) && (
|
||||
<MenuSeparator size={hasCustomEmoji ? 'thin' : 'thick'} />
|
||||
)}
|
||||
{!isSponsoredMessage && (canLoadReadDate || shouldRenderShowWhen) && (
|
||||
{(canLoadReadDate || shouldRenderShowWhen) && (
|
||||
<ReadTimeMenuItem
|
||||
canLoadReadDate={canLoadReadDate}
|
||||
shouldRenderShowWhen={shouldRenderShowWhen}
|
||||
|
||||
@ -30,6 +30,9 @@
|
||||
}
|
||||
|
||||
.message-type {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
padding-inline-end: 0.25rem;
|
||||
|
||||
text-transform: capitalize;
|
||||
@ -121,17 +124,6 @@
|
||||
|
||||
.ad-about {
|
||||
font-size: 0.6875rem;
|
||||
margin-inline-start: 0.25rem;
|
||||
border-radius: 1rem;
|
||||
padding-inline: 0.375rem;
|
||||
transition: 150ms filter ease-in;
|
||||
background: var(--accent-background-active-color);
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
filter: brightness(1);
|
||||
|
||||
&:hover {
|
||||
filter: brightness(1.1);
|
||||
}
|
||||
}
|
||||
|
||||
.has-media {
|
||||
|
||||
@ -27,13 +27,12 @@ import { calculateMediaDimensions, getMinMediaWidth, MIN_MEDIA_WIDTH_WITH_TEXT }
|
||||
|
||||
import useAppLayout from '../../../hooks/useAppLayout';
|
||||
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import { type ObserveFn, 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 BadgeButton from '../../common/BadgeButton';
|
||||
import Icon from '../../common/icons/Icon';
|
||||
import PeerColorWrapper from '../../common/PeerColorWrapper';
|
||||
import Button from '../../ui/Button';
|
||||
@ -77,8 +76,8 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
|
||||
openUrl,
|
||||
hideSponsoredMessages,
|
||||
clickSponsoredMessage,
|
||||
reportSponsoredMessage,
|
||||
openMediaViewer,
|
||||
openAboutAdsModal,
|
||||
} = getActions();
|
||||
|
||||
const lang = useOldLang();
|
||||
@ -101,7 +100,6 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
|
||||
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<OwnProps & StateProps> = ({
|
||||
handleBeforeContextMenu(e);
|
||||
};
|
||||
|
||||
const handleReportSponsoredMessage = useLastCallback(() => {
|
||||
reportSponsoredMessage({ peerId: chatId, randomId: message!.randomId });
|
||||
});
|
||||
|
||||
const handleHideSponsoredMessage = useLastCallback(() => {
|
||||
hideSponsoredMessages();
|
||||
});
|
||||
@ -135,7 +129,7 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
|
||||
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<OwnProps & StateProps> = ({
|
||||
});
|
||||
});
|
||||
|
||||
const handleOpenAboutAdsModal = useLastCallback(() => {
|
||||
openAboutAdsModal({ chatId });
|
||||
});
|
||||
|
||||
const extraPadding = 0;
|
||||
|
||||
const sizeCalculations = useMemo(() => {
|
||||
@ -283,7 +281,9 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
|
||||
)}
|
||||
<span className={buildClassName('message-title message-type', hasMedia && 'has-media')}>
|
||||
{message!.isRecommended ? lang('Message.RecommendedLabel') : lang('SponsoredMessage')}
|
||||
<span onClick={openAboutAdsModal} className="ad-about">{lang('SponsoredMessageAdWhatIsThis')}</span>
|
||||
<BadgeButton onClick={handleOpenAboutAdsModal} className="ad-about">
|
||||
{lang('SponsoredMessageAdWhatIsThis')}
|
||||
</BadgeButton>
|
||||
</span>
|
||||
{renderContent()}
|
||||
</PeerColorWrapper>
|
||||
@ -318,18 +318,12 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
|
||||
<SponsoredMessageContextMenuContainer
|
||||
isOpen={isContextMenuOpen}
|
||||
anchor={contextMenuAnchor}
|
||||
triggerRef={ref}
|
||||
message={message!}
|
||||
onAboutAdsClick={openAboutAdsModal}
|
||||
onReportAd={handleReportSponsoredMessage}
|
||||
onClose={handleContextMenuClose}
|
||||
onCloseAnimationEnd={handleContextMenuHide}
|
||||
/>
|
||||
)}
|
||||
<AboutAdsModal
|
||||
isOpen={isAboutAdsModalOpen}
|
||||
isMonetizationSharing={message.canReport}
|
||||
onClose={closeAboutAdsModal}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -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<HTMLElement>;
|
||||
shouldSkipAbout?: boolean;
|
||||
onClose: NoneToVoidFunction;
|
||||
onCloseAnimationEnd?: NoneToVoidFunction;
|
||||
onAboutAdsClick: NoneToVoidFunction;
|
||||
onSponsoredHide: NoneToVoidFunction;
|
||||
onSponsorInfo?: NoneToVoidFunction;
|
||||
onSponsoredReport?: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
const SponsoredMessageContextMenu: FC<OwnProps> = ({
|
||||
isOpen,
|
||||
message,
|
||||
anchor,
|
||||
triggerRef,
|
||||
shouldSkipAbout,
|
||||
onClose,
|
||||
onCloseAnimationEnd,
|
||||
onAboutAdsClick,
|
||||
onSponsoredHide,
|
||||
onSponsorInfo,
|
||||
onSponsoredReport,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const menuRef = useRef<HTMLDivElement>(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 (
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
isOpen={isOpen}
|
||||
anchor={anchor}
|
||||
withPortal
|
||||
className="with-menu-transitions"
|
||||
getLayout={getLayout}
|
||||
getTriggerElement={getTriggerElement}
|
||||
getMenuElement={getMenuElement}
|
||||
getRootElement={getRootElement}
|
||||
onClose={onClose}
|
||||
onCloseAnimationEnd={onCloseAnimationEnd}
|
||||
>
|
||||
{message.sponsorInfo && onSponsorInfo && (
|
||||
<MenuItem icon="channel" onClick={onSponsorInfo}>{lang('SponsoredMessageSponsor')}</MenuItem>
|
||||
)}
|
||||
{!shouldSkipAbout && (
|
||||
<MenuItem icon="info" onClick={onAboutAdsClick}>
|
||||
{lang(message.canReport ? 'AboutRevenueSharingAds' : 'SponsoredMessageInfo')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{message.canReport && onSponsoredReport && (
|
||||
<MenuItem icon="hand-stop" onClick={onSponsoredReport}>
|
||||
{lang('ReportAd')}
|
||||
</MenuItem>
|
||||
)}
|
||||
{isSeparatorNeeded && <MenuSeparator />}
|
||||
<MenuItem icon="close-circle" onClick={onSponsoredHide}>
|
||||
{lang('HideAd')}
|
||||
</MenuItem>
|
||||
</Menu>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(SponsoredMessageContextMenu);
|
||||
@ -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<HTMLElement>;
|
||||
shouldSkipAbout?: boolean;
|
||||
onItemClick?: NoneToVoidFunction;
|
||||
onClose: NoneToVoidFunction;
|
||||
onCloseAnimationEnd: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
const SponsoredMessageContextMenuContainer: FC<OwnProps> = ({
|
||||
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<OwnProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={buildClassName('ContextMenuContainer', transitionClassNames)}>
|
||||
<MessageContextMenu
|
||||
isOpen={isMenuOpen}
|
||||
<div ref={ref} className="ContextMenuContainer">
|
||||
<SponsoredMessageContextMenu
|
||||
isOpen={isOpen}
|
||||
anchor={anchor}
|
||||
triggerRef={triggerRef}
|
||||
message={message}
|
||||
onClose={closeMenu}
|
||||
onCloseAnimationEnd={closeMenu}
|
||||
shouldSkipAbout={shouldSkipAbout}
|
||||
onClose={onClose}
|
||||
onCloseAnimationEnd={onClose}
|
||||
onAboutAdsClick={handleAboutAdsOpen}
|
||||
onSponsoredHide={handleSponsoredHide}
|
||||
onSponsorInfo={handleSponsorInfo}
|
||||
|
||||
53
src/components/middle/panes/BotAdPane.module.scss
Normal file
53
src/components/middle/panes/BotAdPane.module.scss
Normal file
@ -0,0 +1,53 @@
|
||||
@use "../../../styles/mixins";
|
||||
|
||||
.root {
|
||||
@include mixins.header-pane;
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.5rem;
|
||||
height: auto;
|
||||
|
||||
line-height: 1.25;
|
||||
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
|
||||
// Slight variation from mixins.header-pane
|
||||
padding-left: max(1rem, env(safe-area-inset-left));
|
||||
transition: background-color 0.2s, transform var(--slide-transition);
|
||||
|
||||
&:hover {
|
||||
background-color: var(--color-background-selected);
|
||||
}
|
||||
}
|
||||
|
||||
.content {
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.25rem;
|
||||
color: var(--accent-color);
|
||||
}
|
||||
|
||||
.avatar {
|
||||
--radius: 0.25rem;
|
||||
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.title, .info {
|
||||
font-size: calc(var(--message-text-size, 1rem) - 0.125rem);
|
||||
font-weight: 500;
|
||||
line-height: 1.25rem;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.aboutAd {
|
||||
font-size: 0.6875rem;
|
||||
}
|
||||
153
src/components/middle/panes/BotAdPane.tsx
Normal file
153
src/components/middle/panes/BotAdPane.tsx
Normal file
@ -0,0 +1,153 @@
|
||||
import React, { memo, useEffect } from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiSponsoredMessage } from '../../../api/types';
|
||||
import type { MessageListType } from '../../../global/types';
|
||||
|
||||
import { selectBot, selectSponsoredMessage } from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { getApiPeerColorClass } from '../../common/helpers/peerColor';
|
||||
import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities';
|
||||
|
||||
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
||||
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useHeaderPane, { type PaneState } from '../hooks/useHeaderPane';
|
||||
|
||||
import Avatar from '../../common/Avatar';
|
||||
import BadgeButton from '../../common/BadgeButton';
|
||||
import SponsoredMessageContextMenuContainer from '../message/SponsoredMessageContextMenuContainer';
|
||||
|
||||
import styles from './BotAdPane.module.scss';
|
||||
|
||||
type OwnProps = {
|
||||
chatId: string;
|
||||
messageListType: MessageListType;
|
||||
onPaneStateChange?: (state: PaneState) => 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<HTMLDivElement>) => {
|
||||
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 (
|
||||
<>
|
||||
<div
|
||||
ref={ref}
|
||||
className={styles.root}
|
||||
onClick={handleClick}
|
||||
onMouseDown={handleBeforeContextMenu}
|
||||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
<div className={buildClassName(styles.content, peerColor && getApiPeerColorClass(peerColor))}>
|
||||
<span className={styles.info}>
|
||||
{lang('SponsoredMessageAd')}
|
||||
<BadgeButton onClick={handleAboutClick} className={styles.aboutAd}>
|
||||
{lang('SponsoredMessageAdWhatIsThis')}
|
||||
</BadgeButton>
|
||||
</span>
|
||||
<div className={styles.title}>{title}</div>
|
||||
{content.text && (
|
||||
<div className={styles.text}>
|
||||
{renderTextWithEntities({
|
||||
text: content.text.text,
|
||||
entities: content.text.entities,
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
{photo && (
|
||||
<Avatar
|
||||
size="large"
|
||||
photo={photo}
|
||||
className={styles.avatar}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{contextMenuAnchor && (
|
||||
<SponsoredMessageContextMenuContainer
|
||||
isOpen={isContextMenuOpen}
|
||||
anchor={contextMenuAnchor}
|
||||
triggerRef={ref}
|
||||
message={renderingSponsoredMessage}
|
||||
onClose={handleContextMenuClose}
|
||||
onCloseAnimationEnd={handleContextMenuHide}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { chatId }): StateProps => {
|
||||
const bot = selectBot(global, chatId);
|
||||
const sponsoredMessage = selectSponsoredMessage(global, chatId);
|
||||
return {
|
||||
isBot: Boolean(bot),
|
||||
sponsoredMessage,
|
||||
};
|
||||
},
|
||||
)(BotAdPane));
|
||||
@ -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<TabState,
|
||||
'giftModal' |
|
||||
'isGiftRecipientPickerOpen' |
|
||||
'isWebAppsCloseConfirmationModalOpen' |
|
||||
'giftInfoModal'
|
||||
'giftInfoModal' |
|
||||
'aboutAdsModal'
|
||||
>;
|
||||
|
||||
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<ModalRegistry>;
|
||||
|
||||
@ -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<OwnProps> = (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 ? <AboutAdsModal {...props} /> : undefined;
|
||||
@ -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;
|
||||
}
|
||||
185
src/components/modals/aboutAds/AboutAdsModal.tsx
Normal file
185
src/components/modals/aboutAds/AboutAdsModal.tsx
Normal file
@ -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<HTMLButtonElement>(null);
|
||||
|
||||
const isOpen = Boolean(message);
|
||||
const isMonetizationSharing = message?.canReport;
|
||||
|
||||
const renderingIsNewDesign = useCurrentOrPrev(isMonetizationSharing);
|
||||
|
||||
const oldLang = useOldLang();
|
||||
|
||||
const regularAdContent = useMemo(() => {
|
||||
return (
|
||||
<>
|
||||
<h3>{oldLang('SponsoredMessageInfoScreen.Title')}</h3>
|
||||
<p>{renderText(oldLang('SponsoredMessageInfoDescription1'), ['br'])}</p>
|
||||
<p>{renderText(oldLang('SponsoredMessageInfoDescription2'), ['br'])}</p>
|
||||
<p>{renderText(oldLang('SponsoredMessageInfoDescription3'), ['br'])}</p>
|
||||
<p>
|
||||
<SafeLink
|
||||
url={oldLang('SponsoredMessageAlertLearnMoreUrl')}
|
||||
text={oldLang('SponsoredMessageAlertLearnMoreUrl')}
|
||||
/>
|
||||
</p>
|
||||
<p>{renderText(oldLang('SponsoredMessageInfoDescription4'), ['br'])}</p>
|
||||
</>
|
||||
);
|
||||
}, [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 = (
|
||||
<>
|
||||
<h3 className={styles.title}>{oldLang('AboutRevenueSharingAds')}</h3>
|
||||
<p className={buildClassName(styles.description, styles.secondary)}>
|
||||
{oldLang('RevenueSharingAdsAlertSubtitle')}
|
||||
</p>
|
||||
<Button
|
||||
ref={moreMenuRef}
|
||||
round
|
||||
size="smaller"
|
||||
color="translucent"
|
||||
className={styles.moreButton}
|
||||
onClick={handleContextMenu}
|
||||
>
|
||||
<Icon name="more" />
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
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 = (
|
||||
<>
|
||||
<h3 className={styles.title}>{renderText(oldLang('RevenueSharingAdsInfo4Title'), ['simple_markdown'])}</h3>
|
||||
<p className={styles.description}>
|
||||
{renderText(oldLang('RevenueSharingAdsInfo4Subtitle2', ''), ['simple_markdown'])}
|
||||
<SafeLink
|
||||
url={oldLang('PromoteUrl')}
|
||||
text={oldLang('LearnMoreArrow')}
|
||||
/>
|
||||
</p>
|
||||
</>
|
||||
);
|
||||
|
||||
return {
|
||||
header,
|
||||
listItemData,
|
||||
footer,
|
||||
};
|
||||
}, [isOpen, oldLang, handleContextMenu, minLevelToRestrictAds]);
|
||||
|
||||
if (renderingIsNewDesign) {
|
||||
return (
|
||||
<>
|
||||
<TableAboutModal
|
||||
isOpen={isOpen}
|
||||
listItemData={modalData?.listItemData}
|
||||
headerIconName="channel"
|
||||
header={modalData?.header}
|
||||
footer={modalData?.footer}
|
||||
buttonText={oldLang('RevenueSharingAdsUnderstood')}
|
||||
onClose={handleClose}
|
||||
/>
|
||||
{contextMenuAnchor && message && (
|
||||
<SponsoredMessageContextMenuContainer
|
||||
isOpen={isContextMenuOpen}
|
||||
anchor={contextMenuAnchor}
|
||||
triggerRef={moreMenuRef}
|
||||
message={message}
|
||||
shouldSkipAbout
|
||||
onItemClick={handleClose}
|
||||
onClose={handleContextMenuClose}
|
||||
onCloseAnimationEnd={handleContextMenuHide}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
className={styles.root}
|
||||
contentClassName={styles.content}
|
||||
onClose={handleClose}
|
||||
>
|
||||
{regularAdContent}
|
||||
<Button
|
||||
size="smaller"
|
||||
onClick={handleClose}
|
||||
>
|
||||
{oldLang('RevenueSharingAdsUnderstood')}
|
||||
</Button>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { modal }): StateProps => {
|
||||
const message = modal?.chatId ? selectSponsoredMessage(global, modal.chatId) : undefined;
|
||||
const minLevelToRestrictAds = global.appConfig?.channelRestrictAdsLevelMin;
|
||||
|
||||
return {
|
||||
message,
|
||||
minLevelToRestrictAds,
|
||||
};
|
||||
},
|
||||
)(AboutAdsModal));
|
||||
@ -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
|
||||
}
|
||||
|
||||
@ -59,7 +59,7 @@ const TableAboutModal = ({
|
||||
<Separator className={styles.separator} />
|
||||
{footer}
|
||||
{buttonText && (
|
||||
<Button onClick={onButtonClick || onClose}>{buttonText}</Button>
|
||||
<Button size="smaller" onClick={onButtonClick || onClose}>{buttonText}</Button>
|
||||
)}
|
||||
</Modal>
|
||||
);
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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],
|
||||
<SafeLink text={lang('lng_report_sponsored_reported_link')} url={lang('ReportAd.Help_URL')} />,
|
||||
<SafeLink
|
||||
withNormalWordBreak
|
||||
text={lang('lng_report_sponsored_reported_link')}
|
||||
url={lang('ReportAd.Help_URL')}
|
||||
/>,
|
||||
parts[1],
|
||||
];
|
||||
}, [lang, modal]);
|
||||
@ -123,6 +127,7 @@ const ReportAdModal = ({
|
||||
<Modal
|
||||
isOpen={isOpen}
|
||||
hasCloseButton
|
||||
className={styles.root}
|
||||
header={header}
|
||||
onClose={closeReportAdModal}
|
||||
>
|
||||
|
||||
@ -453,7 +453,7 @@
|
||||
|
||||
.subtitle {
|
||||
font-size: 0.875rem;
|
||||
line-height: 1.5rem;
|
||||
line-height: 1.25rem;
|
||||
color: var(--color-text-secondary);
|
||||
|
||||
& + .subtitle {
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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);
|
||||
});
|
||||
|
||||
@ -1251,8 +1251,7 @@ export function selectCanForwardMessages<T extends GlobalState>(global: T, chatI
|
||||
}
|
||||
|
||||
export function selectSponsoredMessage<T extends GlobalState>(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;
|
||||
}
|
||||
|
||||
@ -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;
|
||||
|
||||
3
src/types/language.d.ts
vendored
3
src/types/language.d.ts
vendored
@ -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;
|
||||
|
||||
@ -116,10 +116,10 @@ type AdvancedLangFnParameters =
|
||||
export type LangFnParameters = RegularLangFnParameters | AdvancedLangFnParameters;
|
||||
|
||||
export type LangFn = {
|
||||
<K = RegularLangKey>(
|
||||
<K extends RegularLangKey = RegularLangKey>(
|
||||
key: K, variables?: undefined, options?: LangFnOptions,
|
||||
): string;
|
||||
<K = PluralLangKey>(
|
||||
<K extends PluralLangKey = PluralLangKey>(
|
||||
key: K, variables: undefined, options: LangFnOptionsWithPlural,
|
||||
): string;
|
||||
<K extends RegularLangKeyWithVariables = RegularLangKeyWithVariables, V = LangPairWithVariables[K]>(
|
||||
@ -129,10 +129,10 @@ export type LangFn = {
|
||||
key: K, variables: V, options: LangFnOptionsWithPlural,
|
||||
): string;
|
||||
|
||||
<K = RegularLangKey>(
|
||||
<K extends RegularLangKey = RegularLangKey>(
|
||||
key: K, variables?: undefined, options?: AdvancedLangFnOptions,
|
||||
): TeactNode;
|
||||
<K = PluralLangKey>(
|
||||
<K extends PluralLangKey = PluralLangKey>(
|
||||
key: K, variables: undefined, options: AdvancedLangFnOptionsWithPlural,
|
||||
): TeactNode;
|
||||
<K extends RegularLangKeyWithVariables = RegularLangKeyWithVariables, V = LangPairWithVariables[K]>(
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user