Sponsored Message: Support BotApp, buttonText and user colors (#4080)

This commit is contained in:
Alexander Zinchuk 2023-12-12 12:34:29 +01:00
parent e079ba7a16
commit d32afadbd1
14 changed files with 129 additions and 30 deletions

View File

@ -13,6 +13,7 @@ import type {
ApiBotInlineSwitchWebview, ApiBotInlineSwitchWebview,
ApiBotMenuButton, ApiBotMenuButton,
ApiInlineResultType, ApiInlineResultType,
ApiMessagesBotApp,
} from '../../types'; } from '../../types';
import { pick } from '../../../util/iteratees'; 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 { export function buildApiBotApp(app: GramJs.TypeBotApp): ApiBotApp | undefined {
const { app, inactive, requestWriteAccess } = botApp;
if (app instanceof GramJs.BotAppNotModified) return undefined; if (app instanceof GramJs.BotAppNotModified) return undefined;
const { const {
id, accessHash, title, description, shortName, photo, document, id, accessHash, title, description, shortName, photo, document,
} = app; } = app;
@ -167,6 +168,16 @@ export function buildApiBotApp(botApp: GramJs.messages.BotApp): ApiBotApp | unde
shortName, shortName,
photo: apiPhoto, photo: apiPhoto,
document: apiDocument, 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, isInactive: inactive,
shouldRequestWriteAccess: requestWriteAccess, shouldRequestWriteAccess: requestWriteAccess,
}; };

View File

@ -50,6 +50,7 @@ import {
resolveMessageApiChatId, resolveMessageApiChatId,
serializeBytes, serializeBytes,
} from '../helpers'; } from '../helpers';
import { buildApiBotApp } from './bots';
import { buildApiCallDiscardReason } from './calls'; import { buildApiCallDiscardReason } from './calls';
import { import {
buildApiPhoto, buildApiPhoto,
@ -78,7 +79,7 @@ export function setMessageBuilderCurrentUserId(_currentUserId: string) {
export function buildApiSponsoredMessage(mtpMessage: GramJs.SponsoredMessage): ApiSponsoredMessage | undefined { export function buildApiSponsoredMessage(mtpMessage: GramJs.SponsoredMessage): ApiSponsoredMessage | undefined {
const { const {
fromId, message, entities, startParam, channelPost, chatInvite, chatInviteHash, randomId, recommended, sponsorInfo, fromId, message, entities, startParam, channelPost, chatInvite, chatInviteHash, randomId, recommended, sponsorInfo,
additionalInfo, showPeerPhoto, webpage, additionalInfo, showPeerPhoto, webpage, buttonText, app,
} = mtpMessage; } = mtpMessage;
const chatId = fromId ? getApiChatIdFromMtpPeer(fromId) : undefined; const chatId = fromId ? getApiChatIdFromMtpPeer(fromId) : undefined;
const chatInviteTitle = chatInvite const chatInviteTitle = chatInvite
@ -102,6 +103,8 @@ export function buildApiSponsoredMessage(mtpMessage: GramJs.SponsoredMessage): A
...(channelPost && { channelPostId: channelPost }), ...(channelPost && { channelPostId: channelPost }),
...(sponsorInfo && { sponsorInfo }), ...(sponsorInfo && { sponsorInfo }),
...(additionalInfo && { additionalInfo }), ...(additionalInfo && { additionalInfo }),
...(buttonText && { buttonText }),
...(app && { botApp: buildApiBotApp(app) }),
}; };
} }

View File

@ -3,16 +3,21 @@ import { Api as GramJs } from '../../../lib/gramjs';
import type { import type {
ApiBotApp, ApiBotApp,
ApiChat, ApiInputMessageReplyInfo, ApiPeer, ApiThemeParameters, ApiUser, OnApiUpdate, ApiChat,
ApiInputMessageReplyInfo,
ApiPeer,
ApiThemeParameters,
ApiUser,
OnApiUpdate,
} from '../../types'; } from '../../types';
import { WEB_APP_PLATFORM } from '../../../config'; import { WEB_APP_PLATFORM } from '../../../config';
import { buildCollectionByKey } from '../../../util/iteratees'; import { buildCollectionByKey } from '../../../util/iteratees';
import { import {
buildApiAttachBot, buildApiAttachBot,
buildApiBotApp,
buildApiBotInlineMediaResult, buildApiBotInlineMediaResult,
buildApiBotInlineResult, buildApiBotInlineResult,
buildApiMessagesBotApp,
buildBotSwitchPm, buildBotSwitchPm,
buildBotSwitchWebview, buildBotSwitchWebview,
} from '../apiBuilders/bots'; } from '../apiBuilders/bots';
@ -255,7 +260,7 @@ export async function fetchBotApp({
return undefined; return undefined;
} }
return buildApiBotApp(result); return buildApiMessagesBotApp(result);
} }
export async function requestAppWebView({ export async function requestAppWebView({

View File

@ -33,7 +33,7 @@ export {
fetchPinnedMessages, fetchScheduledHistory, sendScheduledMessages, rescheduleMessage, deleteScheduledMessages, fetchPinnedMessages, fetchScheduledHistory, sendScheduledMessages, rescheduleMessage, deleteScheduledMessages,
reportMessages, sendMessageAction, fetchSeenBy, fetchSponsoredMessages, viewSponsoredMessage, fetchSendAs, reportMessages, sendMessageAction, fetchSeenBy, fetchSponsoredMessages, viewSponsoredMessage, fetchSendAs,
saveDefaultSendAs, fetchUnreadReactions, readAllReactions, fetchUnreadMentions, readAllMentions, transcribeAudio, saveDefaultSendAs, fetchUnreadReactions, readAllReactions, fetchUnreadMentions, readAllMentions, transcribeAudio,
closePoll, fetchExtendedMedia, translateText, fetchMessageViews, fetchDiscussionMessage, closePoll, fetchExtendedMedia, translateText, fetchMessageViews, fetchDiscussionMessage, clickSponsoredMessage,
} from './messages'; } from './messages';
export { export {

View File

@ -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({ export function readAllMentions({
chat, chat,
}: { }: {

View File

@ -602,6 +602,8 @@ export type ApiSponsoredMessage = {
expiresAt: number; expiresAt: number;
sponsorInfo?: string; sponsorInfo?: string;
additionalInfo?: string; additionalInfo?: string;
buttonText?: string;
botApp?: ApiBotApp;
}; };
// KeyboardButtons // KeyboardButtons
@ -729,6 +731,9 @@ export type ApiBotApp = {
description: string; description: string;
photo?: ApiPhoto; photo?: ApiPhoto;
document?: ApiDocument; document?: ApiDocument;
};
export type ApiMessagesBotApp = ApiBotApp & {
isInactive?: boolean; isInactive?: boolean;
shouldRequestWriteAccess?: boolean; shouldRequestWriteAccess?: boolean;
}; };

View File

@ -103,6 +103,7 @@ type StateProps = {
isRepliesChat?: boolean; isRepliesChat?: boolean;
isCreator?: boolean; isCreator?: boolean;
isBot?: boolean; isBot?: boolean;
isSynced?: boolean;
messageIds?: number[]; messageIds?: number[];
messagesById?: Record<number, ApiMessage>; messagesById?: Record<number, ApiMessage>;
firstUnreadId?: number; firstUnreadId?: number;
@ -147,6 +148,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
isChannelChat, isChannelChat,
isGroupChat, isGroupChat,
canPost, canPost,
isSynced,
isReady, isReady,
isChatWithSelf, isChatWithSelf,
isRepliesChat, isRepliesChat,
@ -214,10 +216,10 @@ const MessageList: FC<OwnProps & StateProps> = ({
}, [firstUnreadId]); }, [firstUnreadId]);
useEffect(() => { useEffect(() => {
if (!isCurrentUserPremium && isChannelChat && isReady) { if (!isCurrentUserPremium && isChannelChat && isSynced && isReady) {
loadSponsoredMessages({ chatId }); 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) // Updated only once when messages are loaded (as we want the unread divider to keep its position)
useSyncEffect(() => { useSyncEffect(() => {
@ -682,6 +684,7 @@ export default memo(withGlobal<OwnProps>(
isChatWithSelf: selectIsChatWithSelf(global, chatId), isChatWithSelf: selectIsChatWithSelf(global, chatId),
isRepliesChat: isChatWithRepliesBot(chatId), isRepliesChat: isChatWithRepliesBot(chatId),
isBot: Boolean(chatBot), isBot: Boolean(chatBot),
isSynced: global.isSynced,
messageIds, messageIds,
messagesById, messagesById,
firstUnreadId: selectFirstUnreadId(global, chatId, threadId), firstUnreadId: selectFirstUnreadId(global, chatId, threadId),

View File

@ -27,6 +27,8 @@
} }
.message-type { .message-type {
padding-inline-end: 0.25rem;
text-transform: capitalize; text-transform: capitalize;
} }

View File

@ -10,6 +10,7 @@ import type {
import { getChatTitle, getUserFullName } from '../../../global/helpers'; import { getChatTitle, getUserFullName } from '../../../global/helpers';
import { selectChat, selectSponsoredMessage, selectUser } from '../../../global/selectors'; import { selectChat, selectSponsoredMessage, selectUser } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { extractCurrentThemeParams } from '../../../util/themeStyle';
import { IS_ANDROID, IS_TOUCH_ENV } from '../../../util/windowEnvironment'; import { IS_ANDROID, IS_TOUCH_ENV } from '../../../util/windowEnvironment';
import { getPeerColorClass } from '../../common/helpers/peerColor'; import { getPeerColorClass } from '../../common/helpers/peerColor';
import renderText from '../../common/helpers/renderText'; import renderText from '../../common/helpers/renderText';
@ -57,10 +58,12 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
viewSponsoredMessage, viewSponsoredMessage,
openChat, openChat,
openChatByInvite, openChatByInvite,
requestAppWebView,
startBot, startBot,
focusMessage, focusMessage,
openUrl, openUrl,
openPremiumModal, openPremiumModal,
clickSponsoredMessage,
} = getActions(); } = getActions();
const lang = useLang(); const lang = useLang();
@ -84,6 +87,7 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
const [isAboutAdsModalOpen, openAboutAdsModal, closeAboutAdsModal] = useFlag(false); const [isAboutAdsModalOpen, openAboutAdsModal, closeAboutAdsModal] = useFlag(false);
const { isMobile } = useAppLayout(); const { isMobile } = useAppLayout();
const withAvatar = Boolean(message?.isAvatarShown && peer); const withAvatar = Boolean(message?.isAvatarShown && peer);
const isBotApp = Boolean(message?.botApp);
useEffect(() => { useEffect(() => {
return shouldObserve ? observeIntersection(contentRef.current!, (target) => { return shouldObserve ? observeIntersection(contentRef.current!, (target) => {
@ -108,6 +112,8 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
const handleLinkClick = useLastCallback((e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => { const handleLinkClick = useLastCallback((e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault(); e.preventDefault();
clickSponsoredMessage({ chatId });
openUrl({ url: message!.webPage!.url, shouldSkipModal: true }); openUrl({ url: message!.webPage!.url, shouldSkipModal: true });
return false; return false;
@ -119,7 +125,20 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
const handleClick = useLastCallback(() => { const handleClick = useLastCallback(() => {
if (!message) return; 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 }); openChatByInvite({ hash: message.chatInviteHash });
} else if (message.channelPostId) { } else if (message.channelPostId) {
focusMessage({ chatId: message.chatId!, messageId: message.channelPostId }); focusMessage({ chatId: message.chatId!, messageId: message.channelPostId });
@ -149,6 +168,33 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
); );
} }
function renderPhoto() {
if (message?.botApp) {
if (!message.botApp.photo) return undefined;
return (
<Avatar
size="large"
peer={bot}
photo={message.botApp.photo}
className={buildClassName('channel-avatar', lang.isRtl && 'is-rtl')}
/>
);
}
if (channel) {
return (
<Avatar
size="large"
peer={channel}
className={buildClassName('channel-avatar', lang.isRtl && 'is-rtl')}
/>
);
}
return undefined;
}
function renderContent() { function renderContent() {
if (message?.webPage) { if (message?.webPage) {
return ( return (
@ -179,12 +225,23 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
); );
} }
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 ( return (
<> <>
<div className="message-title message-peer" dir="auto"> <div className="message-title message-peer" dir="auto">{title}</div>
{bot && renderText(getUserFullName(bot) || '')}
{channel && renderText(message!.chatInviteTitle || getChatTitle(lang, channel) || '')}
</div>
<div className="text-content with-meta" dir="auto" ref={contentRef}> <div className="text-content with-meta" dir="auto" ref={contentRef}>
<span className="text-content-inner" dir="auto"> <span className="text-content-inner" dir="auto">
{renderTextWithEntities({ {renderTextWithEntities({
@ -201,9 +258,7 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
isRectangular isRectangular
onClick={handleClick} onClick={handleClick}
> >
{lang(message!.isBot {buttonText}
? 'Conversation.ViewBot'
: (message!.channelPostId ? 'Conversation.ViewPost' : 'Conversation.ViewChannel'))}
</Button> </Button>
</> </>
); );
@ -211,7 +266,7 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
const contentClassName = buildClassName( const contentClassName = buildClassName(
'message-content has-shadow has-solid-background has-appendix', 'message-content has-shadow has-solid-background has-appendix',
getPeerColorClass(peer || channel, true, true), getPeerColorClass(bot || peer || channel),
); );
return ( return (
@ -228,13 +283,7 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
> >
<div className="content-inner" dir="auto"> <div className="content-inner" dir="auto">
{channel && ( {renderPhoto()}
<Avatar
size="large"
peer={channel}
className={buildClassName('channel-avatar', lang.isRtl && 'is-rtl')}
/>
)}
<span className="message-title message-type"> <span className="message-title message-type">
{message!.isRecommended ? lang('Message.RecommendedLabel') : lang('SponsoredMessage')} {message!.isRecommended ? lang('Message.RecommendedLabel') : lang('SponsoredMessage')}
</span> </span>

View File

@ -1458,6 +1458,17 @@ addActionHandler('viewSponsoredMessage', (global, actions, payload): ActionRetur
void callApi('viewSponsoredMessage', { chat, random: message.randomId }); 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<void> => { addActionHandler('fetchUnreadMentions', async (global, actions, payload): Promise<void> => {
const { chatId, offsetId } = payload; const { chatId, offsetId } = payload;
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);

View File

@ -9,9 +9,7 @@ import type {
} from '../../api/types'; } from '../../api/types';
import type { LangFn } from '../../hooks/useLang'; import type { LangFn } from '../../hooks/useLang';
import type { NotifyException, NotifySettings } from '../../types'; import type { NotifyException, NotifySettings } from '../../types';
import { import { MAIN_THREAD_ID } from '../../api/types';
MAIN_THREAD_ID,
} from '../../api/types';
import { import {
ARCHIVED_FOLDER_ID, CHANNEL_ID_LENGTH, GENERAL_TOPIC_ID, REPLIES_USER_ID, TME_LINK_PREFIX, 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) { export function getPeerColorKey(peer: ApiPeer | undefined) {
if (peer?.color?.color) return peer.color.color; if (peer?.color?.color) return peer.color.color;
const index = peer ? getPeerIdDividend(peer.id) % 7 : 0; return peer ? getPeerIdDividend(peer.id) % 7 : 0;
return index;
} }
export function getPeerColorCount(peer: ApiPeer) { export function getPeerColorCount(peer: ApiPeer) {
const key = getPeerColorKey(peer); const key = getPeerColorKey(peer);
// eslint-disable-next-line eslint-multitab-tt/no-immediate-global
return getGlobal().peerColors?.general[key].colors?.length || 1; return getGlobal().peerColors?.general[key].colors?.length || 1;
} }

View File

@ -1336,6 +1336,9 @@ export interface ActionPayloads {
viewSponsoredMessage: { viewSponsoredMessage: {
chatId: string; chatId: string;
}; };
clickSponsoredMessage: {
chatId: string;
};
loadSendAs: { loadSendAs: {
chatId: string; chatId: string;
}; };

View File

@ -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.updatePinnedForumTopic#6c2d9026 channel:InputChannel topic_id:int pinned:Bool = Updates;
channels.deleteTopicHistory#34435f2d channel:InputChannel top_msg_id:int = messages.AffectedHistory; channels.deleteTopicHistory#34435f2d channel:InputChannel top_msg_id:int = messages.AffectedHistory;
channels.toggleParticipantsHidden#6a6e7854 channel:InputChannel enabled:Bool = Updates; channels.toggleParticipantsHidden#6a6e7854 channel:InputChannel enabled:Bool = Updates;
channels.clickSponsoredMessage#18afbc93 channel:InputChannel random_id:bytes = Bool;
bots.canSendMessage#1359f4e6 bot:InputUser = Bool; bots.canSendMessage#1359f4e6 bot:InputUser = Bool;
bots.allowSendMessage#f132e3ef bot:InputUser = Updates; bots.allowSendMessage#f132e3ef bot:InputUser = Updates;
bots.invokeWebViewCustomMethod#87fc5e7 bot:InputUser custom_method:string params:DataJSON = DataJSON; bots.invokeWebViewCustomMethod#87fc5e7 bot:InputUser custom_method:string params:DataJSON = DataJSON;

View File

@ -288,6 +288,7 @@
"channels.updatePinnedForumTopic", "channels.updatePinnedForumTopic",
"channels.deleteTopicHistory", "channels.deleteTopicHistory",
"channels.toggleParticipantsHidden", "channels.toggleParticipantsHidden",
"channels.clickSponsoredMessage",
"photos.uploadContactProfilePhoto", "photos.uploadContactProfilePhoto",
"messages.getMessagesViews", "messages.getMessagesViews",
"chatlists.exportChatlistInvite", "chatlists.exportChatlistInvite",