Sponsored Message: Implement media in ads (#4925)
This commit is contained in:
parent
7d73682aa7
commit
505f67674a
@ -79,7 +79,9 @@ export function setMessageBuilderCurrentUserId(_currentUserId: string) {
|
||||
currentUserId = _currentUserId;
|
||||
}
|
||||
|
||||
export function buildApiSponsoredMessage(mtpMessage: GramJs.SponsoredMessage): ApiSponsoredMessage | undefined {
|
||||
export function buildApiSponsoredMessage(
|
||||
mtpMessage: GramJs.SponsoredMessage, chatId: string,
|
||||
): ApiSponsoredMessage | undefined {
|
||||
const {
|
||||
message, entities, randomId, recommended, sponsorInfo, additionalInfo, buttonText, canReport, title, url, color,
|
||||
} = mtpMessage;
|
||||
@ -90,9 +92,14 @@ export function buildApiSponsoredMessage(mtpMessage: GramJs.SponsoredMessage): A
|
||||
photo = buildApiPhoto(mtpMessage.photo);
|
||||
}
|
||||
|
||||
let media: MediaContent | undefined;
|
||||
if (mtpMessage.media) {
|
||||
media = buildMessageMediaContent(mtpMessage.media);
|
||||
}
|
||||
|
||||
return {
|
||||
chatId,
|
||||
randomId: serializeBytes(randomId),
|
||||
text: buildMessageTextContent(message, entities),
|
||||
expiresAt: Math.round(Date.now() / 1000) + SPONSORED_MESSAGE_CACHE_MS,
|
||||
isRecommended: recommended,
|
||||
sponsorInfo,
|
||||
@ -103,6 +110,10 @@ export function buildApiSponsoredMessage(mtpMessage: GramJs.SponsoredMessage): A
|
||||
url,
|
||||
peerColor: color && buildApiPeerColor(color),
|
||||
photo,
|
||||
content: {
|
||||
...media,
|
||||
text: buildMessageTextContent(message, entities),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -1661,7 +1661,7 @@ export async function fetchSponsoredMessages({ chat }: { chat: ApiChat }) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const messages = result.messages.map(buildApiSponsoredMessage).filter(Boolean);
|
||||
const messages = result.messages.map((message) => buildApiSponsoredMessage(message, chat.id)).filter(Boolean);
|
||||
|
||||
return {
|
||||
messages,
|
||||
|
||||
@ -763,9 +763,9 @@ export type ApiThreadInfo = ApiCommentsInfo | ApiMessageThreadInfo;
|
||||
export type ApiMessageOutgoingStatus = 'read' | 'succeeded' | 'pending' | 'failed';
|
||||
|
||||
export type ApiSponsoredMessage = {
|
||||
chatId: string;
|
||||
randomId: string;
|
||||
isRecommended?: true;
|
||||
text: ApiFormattedText;
|
||||
expiresAt: number;
|
||||
sponsorInfo?: string;
|
||||
additionalInfo?: string;
|
||||
@ -774,6 +774,7 @@ export type ApiSponsoredMessage = {
|
||||
title: string;
|
||||
url: string;
|
||||
photo?: ApiPhoto;
|
||||
content: MediaContent;
|
||||
peerColor?: ApiPeerColor;
|
||||
};
|
||||
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { ApiMessage } from '../../../api/types';
|
||||
import type { ApiMessage, ApiSponsoredMessage } from '../../../api/types';
|
||||
import type { LangFn } from '../../../hooks/useOldLang';
|
||||
import type { TextPart } from '../../../types';
|
||||
import { ApiMessageEntityTypes } from '../../../api/types';
|
||||
@ -28,7 +28,7 @@ export function renderMessageText({
|
||||
shouldRenderAsHtml,
|
||||
isForMediaViewer,
|
||||
} : {
|
||||
message: ApiMessage;
|
||||
message: ApiMessage | ApiSponsoredMessage;
|
||||
highlight?: string;
|
||||
emojiSize?: number;
|
||||
isSimple?: boolean;
|
||||
|
||||
@ -5,7 +5,7 @@ import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type {
|
||||
ApiChat,
|
||||
ApiMessage, ApiPeer, ApiPhoto,
|
||||
ApiMessage, ApiPeer, ApiPhoto, ApiSponsoredMessage,
|
||||
} from '../../api/types';
|
||||
import { type MediaViewerMedia, MediaViewerOrigin, type ThreadId } from '../../types';
|
||||
|
||||
@ -25,7 +25,7 @@ import {
|
||||
selectOutlyingListByMessageId,
|
||||
selectPeer,
|
||||
selectPerformanceSettingsValue,
|
||||
selectScheduledMessage,
|
||||
selectScheduledMessage, selectSponsoredMessage,
|
||||
selectTabState,
|
||||
} from '../../global/selectors';
|
||||
import { stopCurrentAudio } from '../../util/audioPlayer';
|
||||
@ -70,6 +70,7 @@ type StateProps = {
|
||||
avatar?: ApiPhoto;
|
||||
avatarOwner?: ApiPeer;
|
||||
chatMessages?: Record<number, ApiMessage>;
|
||||
sponsoredMessage?: ApiSponsoredMessage;
|
||||
standaloneMedia?: MediaViewerMedia[];
|
||||
mediaIndex?: number;
|
||||
isHidden?: boolean;
|
||||
@ -95,6 +96,7 @@ const MediaViewer = ({
|
||||
avatar,
|
||||
avatarOwner,
|
||||
chatMessages,
|
||||
sponsoredMessage,
|
||||
standaloneMedia,
|
||||
mediaIndex,
|
||||
withAnimation,
|
||||
@ -112,9 +114,11 @@ const MediaViewer = ({
|
||||
toggleChatInfo,
|
||||
searchChatMediaMessages,
|
||||
loadMoreProfilePhotos,
|
||||
clickSponsoredMessage,
|
||||
openUrl,
|
||||
} = getActions();
|
||||
|
||||
const isOpen = Boolean(avatarOwner || message || standaloneMedia);
|
||||
const isOpen = Boolean(avatarOwner || message || standaloneMedia || sponsoredMessage);
|
||||
const { isMobile } = useAppLayout();
|
||||
|
||||
/* Animation */
|
||||
@ -128,7 +132,7 @@ const MediaViewer = ({
|
||||
const [isReportModalOpen, openReportModal, closeReportModal] = useFlag();
|
||||
|
||||
const currentItem = getMediaViewerItem({
|
||||
message, avatarOwner, standaloneMedia, mediaIndex,
|
||||
message, avatarOwner, standaloneMedia, mediaIndex, sponsoredMessage,
|
||||
});
|
||||
const { media, isSingle } = getViewableMedia(currentItem) || {};
|
||||
|
||||
@ -242,6 +246,14 @@ const MediaViewer = ({
|
||||
}
|
||||
});
|
||||
|
||||
const onSponsoredButtonClick = useLastCallback(() => {
|
||||
if (!sponsoredMessage || !chatId) return;
|
||||
|
||||
clickSponsoredMessage({ chatId });
|
||||
openUrl({ url: sponsoredMessage!.url });
|
||||
closeMediaViewer();
|
||||
});
|
||||
|
||||
const handleForward = useLastCallback(() => {
|
||||
openForwardMenu({
|
||||
fromChatId: chatId!,
|
||||
@ -296,6 +308,16 @@ const MediaViewer = ({
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (from.type === 'sponsoredMessage') {
|
||||
const { message: fromSponsoredMessage, mediaIndex: fromSponsoredMessageIndex } = from;
|
||||
const nextIndex = fromSponsoredMessageIndex! + direction;
|
||||
if (nextIndex >= 0 && fromSponsoredMessage) {
|
||||
return { type: 'sponsoredMessage', message: fromSponsoredMessage, mediaIndex: nextIndex };
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { message: fromMessage, mediaIndex: fromMediaIndex } = from;
|
||||
|
||||
const paidMedia = getMessagePaidMedia(fromMessage);
|
||||
@ -434,6 +456,7 @@ const MediaViewer = ({
|
||||
selectItem={openMediaViewerItem}
|
||||
isHidden={isHidden}
|
||||
onFooterClick={handleFooterClick}
|
||||
onSponsoredButtonClick={onSponsoredButtonClick}
|
||||
/>
|
||||
</ShowTransition>
|
||||
);
|
||||
@ -452,6 +475,7 @@ export default memo(withGlobal(
|
||||
standaloneMedia,
|
||||
mediaIndex,
|
||||
isAvatarView,
|
||||
isSponsoredMessage,
|
||||
} = mediaViewer;
|
||||
const withAnimation = selectPerformanceSettingsValue(global, 'mediaViewerAnimations');
|
||||
|
||||
@ -492,6 +516,13 @@ export default memo(withGlobal(
|
||||
}
|
||||
}
|
||||
|
||||
let sponsoredMessage: ApiSponsoredMessage | undefined;
|
||||
if (isSponsoredMessage && chatId) {
|
||||
if (origin === MediaViewerOrigin.SponsoredMessage) {
|
||||
sponsoredMessage = selectSponsoredMessage(global, chatId);
|
||||
}
|
||||
}
|
||||
|
||||
let chatMessages: Record<number, ApiMessage> | undefined;
|
||||
|
||||
if (chatId) {
|
||||
@ -531,6 +562,7 @@ export default memo(withGlobal(
|
||||
origin,
|
||||
message,
|
||||
chatMessages,
|
||||
sponsoredMessage,
|
||||
collectedMessageIds,
|
||||
withAnimation,
|
||||
isHidden,
|
||||
|
||||
@ -180,7 +180,7 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return isVideo ? (
|
||||
return item?.type !== 'sponsoredMessage' && (isVideo ? (
|
||||
<Button
|
||||
round
|
||||
size="smaller"
|
||||
@ -205,7 +205,7 @@ const MediaViewerActions: FC<OwnProps & StateProps> = ({
|
||||
>
|
||||
<i className="icon icon-download" />
|
||||
</Button>
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
const openDeleteModalHandler = useLastCallback(() => {
|
||||
|
||||
@ -2,7 +2,7 @@ import React, { memo } from '../../lib/teact/teact';
|
||||
import { withGlobal } from '../../global';
|
||||
|
||||
import type {
|
||||
ApiDimensions, ApiMessage,
|
||||
ApiDimensions, ApiMessage, ApiSponsoredMessage,
|
||||
} from '../../api/types';
|
||||
import type { MediaViewerOrigin } from '../../types';
|
||||
import type { MediaViewerItem } from './helpers/getViewableMedia';
|
||||
@ -36,10 +36,11 @@ type OwnProps = {
|
||||
isMoving?: boolean;
|
||||
onClose: () => void;
|
||||
onFooterClick: () => void;
|
||||
onSponsoredButtonClick: () => void;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
textMessage?: ApiMessage;
|
||||
textMessage?: ApiMessage | ApiSponsoredMessage;
|
||||
origin?: MediaViewerOrigin;
|
||||
isProtected?: boolean;
|
||||
volume: number;
|
||||
@ -65,6 +66,7 @@ const MediaViewerContent = ({
|
||||
isMoving,
|
||||
onClose,
|
||||
onFooterClick,
|
||||
onSponsoredButtonClick,
|
||||
}: OwnProps & StateProps) => {
|
||||
const lang = useOldLang();
|
||||
|
||||
@ -139,7 +141,7 @@ const MediaViewerContent = ({
|
||||
const textParts = textMessage && (textMessage.content.action?.type === 'suggestProfilePhoto'
|
||||
? lang('Conversation.SuggestedPhotoTitle')
|
||||
: renderMessageText({ message: textMessage, forcePlayback: true, isForMediaViewer: true }));
|
||||
|
||||
const buttonText = textMessage && 'buttonText' in textMessage ? textMessage.buttonText : undefined;
|
||||
const hasFooter = Boolean(textParts);
|
||||
const posterSize = calculateMediaViewerDimensions(dimensions!, hasFooter, isVideo);
|
||||
const isForceMobileVersion = isMobile || shouldForceMobileVersion(posterSize);
|
||||
@ -185,10 +187,12 @@ const MediaViewerContent = ({
|
||||
{textParts && (
|
||||
<MediaViewerFooter
|
||||
text={textParts}
|
||||
buttonText={buttonText}
|
||||
onClick={onFooterClick}
|
||||
isProtected={isProtected}
|
||||
isForceMobileVersion={isForceMobileVersion}
|
||||
isForVideo={isVideo && !isGif}
|
||||
onButtonClick={onSponsoredButtonClick}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@ -204,12 +208,14 @@ export default memo(withGlobal<OwnProps>(
|
||||
isHidden,
|
||||
origin,
|
||||
} = selectTabState(global).mediaViewer;
|
||||
const textMessage = item.type === 'message' ? item.message : undefined;
|
||||
const message = item.type === 'message' ? item.message : undefined;
|
||||
const sponsoredMessage = item.type === 'sponsoredMessage' ? item.message : undefined;
|
||||
const textMessage = message || sponsoredMessage;
|
||||
|
||||
return {
|
||||
origin,
|
||||
textMessage,
|
||||
isProtected: textMessage && selectIsMessageProtected(global, textMessage),
|
||||
isProtected: message && selectIsMessageProtected(global, message),
|
||||
volume,
|
||||
isMuted,
|
||||
isHidden,
|
||||
|
||||
@ -96,4 +96,8 @@
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
.media-viewer-button {
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,20 +12,24 @@ import useAppLayout from '../../hooks/useAppLayout';
|
||||
import useDerivedState from '../../hooks/useDerivedState';
|
||||
import useControlsSignal from './hooks/useControlsSignal';
|
||||
|
||||
import Button from '../ui/Button';
|
||||
|
||||
import './MediaViewerFooter.scss';
|
||||
|
||||
const RESIZE_THROTTLE_MS = 500;
|
||||
|
||||
type OwnProps = {
|
||||
text: TextPart | TextPart[];
|
||||
buttonText?: string;
|
||||
onClick: () => void;
|
||||
onButtonClick: () => void;
|
||||
isForVideo: boolean;
|
||||
isForceMobileVersion?: boolean;
|
||||
isProtected?: boolean;
|
||||
};
|
||||
|
||||
const MediaViewerFooter: FC<OwnProps> = ({
|
||||
text = '', isForVideo, onClick, isProtected, isForceMobileVersion,
|
||||
text = '', buttonText, isForVideo, onClick, onButtonClick, isProtected, isForceMobileVersion,
|
||||
}) => {
|
||||
const [isMultiline, setIsMultiline] = useState(false);
|
||||
const { isMobile } = useAppLayout();
|
||||
@ -76,6 +80,17 @@ const MediaViewerFooter: FC<OwnProps> = ({
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{Boolean(buttonText) && (
|
||||
<Button
|
||||
className={buildClassName('media-viewer-footer-content', 'media-viewer-button')}
|
||||
size="default"
|
||||
color="translucent"
|
||||
isRectangular
|
||||
onClick={onButtonClick}
|
||||
>
|
||||
{buttonText}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -56,6 +56,7 @@ type OwnProps = {
|
||||
selectItem: (item: MediaViewerItem) => void;
|
||||
loadMoreItemsIfNeeded: (item: MediaViewerItem) => void;
|
||||
onFooterClick: () => void;
|
||||
onSponsoredButtonClick: () => void;
|
||||
onClose: () => void;
|
||||
};
|
||||
|
||||
@ -99,6 +100,7 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
selectItem,
|
||||
onClose,
|
||||
onFooterClick,
|
||||
onSponsoredButtonClick,
|
||||
}) => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@ -729,6 +731,7 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
item={prevItem}
|
||||
onClose={onClose}
|
||||
onFooterClick={onFooterClick}
|
||||
onSponsoredButtonClick={onSponsoredButtonClick}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
@ -748,6 +751,7 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
isMoving={isMoving}
|
||||
onClose={onClose}
|
||||
onFooterClick={onFooterClick}
|
||||
onSponsoredButtonClick={onSponsoredButtonClick}
|
||||
/>
|
||||
</div>
|
||||
<div className="MediaViewerSlide" ref={rightSlideRef}>
|
||||
@ -758,6 +762,7 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
item={nextItem}
|
||||
onClose={onClose}
|
||||
onFooterClick={onFooterClick}
|
||||
onSponsoredButtonClick={onSponsoredButtonClick}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { ApiMessage, ApiPeer } from '../../../api/types';
|
||||
import type { ApiMessage, ApiPeer, ApiSponsoredMessage } from '../../../api/types';
|
||||
import type { MediaViewerMedia } from '../../../types';
|
||||
|
||||
import { getMessageContent, isDocumentPhoto, isDocumentVideo } from '../../../global/helpers';
|
||||
@ -15,6 +15,10 @@ export type MediaViewerItem = {
|
||||
type: 'standalone';
|
||||
media: MediaViewerMedia[];
|
||||
mediaIndex: number;
|
||||
} | {
|
||||
type: 'sponsoredMessage';
|
||||
message: ApiSponsoredMessage;
|
||||
mediaIndex?: number;
|
||||
};
|
||||
|
||||
type ViewableMedia = {
|
||||
@ -23,11 +27,12 @@ type ViewableMedia = {
|
||||
};
|
||||
|
||||
export function getMediaViewerItem({
|
||||
message, avatarOwner, standaloneMedia, mediaIndex,
|
||||
message, avatarOwner, standaloneMedia, mediaIndex, sponsoredMessage,
|
||||
}: {
|
||||
message?: ApiMessage;
|
||||
avatarOwner?: ApiPeer;
|
||||
standaloneMedia?: MediaViewerMedia[];
|
||||
sponsoredMessage?: ApiSponsoredMessage;
|
||||
mediaIndex?: number;
|
||||
}): MediaViewerItem | undefined {
|
||||
if (avatarOwner) {
|
||||
@ -54,6 +59,14 @@ export function getMediaViewerItem({
|
||||
};
|
||||
}
|
||||
|
||||
if (sponsoredMessage) {
|
||||
return {
|
||||
type: 'sponsoredMessage',
|
||||
message: sponsoredMessage,
|
||||
mediaIndex,
|
||||
};
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
|
||||
@ -339,6 +339,11 @@ function getNodes(origin: MediaViewerOrigin, message?: ApiMessage, index?: numbe
|
||||
mediaSelector = index === 0 ? `.stars-transaction-media-${index} :is(img, video)` : undefined;
|
||||
break;
|
||||
|
||||
case MediaViewerOrigin.SponsoredMessage:
|
||||
containerSelector = '.Transition_slide-active > .MessageList .sponsored-media-preview';
|
||||
mediaSelector = `${MESSAGE_CONTENT_SELECTOR} .full-media,${MESSAGE_CONTENT_SELECTOR} .thumbnail:not(.blurred-bg)`;
|
||||
break;
|
||||
|
||||
case MediaViewerOrigin.ScheduledInline:
|
||||
case MediaViewerOrigin.Inline:
|
||||
default:
|
||||
|
||||
@ -298,7 +298,13 @@ const MessageListContent: FC<OwnProps> = ({
|
||||
{shouldRenderBotInfo && <MessageListBotInfo isInMessageList key={`bot_info_${chatId}`} chatId={chatId} />}
|
||||
{dateGroups.flat()}
|
||||
{areAdsEnabled && isViewportNewest && (
|
||||
<SponsoredMessage key={chatId} chatId={chatId} containerRef={containerRef} />
|
||||
<SponsoredMessage
|
||||
key={chatId}
|
||||
chatId={chatId}
|
||||
containerRef={containerRef}
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
/>
|
||||
)}
|
||||
{withHistoryTriggers && (
|
||||
<div
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
@use "message-content";
|
||||
|
||||
.SponsoredMessage {
|
||||
--border-top-left-radius: var(--border-radius-messages) !important;
|
||||
--border-bottom-left-radius: 0 !important;
|
||||
@ -44,6 +46,10 @@
|
||||
|
||||
.message-content {
|
||||
padding: 0.5rem;
|
||||
--border-top-left-radius: var(--border-radius-messages-small);
|
||||
--border-top-right-radius: var(--border-radius-messages-small);
|
||||
--border-bottom-right-radius: var(--border-radius-messages-small);
|
||||
--border-bottom-left-radius: var(--border-radius-messages-small);
|
||||
|
||||
@media (max-width: 600px) {
|
||||
max-width: min(29rem, calc(100vw - 4.5rem)) !important;
|
||||
@ -129,4 +135,8 @@
|
||||
filter: opacity(1);
|
||||
}
|
||||
}
|
||||
|
||||
.has-media {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,19 +1,34 @@
|
||||
import type { RefObject } from 'react';
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { memo, useEffect, useRef } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useEffect, useMemo, useRef,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiSponsoredMessage } from '../../../api/types';
|
||||
import type { ISettings } from '../../../types';
|
||||
import { MediaViewerOrigin } from '../../../types';
|
||||
|
||||
import { selectSponsoredMessage } from '../../../global/selectors';
|
||||
import {
|
||||
getIsDownloading,
|
||||
getMessageContent,
|
||||
getMessageDownloadableMedia,
|
||||
} from '../../../global/helpers';
|
||||
import {
|
||||
selectActiveDownloads, selectCanAutoLoadMedia, selectCanAutoPlayMedia,
|
||||
selectSponsoredMessage,
|
||||
selectTheme,
|
||||
} 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 { calculateMediaDimensions, getMinMediaWidth, MIN_MEDIA_WIDTH_WITH_TEXT } from './helpers/mediaDimensions';
|
||||
|
||||
import useAppLayout from '../../../hooks/useAppLayout';
|
||||
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
|
||||
import { type ObserveFn, useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
@ -23,17 +38,25 @@ import Icon from '../../common/icons/Icon';
|
||||
import PeerColorWrapper from '../../common/PeerColorWrapper';
|
||||
import Button from '../../ui/Button';
|
||||
import MessageAppendix from './MessageAppendix';
|
||||
import Photo from './Photo';
|
||||
import SponsoredMessageContextMenuContainer from './SponsoredMessageContextMenuContainer.async';
|
||||
import Video from './Video';
|
||||
|
||||
import './SponsoredMessage.scss';
|
||||
|
||||
type OwnProps = {
|
||||
chatId: string;
|
||||
containerRef: RefObject<HTMLDivElement>;
|
||||
observeIntersectionForLoading: ObserveFn;
|
||||
observeIntersectionForPlaying: ObserveFn;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
message?: ApiSponsoredMessage;
|
||||
theme: ISettings['theme'];
|
||||
isDownloading?: boolean;
|
||||
canAutoLoadMedia?: boolean;
|
||||
canAutoPlayMedia?: boolean;
|
||||
};
|
||||
|
||||
const INTERSECTION_DEBOUNCE_MS = 200;
|
||||
@ -42,6 +65,12 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
|
||||
chatId,
|
||||
message,
|
||||
containerRef,
|
||||
theme,
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
isDownloading,
|
||||
canAutoLoadMedia,
|
||||
canAutoPlayMedia,
|
||||
}) => {
|
||||
const {
|
||||
viewSponsoredMessage,
|
||||
@ -49,6 +78,7 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
|
||||
hideSponsoredMessages,
|
||||
clickSponsoredMessage,
|
||||
reportSponsoredMessage,
|
||||
openMediaViewer,
|
||||
} = getActions();
|
||||
|
||||
const lang = useOldLang();
|
||||
@ -57,6 +87,8 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const contentRef = useRef<HTMLDivElement>(null);
|
||||
const shouldObserve = Boolean(message);
|
||||
|
||||
const { isMobile } = useAppLayout();
|
||||
const {
|
||||
observe: observeIntersection,
|
||||
} = useIntersectionObserver({
|
||||
@ -99,7 +131,64 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
|
||||
openUrl({ url: message!.url, shouldSkipModal: true });
|
||||
});
|
||||
|
||||
if (!message) {
|
||||
const handleOpenMedia = useLastCallback(() => {
|
||||
openMediaViewer({
|
||||
origin: MediaViewerOrigin.SponsoredMessage,
|
||||
chatId,
|
||||
isSponsoredMessage: true,
|
||||
});
|
||||
});
|
||||
|
||||
const {
|
||||
photo, video,
|
||||
} = message ? getMessageContent(message) : { photo: undefined, video: undefined };
|
||||
|
||||
const hasMedia = Boolean(photo || video);
|
||||
|
||||
const extraPadding = 0;
|
||||
|
||||
const sizeCalculations = useMemo(() => {
|
||||
let calculatedWidth;
|
||||
let contentWidth: number | undefined;
|
||||
const noMediaCorners = false;
|
||||
let style = '';
|
||||
|
||||
if (photo || video) {
|
||||
let width: number | undefined;
|
||||
if (photo) {
|
||||
width = calculateMediaDimensions({
|
||||
media: photo,
|
||||
isMobile,
|
||||
}).width;
|
||||
} else if (video) {
|
||||
width = calculateMediaDimensions({
|
||||
media: video,
|
||||
isMobile,
|
||||
}).width;
|
||||
}
|
||||
|
||||
if (width) {
|
||||
if (width < MIN_MEDIA_WIDTH_WITH_TEXT) {
|
||||
contentWidth = width;
|
||||
}
|
||||
calculatedWidth = Math.max(getMinMediaWidth(), width);
|
||||
}
|
||||
}
|
||||
|
||||
if (calculatedWidth) {
|
||||
style = `width: ${calculatedWidth + extraPadding}px`;
|
||||
}
|
||||
|
||||
return {
|
||||
contentWidth, noMediaCorners, style,
|
||||
};
|
||||
}, [photo, video, isMobile]);
|
||||
|
||||
const {
|
||||
contentWidth, style,
|
||||
} = sizeCalculations;
|
||||
|
||||
if (!message || !message.content) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -108,14 +197,16 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
|
||||
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>
|
||||
{Boolean(message.content?.text) && (
|
||||
<div className="text-content with-meta" dir="auto" ref={contentRef}>
|
||||
<span className="text-content-inner" dir="auto">
|
||||
{renderTextWithEntities({
|
||||
text: message.content.text.text,
|
||||
entities: message.content.text.entities,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
className="SponsoredMessage__button"
|
||||
@ -130,19 +221,57 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
function renderMediaContent() {
|
||||
if (!message) return undefined;
|
||||
|
||||
if (photo) {
|
||||
return (
|
||||
<Photo
|
||||
photo={photo}
|
||||
theme={theme}
|
||||
canAutoLoad={canAutoLoadMedia}
|
||||
isDownloading={isDownloading}
|
||||
observeIntersection={observeIntersectionForLoading}
|
||||
noAvatars
|
||||
onClick={handleOpenMedia}
|
||||
forcedWidth={contentWidth}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (video) {
|
||||
return (
|
||||
<Video
|
||||
video={video}
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
noAvatars
|
||||
canAutoLoad={canAutoLoadMedia}
|
||||
canAutoPlay={canAutoPlayMedia}
|
||||
isDownloading={isDownloading}
|
||||
onClick={handleOpenMedia}
|
||||
forcedWidth={contentWidth}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
style={style}
|
||||
key="sponsored-message"
|
||||
className="SponsoredMessage Message open"
|
||||
className="SponsoredMessage Message open sponsored-media-preview"
|
||||
>
|
||||
<div
|
||||
className="message-content has-shadow has-solid-background has-appendix"
|
||||
className="message-content media has-shadow has-solid-background has-appendix"
|
||||
dir="auto"
|
||||
onMouseDown={handleMouseDown}
|
||||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
<PeerColorWrapper peerColor={message.peerColor} className="content-inner" dir="auto">
|
||||
{renderMediaContent()}
|
||||
{message.photo && (
|
||||
<Avatar
|
||||
size="large"
|
||||
@ -150,7 +279,7 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
|
||||
className={buildClassName('channel-avatar', lang.isRtl && 'is-rtl')}
|
||||
/>
|
||||
)}
|
||||
<span className="message-title message-type">
|
||||
<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>
|
||||
</span>
|
||||
@ -207,8 +336,16 @@ export default memo(withGlobal<OwnProps>(
|
||||
(global, { chatId }): StateProps => {
|
||||
const message = selectSponsoredMessage(global, chatId);
|
||||
|
||||
const activeDownloads = selectActiveDownloads(global);
|
||||
const downloadableMedia = message ? getMessageDownloadableMedia(message) : undefined;
|
||||
const isDownloading = downloadableMedia && getIsDownloading(activeDownloads, downloadableMedia);
|
||||
|
||||
return {
|
||||
message,
|
||||
theme: selectTheme(global),
|
||||
isDownloading,
|
||||
canAutoLoadMedia: message ? selectCanAutoLoadMedia(global, message) : undefined,
|
||||
canAutoPlayMedia: message ? selectCanAutoPlayMedia(global, message) : undefined,
|
||||
};
|
||||
},
|
||||
)(SponsoredMessage));
|
||||
|
||||
@ -8,7 +8,7 @@ import { selectTabState } from '../../selectors';
|
||||
|
||||
addActionHandler('openMediaViewer', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
chatId, threadId = MAIN_THREAD_ID, messageId, mediaIndex, isAvatarView, origin,
|
||||
chatId, threadId = MAIN_THREAD_ID, messageId, mediaIndex, isAvatarView, isSponsoredMessage, origin,
|
||||
withDynamicLoading, standaloneMedia, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
|
||||
@ -22,6 +22,7 @@ addActionHandler('openMediaViewer', (global, actions, payload): ActionReturnType
|
||||
messageId,
|
||||
mediaIndex: mediaIndex || 0,
|
||||
isAvatarView,
|
||||
isSponsoredMessage,
|
||||
origin,
|
||||
standaloneMedia,
|
||||
isHidden: false,
|
||||
|
||||
@ -2,7 +2,7 @@ import type {
|
||||
ApiAttachment,
|
||||
ApiMessage,
|
||||
ApiMessageEntityTextUrl,
|
||||
ApiPeer,
|
||||
ApiPeer, ApiSponsoredMessage,
|
||||
ApiStory,
|
||||
} from '../../api/types';
|
||||
import type { MediaContent } from '../../api/types/messages';
|
||||
@ -52,7 +52,7 @@ export function getMessageTranscription(message: ApiMessage) {
|
||||
return transcriptionId && global.transcriptions[transcriptionId]?.text;
|
||||
}
|
||||
|
||||
export function hasMessageText(message: ApiMessage | ApiStory) {
|
||||
export function hasMessageText(message: ApiMessage | ApiStory | ApiSponsoredMessage) {
|
||||
const {
|
||||
text, sticker, photo, video, audio, voice, document, poll, webPage, contact, invoice, location,
|
||||
game, action, storyData, giveaway, giveawayResults, isExpiredVoice, paidMedia,
|
||||
@ -64,7 +64,7 @@ export function hasMessageText(message: ApiMessage | ApiStory) {
|
||||
);
|
||||
}
|
||||
|
||||
export function getMessageText(message: ApiMessage | ApiStory) {
|
||||
export function getMessageText(message: ApiMessage | ApiStory | ApiSponsoredMessage) {
|
||||
return hasMessageText(message) ? message.content.text?.text || CONTENT_NOT_SUPPORTED : undefined;
|
||||
}
|
||||
|
||||
|
||||
@ -5,7 +5,7 @@ import type {
|
||||
ApiMessageEntityCustomEmoji,
|
||||
ApiMessageForwardInfo,
|
||||
ApiMessageOutgoingStatus,
|
||||
ApiPeer,
|
||||
ApiPeer, ApiSponsoredMessage,
|
||||
ApiStickerSetInfo,
|
||||
} from '../../api/types';
|
||||
import type { ThreadId } from '../../types';
|
||||
@ -1101,13 +1101,15 @@ function selectShouldDisplayReplyKeyboard<T extends GlobalState>(global: T, mess
|
||||
return true;
|
||||
}
|
||||
|
||||
export function selectCanAutoLoadMedia<T extends GlobalState>(global: T, message: ApiMessage) {
|
||||
export function selectCanAutoLoadMedia<T extends GlobalState>(
|
||||
global: T, message: ApiMessage | ApiSponsoredMessage,
|
||||
) {
|
||||
const chat = selectChat(global, message.chatId);
|
||||
if (!chat) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const sender = selectSender(global, message);
|
||||
const sender = 'id' in message ? selectSender(global, message) : undefined;
|
||||
|
||||
const isPhoto = Boolean(getMessagePhoto(message) || getMessageWebPagePhoto(message));
|
||||
const isVideo = Boolean(getMessageVideo(message) || getMessageWebPageVideo(message));
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
import type { ApiMessage } from '../../api/types';
|
||||
import type { ApiMessage, ApiSponsoredMessage } from '../../api/types';
|
||||
import type { PerformanceTypeKey } from '../../types';
|
||||
import type { GlobalState, TabArgs } from '../types';
|
||||
import { NewChatMembersProgress, RightColumnContent } from '../../types';
|
||||
@ -19,9 +19,10 @@ export function selectIsMediaViewerOpen<T extends GlobalState>(
|
||||
messageId,
|
||||
isAvatarView,
|
||||
standaloneMedia,
|
||||
isSponsoredMessage,
|
||||
},
|
||||
} = selectTabState(global, tabId);
|
||||
return Boolean(standaloneMedia || (chatId && (isAvatarView || messageId)));
|
||||
return Boolean(standaloneMedia || (chatId && (isAvatarView || messageId || isSponsoredMessage)));
|
||||
}
|
||||
|
||||
export function selectRightColumnContentKey<T extends GlobalState>(
|
||||
@ -111,7 +112,7 @@ export function selectPerformanceSettingsValue<T extends GlobalState>(
|
||||
return global.settings.performance[key];
|
||||
}
|
||||
|
||||
export function selectCanAutoPlayMedia<T extends GlobalState>(global: T, message: ApiMessage) {
|
||||
export function selectCanAutoPlayMedia<T extends GlobalState>(global: T, message: ApiMessage | ApiSponsoredMessage) {
|
||||
const video = getMessageVideo(message) || getMessageWebPageVideo(message);
|
||||
if (!video) {
|
||||
return undefined;
|
||||
|
||||
@ -469,6 +469,7 @@ export type TabState = {
|
||||
withDynamicLoading?: boolean;
|
||||
mediaIndex?: number;
|
||||
isAvatarView?: boolean;
|
||||
isSponsoredMessage?: boolean;
|
||||
standaloneMedia?: MediaViewerMedia[];
|
||||
origin?: MediaViewerOrigin;
|
||||
volume: number;
|
||||
@ -2570,6 +2571,7 @@ export interface ActionPayloads {
|
||||
standaloneMedia?: MediaViewerMedia[];
|
||||
mediaIndex?: number;
|
||||
isAvatarView?: boolean;
|
||||
isSponsoredMessage?: boolean;
|
||||
origin: MediaViewerOrigin;
|
||||
withDynamicLoading?: boolean;
|
||||
} & WithTabId;
|
||||
|
||||
@ -330,6 +330,7 @@ export enum MediaViewerOrigin {
|
||||
SuggestedAvatar,
|
||||
StarsTransaction,
|
||||
PreviewMedia,
|
||||
SponsoredMessage,
|
||||
}
|
||||
|
||||
export enum StoryViewerOrigin {
|
||||
|
||||
@ -1,11 +1,16 @@
|
||||
import type { ApiMessage } from '../../api/types';
|
||||
import type { ApiMessage, ApiSponsoredMessage } from '../../api/types';
|
||||
|
||||
export type MessageKey = `msg${string}-${number}`;
|
||||
|
||||
export function getMessageKey(message: ApiMessage): MessageKey {
|
||||
const { chatId, id, previousLocalId } = message;
|
||||
export function getMessageKey(message: ApiMessage | ApiSponsoredMessage): MessageKey {
|
||||
const {
|
||||
chatId,
|
||||
} = message;
|
||||
|
||||
return buildMessageKey(chatId, previousLocalId || id);
|
||||
if ('randomId' in message) {
|
||||
return buildMessageKey(chatId, Number(message.randomId));
|
||||
}
|
||||
return buildMessageKey(chatId, message.previousLocalId || message.id);
|
||||
}
|
||||
|
||||
export function getMessageServerKey(message: ApiMessage): MessageKey | undefined {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user