From d32afadbd1f7d4f33a5ea40ad9dfecbab322e7c0 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Tue, 12 Dec 2023 12:34:29 +0100 Subject: [PATCH] Sponsored Message: Support BotApp, buttonText and user colors (#4080) --- src/api/gramjs/apiBuilders/bots.ts | 15 +++- src/api/gramjs/apiBuilders/messages.ts | 5 +- src/api/gramjs/methods/bots.ts | 11 ++- src/api/gramjs/methods/index.ts | 2 +- src/api/gramjs/methods/messages.ts | 7 ++ src/api/types/messages.ts | 5 ++ src/components/middle/MessageList.tsx | 7 +- .../middle/message/SponsoredMessage.scss | 2 + .../middle/message/SponsoredMessage.tsx | 81 +++++++++++++++---- src/global/actions/api/messages.ts | 11 +++ src/global/helpers/chats.ts | 8 +- src/global/types.ts | 3 + src/lib/gramjs/tl/apiTl.js | 1 + src/lib/gramjs/tl/static/api.json | 1 + 14 files changed, 129 insertions(+), 30 deletions(-) diff --git a/src/api/gramjs/apiBuilders/bots.ts b/src/api/gramjs/apiBuilders/bots.ts index 7798ad9eb..17272849b 100644 --- a/src/api/gramjs/apiBuilders/bots.ts +++ b/src/api/gramjs/apiBuilders/bots.ts @@ -13,6 +13,7 @@ import type { ApiBotInlineSwitchWebview, ApiBotMenuButton, ApiInlineResultType, + ApiMessagesBotApp, } from '../../types'; import { pick } from '../../../util/iteratees'; @@ -149,9 +150,9 @@ export function buildApiBotMenuButton(menuButton?: GramJs.TypeBotMenuButton): Ap }; } -export function buildApiBotApp(botApp: GramJs.messages.BotApp): ApiBotApp | undefined { - const { app, inactive, requestWriteAccess } = botApp; +export function buildApiBotApp(app: GramJs.TypeBotApp): ApiBotApp | undefined { if (app instanceof GramJs.BotAppNotModified) return undefined; + const { id, accessHash, title, description, shortName, photo, document, } = app; @@ -167,6 +168,16 @@ export function buildApiBotApp(botApp: GramJs.messages.BotApp): ApiBotApp | unde shortName, photo: apiPhoto, document: apiDocument, + }; +} + +export function buildApiMessagesBotApp(botApp: GramJs.messages.BotApp): ApiMessagesBotApp | undefined { + const { app, inactive, requestWriteAccess } = botApp; + const baseApp = buildApiBotApp(app); + if (!baseApp) return undefined; + + return { + ...baseApp, isInactive: inactive, shouldRequestWriteAccess: requestWriteAccess, }; diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index ea1a7a804..3e52a1d68 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -50,6 +50,7 @@ import { resolveMessageApiChatId, serializeBytes, } from '../helpers'; +import { buildApiBotApp } from './bots'; import { buildApiCallDiscardReason } from './calls'; import { buildApiPhoto, @@ -78,7 +79,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, showPeerPhoto, webpage, + additionalInfo, showPeerPhoto, webpage, buttonText, app, } = mtpMessage; const chatId = fromId ? getApiChatIdFromMtpPeer(fromId) : undefined; const chatInviteTitle = chatInvite @@ -102,6 +103,8 @@ export function buildApiSponsoredMessage(mtpMessage: GramJs.SponsoredMessage): A ...(channelPost && { channelPostId: channelPost }), ...(sponsorInfo && { sponsorInfo }), ...(additionalInfo && { additionalInfo }), + ...(buttonText && { buttonText }), + ...(app && { botApp: buildApiBotApp(app) }), }; } diff --git a/src/api/gramjs/methods/bots.ts b/src/api/gramjs/methods/bots.ts index e51b54e8c..31a7b0d8e 100644 --- a/src/api/gramjs/methods/bots.ts +++ b/src/api/gramjs/methods/bots.ts @@ -3,16 +3,21 @@ import { Api as GramJs } from '../../../lib/gramjs'; import type { ApiBotApp, - ApiChat, ApiInputMessageReplyInfo, ApiPeer, ApiThemeParameters, ApiUser, OnApiUpdate, + ApiChat, + ApiInputMessageReplyInfo, + ApiPeer, + ApiThemeParameters, + ApiUser, + OnApiUpdate, } from '../../types'; import { WEB_APP_PLATFORM } from '../../../config'; import { buildCollectionByKey } from '../../../util/iteratees'; import { buildApiAttachBot, - buildApiBotApp, buildApiBotInlineMediaResult, buildApiBotInlineResult, + buildApiMessagesBotApp, buildBotSwitchPm, buildBotSwitchWebview, } from '../apiBuilders/bots'; @@ -255,7 +260,7 @@ export async function fetchBotApp({ return undefined; } - return buildApiBotApp(result); + return buildApiMessagesBotApp(result); } export async function requestAppWebView({ diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index 1d44f6619..04ae81dc6 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -33,7 +33,7 @@ export { fetchPinnedMessages, fetchScheduledHistory, sendScheduledMessages, rescheduleMessage, deleteScheduledMessages, reportMessages, sendMessageAction, fetchSeenBy, fetchSponsoredMessages, viewSponsoredMessage, fetchSendAs, saveDefaultSendAs, fetchUnreadReactions, readAllReactions, fetchUnreadMentions, readAllMentions, transcribeAudio, - closePoll, fetchExtendedMedia, translateText, fetchMessageViews, fetchDiscussionMessage, + closePoll, fetchExtendedMedia, translateText, fetchMessageViews, fetchDiscussionMessage, clickSponsoredMessage, } from './messages'; export { diff --git a/src/api/gramjs/methods/messages.ts b/src/api/gramjs/methods/messages.ts index 496df9294..0002e1b15 100644 --- a/src/api/gramjs/methods/messages.ts +++ b/src/api/gramjs/methods/messages.ts @@ -1554,6 +1554,13 @@ export async function viewSponsoredMessage({ chat, random }: { chat: ApiChat; ra })); } +export function clickSponsoredMessage({ chat, random }: { chat: ApiChat; random: string }) { + return invokeRequest(new GramJs.channels.ClickSponsoredMessage({ + channel: buildInputPeer(chat.id, chat.accessHash), + randomId: deserializeBytes(random), + })); +} + export function readAllMentions({ chat, }: { diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index 0cf455cc9..10d236baf 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -602,6 +602,8 @@ export type ApiSponsoredMessage = { expiresAt: number; sponsorInfo?: string; additionalInfo?: string; + buttonText?: string; + botApp?: ApiBotApp; }; // KeyboardButtons @@ -729,6 +731,9 @@ export type ApiBotApp = { description: string; photo?: ApiPhoto; document?: ApiDocument; +}; + +export type ApiMessagesBotApp = ApiBotApp & { isInactive?: boolean; shouldRequestWriteAccess?: boolean; }; diff --git a/src/components/middle/MessageList.tsx b/src/components/middle/MessageList.tsx index 215139b14..9885459dc 100644 --- a/src/components/middle/MessageList.tsx +++ b/src/components/middle/MessageList.tsx @@ -103,6 +103,7 @@ type StateProps = { isRepliesChat?: boolean; isCreator?: boolean; isBot?: boolean; + isSynced?: boolean; messageIds?: number[]; messagesById?: Record; firstUnreadId?: number; @@ -147,6 +148,7 @@ const MessageList: FC = ({ isChannelChat, isGroupChat, canPost, + isSynced, isReady, isChatWithSelf, isRepliesChat, @@ -214,10 +216,10 @@ const MessageList: FC = ({ }, [firstUnreadId]); useEffect(() => { - if (!isCurrentUserPremium && isChannelChat && isReady) { + if (!isCurrentUserPremium && isChannelChat && isSynced && isReady) { loadSponsoredMessages({ chatId }); } - }, [isCurrentUserPremium, chatId, isReady, isChannelChat]); + }, [isCurrentUserPremium, chatId, isSynced, isReady, isChannelChat]); // Updated only once when messages are loaded (as we want the unread divider to keep its position) useSyncEffect(() => { @@ -682,6 +684,7 @@ export default memo(withGlobal( isChatWithSelf: selectIsChatWithSelf(global, chatId), isRepliesChat: isChatWithRepliesBot(chatId), isBot: Boolean(chatBot), + isSynced: global.isSynced, messageIds, messagesById, firstUnreadId: selectFirstUnreadId(global, chatId, threadId), diff --git a/src/components/middle/message/SponsoredMessage.scss b/src/components/middle/message/SponsoredMessage.scss index d9e330bcc..9b6c0ca6b 100644 --- a/src/components/middle/message/SponsoredMessage.scss +++ b/src/components/middle/message/SponsoredMessage.scss @@ -27,6 +27,8 @@ } .message-type { + padding-inline-end: 0.25rem; + text-transform: capitalize; } diff --git a/src/components/middle/message/SponsoredMessage.tsx b/src/components/middle/message/SponsoredMessage.tsx index 0c6840e94..e5c69f340 100644 --- a/src/components/middle/message/SponsoredMessage.tsx +++ b/src/components/middle/message/SponsoredMessage.tsx @@ -10,6 +10,7 @@ import type { import { getChatTitle, getUserFullName } from '../../../global/helpers'; import { selectChat, selectSponsoredMessage, selectUser } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; +import { extractCurrentThemeParams } from '../../../util/themeStyle'; import { IS_ANDROID, IS_TOUCH_ENV } from '../../../util/windowEnvironment'; import { getPeerColorClass } from '../../common/helpers/peerColor'; import renderText from '../../common/helpers/renderText'; @@ -57,10 +58,12 @@ const SponsoredMessage: FC = ({ viewSponsoredMessage, openChat, openChatByInvite, + requestAppWebView, startBot, focusMessage, openUrl, openPremiumModal, + clickSponsoredMessage, } = getActions(); const lang = useLang(); @@ -84,6 +87,7 @@ const SponsoredMessage: FC = ({ const [isAboutAdsModalOpen, openAboutAdsModal, closeAboutAdsModal] = useFlag(false); const { isMobile } = useAppLayout(); const withAvatar = Boolean(message?.isAvatarShown && peer); + const isBotApp = Boolean(message?.botApp); useEffect(() => { return shouldObserve ? observeIntersection(contentRef.current!, (target) => { @@ -108,6 +112,8 @@ const SponsoredMessage: FC = ({ const handleLinkClick = useLastCallback((e: ReactMouseEvent) => { e.preventDefault(); + + clickSponsoredMessage({ chatId }); openUrl({ url: message!.webPage!.url, shouldSkipModal: true }); return false; @@ -119,7 +125,20 @@ const SponsoredMessage: FC = ({ const handleClick = useLastCallback(() => { if (!message) return; - if (message.chatInviteHash) { + + clickSponsoredMessage({ chatId }); + + if (isBotApp) { + const { shortName } = message.botApp!; + const theme = extractCurrentThemeParams(); + + requestAppWebView({ + botId: message.chatId!, + appName: shortName, + startApp: message.startParam, + theme, + }); + } else if (message.chatInviteHash) { openChatByInvite({ hash: message.chatInviteHash }); } else if (message.channelPostId) { focusMessage({ chatId: message.chatId!, messageId: message.channelPostId }); @@ -149,6 +168,33 @@ const SponsoredMessage: FC = ({ ); } + function renderPhoto() { + if (message?.botApp) { + if (!message.botApp.photo) return undefined; + + return ( + + ); + } + + if (channel) { + return ( + + ); + } + + return undefined; + } + function renderContent() { if (message?.webPage) { return ( @@ -179,12 +225,23 @@ const SponsoredMessage: FC = ({ ); } + const buttonText = message?.buttonText ?? ( + isBotApp + ? lang('BotWebAppInstantViewOpen') + : (message!.isBot + ? lang('Conversation.ViewBot') + : lang(message!.channelPostId ? 'Conversation.ViewPost' : 'Conversation.ViewChannel') + )); + const title = isBotApp + ? message!.botApp!.title + : (bot + ? renderText(getUserFullName(bot) || '') + : (channel ? renderText(message!.chatInviteTitle || getChatTitle(lang, channel) || '') : '') + ); + return ( <> -
- {bot && renderText(getUserFullName(bot) || '')} - {channel && renderText(message!.chatInviteTitle || getChatTitle(lang, channel) || '')} -
+
{title}
{renderTextWithEntities({ @@ -201,9 +258,7 @@ const SponsoredMessage: FC = ({ isRectangular onClick={handleClick} > - {lang(message!.isBot - ? 'Conversation.ViewBot' - : (message!.channelPostId ? 'Conversation.ViewPost' : 'Conversation.ViewChannel'))} + {buttonText} ); @@ -211,7 +266,7 @@ const SponsoredMessage: FC = ({ const contentClassName = buildClassName( 'message-content has-shadow has-solid-background has-appendix', - getPeerColorClass(peer || channel, true, true), + getPeerColorClass(bot || peer || channel), ); return ( @@ -228,13 +283,7 @@ const SponsoredMessage: FC = ({ onContextMenu={handleContextMenu} >
- {channel && ( - - )} + {renderPhoto()} {message!.isRecommended ? lang('Message.RecommendedLabel') : lang('SponsoredMessage')} diff --git a/src/global/actions/api/messages.ts b/src/global/actions/api/messages.ts index f8bd6a717..de5132fdc 100644 --- a/src/global/actions/api/messages.ts +++ b/src/global/actions/api/messages.ts @@ -1458,6 +1458,17 @@ addActionHandler('viewSponsoredMessage', (global, actions, payload): ActionRetur void callApi('viewSponsoredMessage', { chat, random: message.randomId }); }); +addActionHandler('clickSponsoredMessage', (global, actions, payload): ActionReturnType => { + const { chatId } = payload; + const chat = selectChat(global, chatId); + const message = selectSponsoredMessage(global, chatId); + if (!chat || !message) { + return; + } + + void callApi('clickSponsoredMessage', { chat, random: message.randomId }); +}); + addActionHandler('fetchUnreadMentions', async (global, actions, payload): Promise => { const { chatId, offsetId } = payload; const chat = selectChat(global, chatId); diff --git a/src/global/helpers/chats.ts b/src/global/helpers/chats.ts index 85a973934..a875b7e30 100644 --- a/src/global/helpers/chats.ts +++ b/src/global/helpers/chats.ts @@ -9,9 +9,7 @@ import type { } from '../../api/types'; import type { LangFn } from '../../hooks/useLang'; import type { NotifyException, NotifySettings } from '../../types'; -import { - MAIN_THREAD_ID, -} from '../../api/types'; +import { MAIN_THREAD_ID } from '../../api/types'; import { ARCHIVED_FOLDER_ID, CHANNEL_ID_LENGTH, GENERAL_TOPIC_ID, REPLIES_USER_ID, TME_LINK_PREFIX, @@ -474,11 +472,11 @@ export function getPeerIdDividend(peerId: string) { export function getPeerColorKey(peer: ApiPeer | undefined) { if (peer?.color?.color) return peer.color.color; - const index = peer ? getPeerIdDividend(peer.id) % 7 : 0; - return index; + return peer ? getPeerIdDividend(peer.id) % 7 : 0; } export function getPeerColorCount(peer: ApiPeer) { const key = getPeerColorKey(peer); + // eslint-disable-next-line eslint-multitab-tt/no-immediate-global return getGlobal().peerColors?.general[key].colors?.length || 1; } diff --git a/src/global/types.ts b/src/global/types.ts index 5a076b9fc..dc15c0240 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -1336,6 +1336,9 @@ export interface ActionPayloads { viewSponsoredMessage: { chatId: string; }; + clickSponsoredMessage: { + chatId: string; + }; loadSendAs: { chatId: string; }; diff --git a/src/lib/gramjs/tl/apiTl.js b/src/lib/gramjs/tl/apiTl.js index 0cd8824a6..69ff8e03a 100644 --- a/src/lib/gramjs/tl/apiTl.js +++ b/src/lib/gramjs/tl/apiTl.js @@ -1441,6 +1441,7 @@ channels.editForumTopic#f4dfa185 flags:# channel:InputChannel topic_id:int title channels.updatePinnedForumTopic#6c2d9026 channel:InputChannel topic_id:int pinned:Bool = Updates; channels.deleteTopicHistory#34435f2d channel:InputChannel top_msg_id:int = messages.AffectedHistory; channels.toggleParticipantsHidden#6a6e7854 channel:InputChannel enabled:Bool = Updates; +channels.clickSponsoredMessage#18afbc93 channel:InputChannel random_id:bytes = Bool; bots.canSendMessage#1359f4e6 bot:InputUser = Bool; bots.allowSendMessage#f132e3ef bot:InputUser = Updates; bots.invokeWebViewCustomMethod#87fc5e7 bot:InputUser custom_method:string params:DataJSON = DataJSON; diff --git a/src/lib/gramjs/tl/static/api.json b/src/lib/gramjs/tl/static/api.json index ff0b1e6af..712117ba1 100644 --- a/src/lib/gramjs/tl/static/api.json +++ b/src/lib/gramjs/tl/static/api.json @@ -288,6 +288,7 @@ "channels.updatePinnedForumTopic", "channels.deleteTopicHistory", "channels.toggleParticipantsHidden", + "channels.clickSponsoredMessage", "photos.uploadContactProfilePhoto", "messages.getMessagesViews", "chatlists.exportChatlistInvite",