Sponsored Message: Update UI (#3989)

This commit is contained in:
Alexander Zinchuk 2023-12-04 14:37:55 +01:00
parent 1ca211978c
commit 54d0ec5d7e
7 changed files with 321 additions and 65 deletions

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

@ -125,7 +125,6 @@
.site-description {
&:last-child::after {
content: "";
display: inline-block;
width: var(--meta-safe-area-size);
height: 0.75rem;
float: right;