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