Sponsored Message: Update UI (#3989)
This commit is contained in:
parent
1ca211978c
commit
54d0ec5d7e
@ -19,6 +19,7 @@ import type {
|
||||
ApiReplyInfo,
|
||||
ApiReplyKeyboard,
|
||||
ApiSponsoredMessage,
|
||||
ApiSponsoredWebPage,
|
||||
ApiSticker,
|
||||
ApiStory,
|
||||
ApiStorySkipped,
|
||||
@ -77,7 +78,7 @@ export function setMessageBuilderCurrentUserId(_currentUserId: string) {
|
||||
export function buildApiSponsoredMessage(mtpMessage: GramJs.SponsoredMessage): ApiSponsoredMessage | undefined {
|
||||
const {
|
||||
fromId, message, entities, startParam, channelPost, chatInvite, chatInviteHash, randomId, recommended, sponsorInfo,
|
||||
additionalInfo,
|
||||
additionalInfo, showPeerPhoto, webpage,
|
||||
} = mtpMessage;
|
||||
const chatId = fromId ? getApiChatIdFromMtpPeer(fromId) : undefined;
|
||||
const chatInviteTitle = chatInvite
|
||||
@ -92,6 +93,8 @@ export function buildApiSponsoredMessage(mtpMessage: GramJs.SponsoredMessage): A
|
||||
text: buildMessageTextContent(message, entities),
|
||||
expiresAt: Math.round(Date.now() / 1000) + SPONSORED_MESSAGE_CACHE_MS,
|
||||
isRecommended: Boolean(recommended),
|
||||
...(webpage && { webPage: buildSponsoredWebPage(webpage) }),
|
||||
...(showPeerPhoto && { isAvatarShown: true }),
|
||||
...(chatId && { chatId }),
|
||||
...(chatInviteHash && { chatInviteHash }),
|
||||
...(chatInvite && { chatInviteTitle }),
|
||||
@ -994,3 +997,19 @@ function buildThreadInfo(
|
||||
...(recentRepliers && { recentReplierIds: recentRepliers.map(getApiChatIdFromMtpPeer) }),
|
||||
};
|
||||
}
|
||||
|
||||
function buildSponsoredWebPage(webPage: GramJs.TypeSponsoredWebPage): ApiSponsoredWebPage {
|
||||
let photo: ApiPhoto | undefined;
|
||||
if (webPage.photo instanceof GramJs.Photo) {
|
||||
addPhotoToLocalDb(webPage.photo);
|
||||
photo = buildApiPhoto(webPage.photo);
|
||||
}
|
||||
|
||||
return {
|
||||
...pick(webPage, [
|
||||
'url',
|
||||
'siteName',
|
||||
]),
|
||||
photo,
|
||||
};
|
||||
}
|
||||
|
||||
@ -310,6 +310,12 @@ export interface ApiWebPage {
|
||||
story?: ApiWebPageStoryData;
|
||||
}
|
||||
|
||||
export interface ApiSponsoredWebPage {
|
||||
url: string;
|
||||
siteName: string;
|
||||
photo?: ApiPhoto;
|
||||
}
|
||||
|
||||
export type ApiReplyInfo = ApiMessageReplyInfo | ApiStoryReplyInfo;
|
||||
|
||||
export interface ApiMessageReplyInfo {
|
||||
@ -571,12 +577,14 @@ export type ApiSponsoredMessage = {
|
||||
chatId?: string;
|
||||
randomId: string;
|
||||
isRecommended?: boolean;
|
||||
isAvatarShown?: boolean;
|
||||
isBot?: boolean;
|
||||
channelPostId?: number;
|
||||
startParam?: string;
|
||||
chatInviteHash?: string;
|
||||
chatInviteTitle?: string;
|
||||
text: ApiFormattedText;
|
||||
webPage?: ApiSponsoredWebPage;
|
||||
expiresAt: number;
|
||||
sponsorInfo?: string;
|
||||
additionalInfo?: string;
|
||||
|
||||
@ -157,6 +157,7 @@ import InlineButtons from './InlineButtons';
|
||||
import Invoice from './Invoice';
|
||||
import InvoiceMediaPreview from './InvoiceMediaPreview';
|
||||
import Location from './Location';
|
||||
import MessageAppendix from './MessageAppendix';
|
||||
import MessageMeta from './MessageMeta';
|
||||
import MessagePhoneCall from './MessagePhoneCall';
|
||||
import Photo from './Photo';
|
||||
@ -1423,30 +1424,6 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
function MessageAppendix({ isOwn } : { isOwn: boolean }) {
|
||||
const path = isOwn
|
||||
? 'M6 17H0V0c.193 2.84.876 5.767 2.05 8.782.904 2.325 2.446 4.485 4.625 6.48A1 1 0 016 17z'
|
||||
: 'M3 17h6V0c-.193 2.84-.876 5.767-2.05 8.782-.904 2.325-2.446 4.485-4.625 6.48A1 1 0 003 17z';
|
||||
return (
|
||||
<svg width="9" height="20" className="svg-appendix">
|
||||
<defs>
|
||||
<filter x="-50%" y="-14.7%" width="200%" height="141.2%" filterUnits="objectBoundingBox" id="messageAppendix">
|
||||
<feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1" />
|
||||
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1" />
|
||||
<feColorMatrix
|
||||
values="0 0 0 0 0.0621962482 0 0 0 0 0.138574144 0 0 0 0 0.185037364 0 0 0 0.15 0"
|
||||
in="shadowBlurOuter1"
|
||||
/>
|
||||
</filter>
|
||||
</defs>
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path d={path} fill="#000" filter="url(#messageAppendix)" />
|
||||
<path d={path} fill={isOwn ? '#EEFFDE' : 'FFF'} className="corner" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, ownProps): StateProps => {
|
||||
const {
|
||||
|
||||
31
src/components/middle/message/MessageAppendix.tsx
Normal file
31
src/components/middle/message/MessageAppendix.tsx
Normal file
@ -0,0 +1,31 @@
|
||||
import React from '../../../lib/teact/teact';
|
||||
|
||||
interface OwnProps {
|
||||
isOwn?: boolean;
|
||||
}
|
||||
|
||||
function MessageAppendix({ isOwn } : OwnProps) {
|
||||
const path = isOwn
|
||||
? 'M6 17H0V0c.193 2.84.876 5.767 2.05 8.782.904 2.325 2.446 4.485 4.625 6.48A1 1 0 016 17z'
|
||||
: 'M3 17h6V0c-.193 2.84-.876 5.767-2.05 8.782-.904 2.325-2.446 4.485-4.625 6.48A1 1 0 003 17z';
|
||||
return (
|
||||
<svg width="9" height="20" className="svg-appendix">
|
||||
<defs>
|
||||
<filter x="-50%" y="-14.7%" width="200%" height="141.2%" filterUnits="objectBoundingBox" id="messageAppendix">
|
||||
<feOffset dy="1" in="SourceAlpha" result="shadowOffsetOuter1" />
|
||||
<feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1" />
|
||||
<feColorMatrix
|
||||
values="0 0 0 0 0.0621962482 0 0 0 0 0.138574144 0 0 0 0 0.185037364 0 0 0 0.15 0"
|
||||
in="shadowBlurOuter1"
|
||||
/>
|
||||
</filter>
|
||||
</defs>
|
||||
<g fill="none" fill-rule="evenodd">
|
||||
<path d={path} fill="#000" filter="url(#messageAppendix)" />
|
||||
<path d={path} fill={isOwn ? '#EEFFDE' : 'FFF'} className="corner" />
|
||||
</g>
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
export default MessageAppendix;
|
||||
@ -9,10 +9,122 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
&__button.secondary {
|
||||
margin-top: 0.5rem;
|
||||
border: 1px solid var(--color-primary);
|
||||
border-radius: var(--border-radius-default-tiny);
|
||||
color: var(--color-primary);
|
||||
&__button {
|
||||
--riple-color: var(var(--accent-background-active-color));
|
||||
|
||||
margin-top: 0.375rem;
|
||||
margin-bottom: -0.375rem;
|
||||
border-top: 1px solid var(--accent-background-active-color, var(--active-color));
|
||||
|
||||
color: var(--accent-color) !important;
|
||||
|
||||
transition: opacity 0.2s ease-in;
|
||||
|
||||
&:hover, &:active {
|
||||
background-color: transparent !important;
|
||||
opacity: 0.85;
|
||||
}
|
||||
}
|
||||
|
||||
.message-type {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.message-peer {
|
||||
color: var(--color-text);
|
||||
}
|
||||
|
||||
&.with-avatar {
|
||||
--border-bottom-left-radius: 0 !important;
|
||||
|
||||
padding-left: 2.5rem !important;
|
||||
|
||||
& > .Avatar {
|
||||
display: flex !important;
|
||||
}
|
||||
|
||||
|
||||
@media (max-width: 600px) {
|
||||
padding-left: 2.875rem !important;
|
||||
|
||||
.message-content {
|
||||
max-width: min(29rem, calc(100vw - 7.0625rem)) !important;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.message-action-button {
|
||||
bottom: auto !important;
|
||||
top: 0.5rem;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
padding: 0.5rem;
|
||||
|
||||
@media (max-width: 600px) {
|
||||
max-width: min(29rem, calc(100vw - 4.5rem)) !important;
|
||||
}
|
||||
}
|
||||
|
||||
.channel-avatar {
|
||||
--radius: 0.125rem;
|
||||
|
||||
float: right;
|
||||
margin: 0 0 0.5rem 0.5rem;
|
||||
|
||||
&.is-rtl {
|
||||
float: left;
|
||||
margin: 0 0.5rem 0.5rem 0;
|
||||
}
|
||||
}
|
||||
|
||||
.content-inner {
|
||||
padding-top: 0.375rem;
|
||||
padding-inline-end: 0.375rem;
|
||||
padding-bottom: 0;
|
||||
padding-inline-start: 0.625rem;
|
||||
font-size: calc(var(--message-text-size, 1rem) - 0.125rem);
|
||||
background-color: var(--accent-background-color);
|
||||
border-radius: 0.375rem;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
inset-inline-start: 0;
|
||||
bottom: 0;
|
||||
width: 3px;
|
||||
background: var(--bar-gradient, var(--accent-color));
|
||||
}
|
||||
|
||||
> .Button {
|
||||
border-radius: 0 0 0.375rem 0.375rem;
|
||||
border: none;
|
||||
background: none;
|
||||
margin-bottom: 0;
|
||||
line-height: 1;
|
||||
|
||||
&::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 0.0625rem;
|
||||
background: var(--accent-color);
|
||||
opacity: 0.25;
|
||||
}
|
||||
}
|
||||
|
||||
.icon {
|
||||
position: absolute;
|
||||
font-size: 0.75rem;
|
||||
top: 0.25rem;
|
||||
right: 0;
|
||||
transform: rotate(-45deg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,19 +1,22 @@
|
||||
import type { RefObject } from 'react';
|
||||
import type { MouseEvent as ReactMouseEvent, RefObject } from 'react';
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useEffect, useRef,
|
||||
} from '../../../lib/teact/teact';
|
||||
import React, { memo, useEffect, useRef } from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiChat, ApiSponsoredMessage, ApiUser } from '../../../api/types';
|
||||
import type {
|
||||
ApiChat, ApiSponsoredMessage, ApiUser,
|
||||
} from '../../../api/types';
|
||||
|
||||
import { getChatTitle, getUserFullName } from '../../../global/helpers';
|
||||
import { selectChat, selectSponsoredMessage, selectUser } from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { IS_ANDROID, IS_TOUCH_ENV } from '../../../util/windowEnvironment';
|
||||
import { getPeerColorClass } from '../../common/helpers/peerColor';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
import { renderTextWithEntities } from '../../common/helpers/renderTextWithEntities';
|
||||
import { preventMessageInputBlur } from '../helpers/preventMessageInputBlur';
|
||||
|
||||
import useAppLayout from '../../../hooks/useAppLayout';
|
||||
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
|
||||
@ -21,7 +24,9 @@ import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
|
||||
import AboutAdsModal from '../../common/AboutAdsModal.async';
|
||||
import Avatar from '../../common/Avatar';
|
||||
import Button from '../../ui/Button';
|
||||
import MessageAppendix from './MessageAppendix';
|
||||
import SponsoredMessageContextMenuContainer from './SponsoredMessageContextMenuContainer.async';
|
||||
|
||||
import './SponsoredMessage.scss';
|
||||
@ -33,6 +38,7 @@ type OwnProps = {
|
||||
|
||||
type StateProps = {
|
||||
message?: ApiSponsoredMessage;
|
||||
peer?: ApiChat;
|
||||
bot?: ApiUser;
|
||||
channel?: ApiChat;
|
||||
};
|
||||
@ -41,6 +47,7 @@ const INTERSECTION_DEBOUNCE_MS = 200;
|
||||
|
||||
const SponsoredMessage: FC<OwnProps & StateProps> = ({
|
||||
chatId,
|
||||
peer,
|
||||
message,
|
||||
containerRef,
|
||||
bot,
|
||||
@ -52,7 +59,10 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
|
||||
openChatByInvite,
|
||||
startBot,
|
||||
focusMessage,
|
||||
openUrl,
|
||||
openPremiumModal,
|
||||
} = getActions();
|
||||
|
||||
const lang = useLang();
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
@ -72,6 +82,8 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
|
||||
handleContextMenuClose, handleContextMenuHide,
|
||||
} = useContextMenuHandlers(ref, IS_TOUCH_ENV, true, IS_ANDROID);
|
||||
const [isAboutAdsModalOpen, openAboutAdsModal, closeAboutAdsModal] = useFlag(false);
|
||||
const { isMobile } = useAppLayout();
|
||||
const withAvatar = Boolean(message?.isAvatarShown && peer);
|
||||
|
||||
useEffect(() => {
|
||||
return shouldObserve ? observeIntersection(contentRef.current!, (target) => {
|
||||
@ -86,6 +98,25 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
|
||||
handleBeforeContextMenu(e);
|
||||
};
|
||||
|
||||
const handleAvatarClick = useLastCallback(() => {
|
||||
if (!peer) {
|
||||
return;
|
||||
}
|
||||
|
||||
openChat({ id: peer.id });
|
||||
});
|
||||
|
||||
const handleLinkClick = useLastCallback((e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
e.preventDefault();
|
||||
openUrl({ url: message!.webPage!.url, shouldSkipModal: true });
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
const handleCloseSponsoredMessage = useLastCallback(() => {
|
||||
openPremiumModal();
|
||||
});
|
||||
|
||||
const handleClick = useLastCallback(() => {
|
||||
if (!message) return;
|
||||
if (message.chatInviteHash) {
|
||||
@ -108,42 +139,119 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function renderAvatar() {
|
||||
return (
|
||||
<Avatar
|
||||
size={isMobile ? 'small-mobile' : 'small'}
|
||||
peer={peer}
|
||||
onClick={peer ? handleAvatarClick : undefined}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function renderContent() {
|
||||
if (message?.webPage) {
|
||||
return (
|
||||
<>
|
||||
<div className="text-content with-meta" dir="auto" ref={contentRef}>
|
||||
<div className="message-title message-peer" dir="ltr">
|
||||
{renderText(message.webPage.siteName)}
|
||||
</div>
|
||||
<span className="text-content-inner" dir="auto">
|
||||
{renderTextWithEntities({
|
||||
text: message!.text.text,
|
||||
entities: message!.text.entities,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="SponsoredMessage__button"
|
||||
size="tiny"
|
||||
color="translucent"
|
||||
isRectangular
|
||||
onClick={handleLinkClick}
|
||||
>
|
||||
<i className="icon icon-arrow-right" aria-hidden />
|
||||
{lang('OpenLink')}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="message-title message-peer" dir="auto">
|
||||
{bot && renderText(getUserFullName(bot) || '')}
|
||||
{channel && renderText(message!.chatInviteTitle || getChatTitle(lang, channel) || '')}
|
||||
</div>
|
||||
<div className="text-content with-meta" dir="auto" ref={contentRef}>
|
||||
<span className="text-content-inner" dir="auto">
|
||||
{renderTextWithEntities({
|
||||
text: message!.text.text,
|
||||
entities: message!.text.entities,
|
||||
})}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
className="SponsoredMessage__button"
|
||||
size="tiny"
|
||||
color="translucent"
|
||||
isRectangular
|
||||
onClick={handleClick}
|
||||
>
|
||||
{lang(message!.isBot
|
||||
? 'Conversation.ViewBot'
|
||||
: (message!.channelPostId ? 'Conversation.ViewPost' : 'Conversation.ViewChannel'))}
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
const contentClassName = buildClassName(
|
||||
'message-content has-shadow has-solid-background',
|
||||
withAvatar && 'has-appendix',
|
||||
getPeerColorClass(peer || channel, true, true),
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
key="sponsored-message"
|
||||
className="SponsoredMessage Message open"
|
||||
onMouseDown={handleMouseDown}
|
||||
onContextMenu={handleContextMenu}
|
||||
className={buildClassName('SponsoredMessage Message open', withAvatar && 'with-avatar')}
|
||||
>
|
||||
<div className="message-content has-shadow has-solid-background" dir="auto">
|
||||
{withAvatar && renderAvatar()}
|
||||
<div
|
||||
className={contentClassName}
|
||||
dir="auto"
|
||||
onMouseDown={handleMouseDown}
|
||||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
<div className="content-inner" dir="auto">
|
||||
<div className="message-title" dir="ltr">
|
||||
{bot && renderText(getUserFullName(bot) || '')}
|
||||
{channel && renderText(message.chatInviteTitle || getChatTitle(lang, channel) || '')}
|
||||
</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>
|
||||
|
||||
<span className="MessageMeta" dir="ltr">
|
||||
<span className="message-signature">
|
||||
{message.isRecommended ? lang('Message.RecommendedLabel') : lang('SponsoredMessage')}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<Button color="secondary" size="tiny" ripple onClick={handleClick} className="SponsoredMessage__button">
|
||||
{lang(message.isBot
|
||||
? 'Conversation.ViewBot'
|
||||
: (message.channelPostId ? 'Conversation.ViewPost' : 'Conversation.ViewChannel'))}
|
||||
</Button>
|
||||
{channel && (
|
||||
<Avatar
|
||||
size="large"
|
||||
peer={channel}
|
||||
className={buildClassName('channel-avatar', lang.isRtl && 'is-rtl')}
|
||||
/>
|
||||
)}
|
||||
<span className="message-title message-type">
|
||||
{message!.isRecommended ? lang('Message.RecommendedLabel') : lang('SponsoredMessage')}
|
||||
</span>
|
||||
{renderContent()}
|
||||
</div>
|
||||
{withAvatar && <MessageAppendix />}
|
||||
<Button
|
||||
className="message-action-button"
|
||||
color="translucent-white"
|
||||
round
|
||||
size="tiny"
|
||||
ariaLabel={lang('Close')}
|
||||
onClick={handleCloseSponsoredMessage}
|
||||
>
|
||||
<i className="icon icon-close" aria-hidden />
|
||||
</Button>
|
||||
</div>
|
||||
{contextMenuPosition && (
|
||||
<SponsoredMessageContextMenuContainer
|
||||
@ -166,10 +274,12 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { chatId }): StateProps => {
|
||||
const message = selectSponsoredMessage(global, chatId);
|
||||
const peer = message?.chatId ? selectChat(global, message?.chatId) : undefined;
|
||||
const { chatId: fromChatId, isBot } = message || {};
|
||||
|
||||
return {
|
||||
message,
|
||||
peer,
|
||||
bot: fromChatId && isBot ? selectUser(global, fromChatId) : undefined,
|
||||
channel: !isBot && fromChatId ? selectChat(global, fromChatId) : undefined,
|
||||
};
|
||||
|
||||
@ -125,7 +125,6 @@
|
||||
.site-description {
|
||||
&:last-child::after {
|
||||
content: "";
|
||||
display: inline-block;
|
||||
width: var(--meta-safe-area-size);
|
||||
height: 0.75rem;
|
||||
float: right;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user