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

View File

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

View File

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

View File

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

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({
chat,
}: {

View File

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

View File

@ -103,6 +103,7 @@ type StateProps = {
isRepliesChat?: boolean;
isCreator?: boolean;
isBot?: boolean;
isSynced?: boolean;
messageIds?: number[];
messagesById?: Record<number, ApiMessage>;
firstUnreadId?: number;
@ -147,6 +148,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
isChannelChat,
isGroupChat,
canPost,
isSynced,
isReady,
isChatWithSelf,
isRepliesChat,
@ -214,10 +216,10 @@ const MessageList: FC<OwnProps & StateProps> = ({
}, [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<OwnProps>(
isChatWithSelf: selectIsChatWithSelf(global, chatId),
isRepliesChat: isChatWithRepliesBot(chatId),
isBot: Boolean(chatBot),
isSynced: global.isSynced,
messageIds,
messagesById,
firstUnreadId: selectFirstUnreadId(global, chatId, threadId),

View File

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

View File

@ -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<OwnProps & StateProps> = ({
viewSponsoredMessage,
openChat,
openChatByInvite,
requestAppWebView,
startBot,
focusMessage,
openUrl,
openPremiumModal,
clickSponsoredMessage,
} = getActions();
const lang = useLang();
@ -84,6 +87,7 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
const handleLinkClick = useLastCallback((e: ReactMouseEvent<HTMLButtonElement, MouseEvent>) => {
e.preventDefault();
clickSponsoredMessage({ chatId });
openUrl({ url: message!.webPage!.url, shouldSkipModal: true });
return false;
@ -119,7 +125,20 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
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<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() {
if (message?.webPage) {
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 (
<>
<div className="message-title message-peer" dir="auto">
{bot && renderText(getUserFullName(bot) || '')}
{channel && renderText(message!.chatInviteTitle || getChatTitle(lang, channel) || '')}
</div>
<div className="message-title message-peer" dir="auto">{title}</div>
<div className="text-content with-meta" dir="auto" ref={contentRef}>
<span className="text-content-inner" dir="auto">
{renderTextWithEntities({
@ -201,9 +258,7 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
isRectangular
onClick={handleClick}
>
{lang(message!.isBot
? 'Conversation.ViewBot'
: (message!.channelPostId ? 'Conversation.ViewPost' : 'Conversation.ViewChannel'))}
{buttonText}
</Button>
</>
);
@ -211,7 +266,7 @@ const SponsoredMessage: FC<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
onContextMenu={handleContextMenu}
>
<div className="content-inner" dir="auto">
{channel && (
<Avatar
size="large"
peer={channel}
className={buildClassName('channel-avatar', lang.isRtl && 'is-rtl')}
/>
)}
{renderPhoto()}
<span className="message-title message-type">
{message!.isRecommended ? lang('Message.RecommendedLabel') : lang('SponsoredMessage')}
</span>

View File

@ -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<void> => {
const { chatId, offsetId } = payload;
const chat = selectChat(global, chatId);

View File

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

View File

@ -1336,6 +1336,9 @@ export interface ActionPayloads {
viewSponsoredMessage: {
chatId: string;
};
clickSponsoredMessage: {
chatId: string;
};
loadSendAs: {
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.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;

View File

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