diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index 82df7d544..c016efef7 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -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, + }; +} diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index 1496634f4..39516d96f 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -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; diff --git a/src/components/middle/message/Message.tsx b/src/components/middle/message/Message.tsx index 6aaafefcd..13e091a33 100644 --- a/src/components/middle/message/Message.tsx +++ b/src/components/middle/message/Message.tsx @@ -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 = ({ ); }; -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 ( - - - - - - - - - - - - - - ); -} - export default memo(withGlobal( (global, ownProps): StateProps => { const { diff --git a/src/components/middle/message/MessageAppendix.tsx b/src/components/middle/message/MessageAppendix.tsx new file mode 100644 index 000000000..3c6278f36 --- /dev/null +++ b/src/components/middle/message/MessageAppendix.tsx @@ -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 ( + + + + + + + + + + + + + + ); +} + +export default MessageAppendix; diff --git a/src/components/middle/message/SponsoredMessage.scss b/src/components/middle/message/SponsoredMessage.scss index 6c7cd09b0..69fd433ba 100644 --- a/src/components/middle/message/SponsoredMessage.scss +++ b/src/components/middle/message/SponsoredMessage.scss @@ -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); + } } } diff --git a/src/components/middle/message/SponsoredMessage.tsx b/src/components/middle/message/SponsoredMessage.tsx index 6a8facbf4..7a51b45e6 100644 --- a/src/components/middle/message/SponsoredMessage.tsx +++ b/src/components/middle/message/SponsoredMessage.tsx @@ -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 = ({ chatId, + peer, message, containerRef, bot, @@ -52,7 +59,10 @@ const SponsoredMessage: FC = ({ openChatByInvite, startBot, focusMessage, + openUrl, + openPremiumModal, } = getActions(); + const lang = useLang(); // eslint-disable-next-line no-null/no-null const ref = useRef(null); @@ -72,6 +82,8 @@ const SponsoredMessage: FC = ({ 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 = ({ handleBeforeContextMenu(e); }; + const handleAvatarClick = useLastCallback(() => { + if (!peer) { + return; + } + + openChat({ id: peer.id }); + }); + + const handleLinkClick = useLastCallback((e: ReactMouseEvent) => { + 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 = ({ return undefined; } + function renderAvatar() { + return ( + + ); + } + + function renderContent() { + if (message?.webPage) { + return ( + <> +
+
+ {renderText(message.webPage.siteName)} +
+ + {renderTextWithEntities({ + text: message!.text.text, + entities: message!.text.entities, + })} + +
+ + + + ); + } + + return ( + <> +
+ {bot && renderText(getUserFullName(bot) || '')} + {channel && renderText(message!.chatInviteTitle || getChatTitle(lang, channel) || '')} +
+
+ + {renderTextWithEntities({ + text: message!.text.text, + entities: message!.text.entities, + })} + +
+ + + + ); + } + + const contentClassName = buildClassName( + 'message-content has-shadow has-solid-background', + withAvatar && 'has-appendix', + getPeerColorClass(peer || channel, true, true), + ); + return (
-
+ {withAvatar && renderAvatar()} +
-
- {bot && renderText(getUserFullName(bot) || '')} - {channel && renderText(message.chatInviteTitle || getChatTitle(lang, channel) || '')} -
- -
- - {renderTextWithEntities({ - text: message.text.text, - entities: message.text.entities, - })} - - - - - {message.isRecommended ? lang('Message.RecommendedLabel') : lang('SponsoredMessage')} - - -
- - + {channel && ( + + )} + + {message!.isRecommended ? lang('Message.RecommendedLabel') : lang('SponsoredMessage')} + + {renderContent()}
+ {withAvatar && } +
{contextMenuPosition && ( = ({ export default memo(withGlobal( (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, }; diff --git a/src/components/middle/message/WebPage.scss b/src/components/middle/message/WebPage.scss index dcda88ea9..ebbe9c644 100644 --- a/src/components/middle/message/WebPage.scss +++ b/src/components/middle/message/WebPage.scss @@ -125,7 +125,6 @@ .site-description { &:last-child::after { content: ""; - display: inline-block; width: var(--meta-safe-area-size); height: 0.75rem; float: right;