Sponsored Message: Implement media in ads (#4925)

This commit is contained in:
Alexander Zinchuk 2024-09-19 20:43:14 +02:00
parent 7d73682aa7
commit 505f67674a
22 changed files with 307 additions and 50 deletions

View File

@ -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),
},
};
}

View File

@ -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,

View File

@ -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;
};

View File

@ -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;

View File

@ -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,

View File

@ -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(() => {

View File

@ -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,

View File

@ -96,4 +96,8 @@
text-decoration: underline;
}
}
.media-viewer-button {
border-radius: 0.5rem;
}
}

View File

@ -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>
);
};

View File

@ -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>

View File

@ -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;
}

View File

@ -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:

View File

@ -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

View File

@ -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;
}
}

View File

@ -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));

View File

@ -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,

View File

@ -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;
}

View File

@ -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));

View File

@ -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;

View File

@ -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;

View File

@ -330,6 +330,7 @@ export enum MediaViewerOrigin {
SuggestedAvatar,
StarsTransaction,
PreviewMedia,
SponsoredMessage,
}
export enum StoryViewerOrigin {

View File

@ -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 {