From 7b253a41031337f07aebf4d97e8392b7b6c0d5ea Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Tue, 19 Apr 2022 15:12:16 +0200 Subject: [PATCH] Introduce Web Bots (#1813) --- src/api/gramjs/apiBuilders/bots.ts | 76 ++- src/api/gramjs/apiBuilders/chats.ts | 1 - src/api/gramjs/apiBuilders/messages.ts | 19 + src/api/gramjs/apiBuilders/misc.ts | 6 +- src/api/gramjs/apiBuilders/users.ts | 15 +- src/api/gramjs/gramjsBuilders/index.ts | 7 + src/api/gramjs/methods/bots.ts | 135 ++++- src/api/gramjs/methods/index.ts | 1 + src/api/gramjs/methods/media.ts | 3 + src/api/gramjs/methods/settings.ts | 2 +- src/api/gramjs/updater.ts | 21 +- src/api/types/bots.ts | 19 + src/api/types/media.ts | 1 + src/api/types/messages.ts | 27 +- src/api/types/updates.ts | 16 +- src/api/types/users.ts | 19 +- src/assets/fonts/icomoon.woff | Bin 43412 -> 44064 bytes src/assets/fonts/icomoon.woff2 | Bin 20332 -> 20616 bytes src/bundles/extra.ts | 3 + src/components/common/EmbeddedMessage.tsx | 2 +- src/components/main/BotAttachModal.async.tsx | 16 + src/components/main/BotAttachModal.tsx | 34 ++ src/components/main/BotTrustModal.async.tsx | 16 + src/components/main/BotTrustModal.tsx | 47 ++ src/components/main/Main.tsx | 24 +- src/components/main/WebAppModal.async.tsx | 16 + src/components/main/WebAppModal.scss | 69 +++ src/components/main/WebAppModal.tsx | 292 +++++++++++ src/components/main/hooks/useWebAppFrame.ts | 148 ++++++ src/components/middle/AudioPlayer.tsx | 2 +- src/components/middle/HeaderMenuContainer.tsx | 2 +- src/components/middle/HeaderPinnedMessage.tsx | 48 +- src/components/middle/MessageList.tsx | 2 +- src/components/middle/MiddleHeader.scss | 20 +- .../middle/composer/AttachMenu.scss | 9 + src/components/middle/composer/AttachMenu.tsx | 38 +- .../AttachmentMenuBotIcon.module.scss | 21 + .../middle/composer/AttachmentMenuBotIcon.tsx | 49 ++ .../middle/composer/AttachmentMenuBotItem.tsx | 100 ++++ .../middle/composer/BotMenuButton.tsx | 61 +++ src/components/middle/composer/Composer.scss | 47 ++ src/components/middle/composer/Composer.tsx | 54 +- .../middle/composer/SymbolMenu.scss | 8 + .../middle/message/InlineButtons.scss | 7 + .../middle/message/InlineButtons.tsx | 3 +- src/components/middle/message/Message.tsx | 5 +- src/components/ui/Button.tsx | 20 +- src/components/ui/Menu.scss | 2 +- src/components/ui/MenuItem.tsx | 8 +- src/config.ts | 2 +- src/global/actions/api/bots.ts | 294 ++++++++++- src/global/actions/api/chats.ts | 73 ++- src/global/actions/apiUpdaters/misc.ts | 7 + src/global/actions/apiUpdaters/users.ts | 20 + src/global/actions/ui/misc.ts | 33 +- src/global/cache.ts | 11 + src/global/helpers/messageMedia.ts | 15 +- src/global/helpers/messages.ts | 6 + src/global/initialState.ts | 6 + src/global/selectors/chats.ts | 6 +- src/global/selectors/messages.ts | 17 + src/global/types.ts | 88 +++- src/lib/gramjs/tl/AllTLObjects.js | 2 +- src/lib/gramjs/tl/api.d.ts | 325 +++++++++++- src/lib/gramjs/tl/api.js | 6 +- src/lib/gramjs/tl/apiTl.js | 50 +- src/lib/gramjs/tl/static/api.json | 10 +- src/lib/gramjs/tl/static/api.tl | 102 ++-- src/lib/gramjs/tl/types-generator/template.js | 7 +- src/styles/Telegram T.json | 479 ++++++++++-------- src/styles/icons.scss | 21 +- src/util/deeplink.ts | 8 +- src/util/mediaLoader.ts | 1 + src/util/themeStyle.ts | 39 ++ 74 files changed, 2811 insertions(+), 358 deletions(-) create mode 100644 src/components/main/BotAttachModal.async.tsx create mode 100644 src/components/main/BotAttachModal.tsx create mode 100644 src/components/main/BotTrustModal.async.tsx create mode 100644 src/components/main/BotTrustModal.tsx create mode 100644 src/components/main/WebAppModal.async.tsx create mode 100644 src/components/main/WebAppModal.scss create mode 100644 src/components/main/WebAppModal.tsx create mode 100644 src/components/main/hooks/useWebAppFrame.ts create mode 100644 src/components/middle/composer/AttachmentMenuBotIcon.module.scss create mode 100644 src/components/middle/composer/AttachmentMenuBotIcon.tsx create mode 100644 src/components/middle/composer/AttachmentMenuBotItem.tsx create mode 100644 src/components/middle/composer/BotMenuButton.tsx create mode 100644 src/util/themeStyle.ts diff --git a/src/api/gramjs/apiBuilders/bots.ts b/src/api/gramjs/apiBuilders/bots.ts index d86141fec..512539b8a 100644 --- a/src/api/gramjs/apiBuilders/bots.ts +++ b/src/api/gramjs/apiBuilders/bots.ts @@ -1,12 +1,24 @@ import { Api as GramJs } from '../../../lib/gramjs'; import { - ApiBotInlineMediaResult, ApiBotInlineResult, ApiBotInlineSwitchPm, ApiInlineResultType, ApiWebDocument, + ApiAttachMenuBot, + ApiAttachMenuBotIcon, + ApiBotCommand, + ApiBotInfo, + ApiBotInlineMediaResult, + ApiBotInlineResult, + ApiBotInlineSwitchPm, + ApiBotMenuButton, + ApiInlineResultType, + ApiWebDocument, } from '../../types'; import { pick } from '../../../util/iteratees'; import { buildApiPhoto, buildApiThumbnailFromStripped } from './common'; -import { buildVideoFromDocument } from './messages'; +import { buildApiDocument, buildVideoFromDocument } from './messages'; import { buildStickerFromDocument } from './symbols'; +import localDb from '../localDb'; +import { buildApiPeerId } from './peers'; +import { omitVirtualClassFields } from './helpers'; export function buildApiBotInlineResult(result: GramJs.BotInlineResult, queryId: string): ApiBotInlineResult { const { @@ -50,6 +62,66 @@ export function buildBotSwitchPm(switchPm?: GramJs.InlineBotSwitchPM) { return switchPm ? pick(switchPm, ['text', 'startParam']) as ApiBotInlineSwitchPm : undefined; } +export function buildApiAttachMenuBot(bot: GramJs.AttachMenuBot): ApiAttachMenuBot { + return { + id: bot.botId.toString(), + shortName: bot.shortName, + icons: bot.icons.map(buildApiAttachMenuIcon).filter(Boolean), + }; +} + +function buildApiAttachMenuIcon(icon: GramJs.AttachMenuBotIcon): ApiAttachMenuBotIcon | undefined { + if (!(icon.icon instanceof GramJs.Document)) return undefined; + + const document = buildApiDocument(icon.icon); + + if (!document) return undefined; + + localDb.documents[String(icon.icon.id)] = icon.icon; + + return { + name: icon.name, + document, + }; +} + function buildApiWebDocument(document?: GramJs.TypeWebDocument): ApiWebDocument | undefined { return document ? pick(document, ['url', 'mimeType']) : undefined; } + +export function buildApiBotInfo(botInfo: GramJs.BotInfo): ApiBotInfo { + const { + description, userId, commands, menuButton, + } = botInfo; + + const botId = buildApiPeerId(userId, 'user'); + const commandsArray = commands.map((command) => buildApiBotCommand(botId, command)); + + return { + botId, + description, + menuButton: buildApiBotMenuButton(menuButton), + commands: commandsArray.length ? commandsArray : undefined, + }; +} + +function buildApiBotCommand(botId: string, command: GramJs.BotCommand): ApiBotCommand { + return { + botId, + ...omitVirtualClassFields(command), + }; +} + +export function buildApiBotMenuButton(menuButton: GramJs.TypeBotMenuButton): ApiBotMenuButton { + if (menuButton instanceof GramJs.BotMenuButton) { + return { + type: 'webApp', + text: menuButton.text, + url: menuButton.url, + }; + } + + return { + type: 'commands', + }; +} diff --git a/src/api/gramjs/apiBuilders/chats.ts b/src/api/gramjs/apiBuilders/chats.ts index d39e81d66..606d7c47e 100644 --- a/src/api/gramjs/apiBuilders/chats.ts +++ b/src/api/gramjs/apiBuilders/chats.ts @@ -149,7 +149,6 @@ function buildApiChatRestrictions(peerEntity: GramJs.TypeUser | GramJs.TypeChat) if (peerEntity instanceof GramJs.Chat) { Object.assign(restrictions, { isNotJoined: peerEntity.left, - isForbidden: peerEntity.kicked, }); } diff --git a/src/api/gramjs/apiBuilders/messages.ts b/src/api/gramjs/apiBuilders/messages.ts index 67ad93b59..fe566ea1e 100644 --- a/src/api/gramjs/apiBuilders/messages.ts +++ b/src/api/gramjs/apiBuilders/messages.ts @@ -924,6 +924,9 @@ function buildAction( text = senderId === currentUserId ? 'ActionYouScoredInGame' : 'ActionUserScoredInGame'; translationValues.push('%score%'); score = action.score; + } else if (action instanceof GramJs.MessageActionWebViewDataSent) { + text = 'Notification.WebAppSentData'; + translationValues.push(action.text); } else { text = 'ChatList.UnsupportedMessage'; } @@ -1067,6 +1070,22 @@ function buildReplyButtons(message: UniversalMessage): ApiReplyKeyboard | undefi }; } + if (button instanceof GramJs.KeyboardButtonSimpleWebView) { + return { + type: 'simpleWebView', + text, + url: button.url, + }; + } + + if (button instanceof GramJs.KeyboardButtonWebView) { + return { + type: 'webView', + text, + url: button.url, + }; + } + return { type: 'unsupported', text, diff --git a/src/api/gramjs/apiBuilders/misc.ts b/src/api/gramjs/apiBuilders/misc.ts index 0e327c405..3a7655f92 100644 --- a/src/api/gramjs/apiBuilders/misc.ts +++ b/src/api/gramjs/apiBuilders/misc.ts @@ -105,13 +105,15 @@ export function buildApiNotifyException( notifySettings: GramJs.TypePeerNotifySettings, peer: GramJs.TypePeer, serverTimeOffset: number, ) { const { - silent, muteUntil, showPreviews, sound, + silent, muteUntil, showPreviews, otherSound, } = notifySettings; + const hasSound = Boolean(otherSound && !(otherSound instanceof GramJs.NotificationSoundNone)); + return { chatId: getApiChatIdFromMtpPeer(peer), isMuted: silent || (typeof muteUntil === 'number' && getServerTime(serverTimeOffset) < muteUntil), - ...(sound === '' && { isSilent: true }), + ...(!hasSound && { isSilent: true }), ...(showPreviews !== undefined && { shouldShowPreviews: Boolean(showPreviews) }), }; } diff --git a/src/api/gramjs/apiBuilders/users.ts b/src/api/gramjs/apiBuilders/users.ts index 5a181634d..97046a95c 100644 --- a/src/api/gramjs/apiBuilders/users.ts +++ b/src/api/gramjs/apiBuilders/users.ts @@ -1,8 +1,9 @@ import { Api as GramJs } from '../../../lib/gramjs'; import { - ApiBotCommand, ApiUser, ApiUserStatus, ApiUserType, + ApiUser, ApiUserStatus, ApiUserType, } from '../../types'; import { buildApiPeerId } from './peers'; +import { buildApiBotInfo } from './bots'; export function buildApiUserFromFull(mtpUserFull: GramJs.users.UserFull): ApiUser { const { @@ -21,8 +22,7 @@ export function buildApiUserFromFull(mtpUserFull: GramJs.users.UserFull): ApiUse commonChatsCount, pinnedMessageId: pinnedMsgId, isBlocked: Boolean(blocked), - ...(botInfo && { botDescription: botInfo.description }), - ...(botInfo && botInfo.commands.length && { botCommands: buildApiBotCommands(user.id, botInfo) }), + ...(botInfo && { botInfo: buildApiBotInfo(botInfo) }), }, }; } @@ -57,6 +57,7 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined { ...(mtpUser.accessHash && { accessHash: String(mtpUser.accessHash) }), ...(avatarHash && { avatarHash }), ...(mtpUser.bot && mtpUser.botInlinePlaceholder && { botPlaceholder: mtpUser.botInlinePlaceholder }), + ...(mtpUser.bot && mtpUser.botAttachMenu && { isAttachMenuBot: mtpUser.botAttachMenu }), }; } @@ -87,14 +88,6 @@ export function buildApiUserStatus(mtpStatus?: GramJs.TypeUserStatus): ApiUserSt } } -function buildApiBotCommands(botId: string, botInfo: GramJs.BotInfo) { - return botInfo.commands.map(({ command, description }) => ({ - botId, - command, - description, - })) as ApiBotCommand[]; -} - export function buildApiUsersAndStatuses(mtpUsers: GramJs.TypeUser[]) { const userStatusesById: Record = {}; const users: ApiUser[] = []; diff --git a/src/api/gramjs/gramjsBuilders/index.ts b/src/api/gramjs/gramjsBuilders/index.ts index fc213a6bf..7bba98e90 100644 --- a/src/api/gramjs/gramjsBuilders/index.ts +++ b/src/api/gramjs/gramjsBuilders/index.ts @@ -18,6 +18,7 @@ import { ApiSendMessageAction, ApiSticker, ApiVideo, + ApiThemeParameters, } from '../../types'; import localDb from '../localDb'; import { pick } from '../../../util/iteratees'; @@ -466,6 +467,12 @@ export function buildSendMessageAction(action: ApiSendMessageAction) { return undefined; } +export function buildInputThemeParams(params: ApiThemeParameters) { + return new GramJs.DataJSON({ + data: JSON.stringify(params), + }); +} + export function buildMtpPeerId(id: string, type: 'user' | 'chat' | 'channel') { // Workaround for old-fashioned IDs stored locally if (typeof id === 'number') { diff --git a/src/api/gramjs/methods/bots.ts b/src/api/gramjs/methods/bots.ts index 41097aaa1..fa55173eb 100644 --- a/src/api/gramjs/methods/bots.ts +++ b/src/api/gramjs/methods/bots.ts @@ -1,16 +1,19 @@ import BigInt from 'big-integer'; import { Api as GramJs } from '../../../lib/gramjs'; -import { ApiChat, ApiUser } from '../../types'; +import { ApiChat, ApiThemeParameters, ApiUser } from '../../types'; import localDb from '../localDb'; import { invokeRequest } from './client'; -import { buildInputPeer, generateRandomBigInt } from '../gramjsBuilders'; +import { buildInputPeer, buildInputThemeParams, generateRandomBigInt } from '../gramjsBuilders'; import { buildApiUser } from '../apiBuilders/users'; -import { buildApiBotInlineMediaResult, buildApiBotInlineResult, buildBotSwitchPm } from '../apiBuilders/bots'; +import { + buildApiAttachMenuBot, buildApiBotInlineMediaResult, buildApiBotInlineResult, buildBotSwitchPm, +} from '../apiBuilders/bots'; import { buildApiChatFromPreview } from '../apiBuilders/chats'; import { addEntitiesWithPhotosToLocalDb, addUserToLocalDb, deserializeBytes } from '../helpers'; import { omitVirtualClassFields } from '../apiBuilders/helpers'; +import { buildCollectionByKey } from '../../../util/iteratees'; export function init() { } @@ -140,6 +143,132 @@ export async function startBot({ }), true); } +export async function requestWebView({ + isSilent, + peer, + bot, + url, + startParam, + replyToMessageId, + theme, + isFromBotMenu, +}: { + isSilent?: boolean; + peer: ApiChat | ApiUser; + bot: ApiUser; + url?: string; + startParam?: string; + replyToMessageId?: number; + theme?: ApiThemeParameters; + isFromBotMenu?: boolean; +}) { + const result = await invokeRequest(new GramJs.messages.RequestWebView({ + silent: isSilent || undefined, + peer: buildInputPeer(peer.id, peer.accessHash), + bot: buildInputPeer(bot.id, bot.accessHash), + replyToMsgId: replyToMessageId, + url, + startParam, + themeParams: theme ? buildInputThemeParams(theme) : undefined, + fromBotMenu: isFromBotMenu || undefined, + })); + + if (result instanceof GramJs.WebViewResultUrl) { + return { + url: result.url, + queryId: result.queryId.toString(), + }; + } + + return undefined; +} + +export async function requestSimpleWebView({ + bot, url, theme, +}: { + bot: ApiUser; + url: string; + theme?: ApiThemeParameters; +}) { + const result = await invokeRequest(new GramJs.messages.RequestSimpleWebView({ + url, + bot: buildInputPeer(bot.id, bot.accessHash), + themeParams: theme ? buildInputThemeParams(theme) : undefined, + })); + + return result?.url; +} + +export function prolongWebView({ + isSilent, + peer, + bot, + queryId, + replyToMessageId, +}: { + isSilent?: boolean; + peer: ApiChat | ApiUser; + bot: ApiUser; + queryId: string; + replyToMessageId?: number; +}) { + return invokeRequest(new GramJs.messages.ProlongWebView({ + silent: isSilent || undefined, + peer: buildInputPeer(peer.id, peer.accessHash), + bot: buildInputPeer(bot.id, bot.accessHash), + queryId: BigInt(queryId), + replyToMsgId: replyToMessageId, + })); +} + +export async function sendWebViewData({ + bot, buttonText, data, +}: { + bot: ApiUser; + buttonText: string; + data: string; +}) { + const randomId = generateRandomBigInt(); + await invokeRequest(new GramJs.messages.SendWebViewData({ + bot: buildInputPeer(bot.id, bot.accessHash), + buttonText, + data, + randomId, + }), true); +} + +export async function loadAttachMenuBots({ + hash, +}: { + hash?: string; +}) { + const result = await invokeRequest(new GramJs.messages.GetAttachMenuBots({ + hash: hash ? BigInt(hash) : undefined, + })); + + if (result instanceof GramJs.AttachMenuBots) { + addEntitiesWithPhotosToLocalDb(result.users); + return { + hash: result.hash.toString(), + bots: buildCollectionByKey(result.bots.map(buildApiAttachMenuBot), 'id'), + }; + } + return undefined; +} + +export function toggleBotInAttachMenu({ + bot, + isEnabled, +}: { + bot: ApiUser; + isEnabled: boolean; +}) { + return invokeRequest(new GramJs.messages.ToggleBotInAttachMenu({ + bot: buildInputPeer(bot.id, bot.accessHash), + enabled: isEnabled, + })); +} + function processInlineBotResult(queryId: string, results: GramJs.TypeBotInlineResult[]) { return results.map((result) => { if (result instanceof GramJs.BotInlineMediaResult) { diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index b66443665..3386607e2 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -64,6 +64,7 @@ export { export { answerCallbackButton, fetchTopInlineBots, fetchInlineBot, fetchInlineBotResults, sendInlineBotResult, startBot, + requestWebView, requestSimpleWebView, sendWebViewData, prolongWebView, loadAttachMenuBots, toggleBotInAttachMenu, } from './bots'; export { diff --git a/src/api/gramjs/methods/media.ts b/src/api/gramjs/methods/media.ts index 128734db3..68c1dfcf1 100644 --- a/src/api/gramjs/methods/media.ts +++ b/src/api/gramjs/methods/media.ts @@ -248,6 +248,9 @@ async function parseMedia( case ApiMediaFormat.Lottie: { return new Blob([data], { type: mimeType }); } + case ApiMediaFormat.Text: { + return data.toString(); + } case ApiMediaFormat.Progressive: { return data.buffer; } diff --git a/src/api/gramjs/methods/settings.ts b/src/api/gramjs/methods/settings.ts index f8fa14f6b..c1af376df 100644 --- a/src/api/gramjs/methods/settings.ts +++ b/src/api/gramjs/methods/settings.ts @@ -34,7 +34,7 @@ import { buildApiConfig } from '../apiBuilders/appConfig'; import { addEntitiesWithPhotosToLocalDb } from '../helpers'; const MAX_INT_32 = 2 ** 31 - 1; -const BETA_LANG_CODES = ['ar', 'fa', 'id', 'ko', 'uz']; +const BETA_LANG_CODES = ['ar', 'fa', 'id', 'ko', 'uz', 'en']; export function updateProfile({ firstName, diff --git a/src/api/gramjs/updater.ts b/src/api/gramjs/updater.ts index 787850ee6..007986f7c 100644 --- a/src/api/gramjs/updater.ts +++ b/src/api/gramjs/updater.ts @@ -44,11 +44,13 @@ import { buildApiNotifyException, buildPrivacyKey, buildPrivacyRules } from './a import { buildApiPhoto } from './apiBuilders/common'; import { buildApiGroupCall, - buildApiGroupCallParticipant, buildPhoneCall, + buildApiGroupCallParticipant, + buildPhoneCall, getGroupCallId, } from './apiBuilders/calls'; import { buildApiPeerId, getApiChatIdFromMtpPeer } from './apiBuilders/peers'; import { buildApiEmojiInteraction } from './apiBuilders/symbols'; +import { buildApiBotMenuButton } from './apiBuilders/bots'; type Update = ( (GramJs.TypeUpdate | GramJs.TypeUpdates) & { _entities?: (GramJs.TypeUser | GramJs.TypeChat)[] } @@ -915,6 +917,23 @@ export function updater(update: Update, originRequest?: GramJs.AnyRequest) { callId: update.phoneCallId.toString(), data: Array.from(update.data), }); + } else if (update instanceof GramJs.UpdateWebViewResultSent) { + const { queryId } = update; + + onUpdate({ + '@type': 'updateWebViewResultSent', + queryId: queryId.toString(), + }); + } else if (update instanceof GramJs.UpdateBotMenuButton) { + const { botId, button } = update; + + const id = buildApiPeerId(botId, 'user'); + + onUpdate({ + '@type': 'updateBotMenuButton', + botId: id, + button: buildApiBotMenuButton(button), + }); } else if (DEBUG) { const params = typeof update === 'object' && 'className' in update ? update.className : update; // eslint-disable-next-line no-console diff --git a/src/api/types/bots.ts b/src/api/types/bots.ts index b0bb29e4a..c5166c8e1 100644 --- a/src/api/types/bots.ts +++ b/src/api/types/bots.ts @@ -44,3 +44,22 @@ export interface ApiBotCommand { command: string; description: string; } + +type ApiBotMenuButtonCommands = { + type: 'commands'; +}; + +type ApiBotMenuButtonWebApp = { + type: 'webApp'; + text: string; + url: string; +}; + +export type ApiBotMenuButton = ApiBotMenuButtonWebApp | ApiBotMenuButtonCommands; + +export interface ApiBotInfo { + botId: string; + commands?: ApiBotCommand[]; + description: string; + menuButton: ApiBotMenuButton; +} diff --git a/src/api/types/media.ts b/src/api/types/media.ts index 2a19b9411..6cfb51b2d 100644 --- a/src/api/types/media.ts +++ b/src/api/types/media.ts @@ -6,6 +6,7 @@ export enum ApiMediaFormat { Lottie, Progressive, Stream, + Text, } export type ApiParsedMedia = string | Blob | ArrayBuffer; diff --git a/src/api/types/messages.ts b/src/api/types/messages.ts index c7b065ece..e7d272255 100644 --- a/src/api/types/messages.ts +++ b/src/api/types/messages.ts @@ -395,6 +395,18 @@ interface ApiKeyboardButtonUrl { url: string; } +interface ApiKeyboardButtonSimpleWebView { + type: 'simpleWebView'; + text: string; + url: string; +} + +interface ApiKeyboardButtonWebView { + type: 'webView'; + text: string; + url: string; +} + interface ApiKeyboardButtonCallback { type: 'callback'; text: string; @@ -407,7 +419,7 @@ interface ApiKeyboardButtonRequestPoll { isQuiz?: boolean; } -interface ApiKeyboardButtonSwitchInline { +interface ApiKeyboardButtonSwitchBotInline { type: 'switchBotInline'; text: string; query: string; @@ -426,8 +438,10 @@ export type ApiKeyboardButton = ( | ApiKeyboardButtonUrl | ApiKeyboardButtonCallback | ApiKeyboardButtonRequestPoll - | ApiKeyboardButtonSwitchInline + | ApiKeyboardButtonSwitchBotInline | ApiKeyboardButtonUserProfile + | ApiKeyboardButtonWebView + | ApiKeyboardButtonSimpleWebView ); export type ApiKeyboardButtons = ApiKeyboardButton[][]; @@ -448,6 +462,15 @@ export type ApiSendMessageAction = { type: 'cancel' | 'typing' | 'recordAudio' | 'chooseSticker' | 'playingGame'; }; +export type ApiThemeParameters = { + bg_color: string; + text_color: string; + hint_color: string; + link_color: string; + button_color: string; + button_text_color: string; +}; + export const MAIN_THREAD_ID = -1; // `Symbol` can not be transferred from worker diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index 662f6f7a5..095aa4c6d 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -22,6 +22,7 @@ import { import { ApiGroupCall, ApiPhoneCall, } from './calls'; +import { ApiBotMenuButton } from './bots'; export type ApiUpdateReady = { '@type': 'updateApiReady'; @@ -486,6 +487,17 @@ export type ApiUpdatePhoneCallConnectionState = { connectionState: RTCPeerConnectionState; }; +export type ApiUpdateWebViewResultSent = { + '@type': 'updateWebViewResultSent'; + queryId: string; +}; + +export type ApiUpdateBotMenuButton = { + '@type': 'updateBotMenuButton'; + botId: string; + button: ApiBotMenuButton; +}; + export type ApiUpdate = ( ApiUpdateReady | ApiUpdateSession | ApiUpdateAuthorizationState | ApiUpdateAuthorizationError | ApiUpdateConnectionState | ApiUpdateCurrentUser | @@ -501,14 +513,14 @@ export type ApiUpdate = ( ApiUpdateFavoriteStickers | ApiUpdateStickerSet | ApiUpdateNewScheduledMessage | ApiUpdateScheduledMessageSendSucceeded | ApiUpdateScheduledMessage | ApiUpdateDeleteScheduledMessages | ApiUpdateResetMessages | - ApiUpdateTwoFaError | ApiUpdateTwoFaStateWaitCode | + ApiUpdateTwoFaError | ApiUpdateTwoFaStateWaitCode | ApiUpdateWebViewResultSent | ApiUpdateNotifySettings | ApiUpdateNotifyExceptions | ApiUpdatePeerBlocked | ApiUpdatePrivacy | ApiUpdateServerTimeOffset | ApiUpdateShowInvite | ApiUpdateMessageReactions | ApiUpdateGroupCallParticipants | ApiUpdateGroupCallConnection | ApiUpdateGroupCall | ApiUpdateGroupCallStreams | ApiUpdateGroupCallConnectionState | ApiUpdateGroupCallLeavePresentation | ApiUpdateGroupCallChatId | ApiUpdatePendingJoinRequests | ApiUpdatePaymentVerificationNeeded | ApiUpdatePaymentStateCompleted | ApiUpdatePhoneCall | ApiUpdatePhoneCallSignalingData | ApiUpdatePhoneCallMediaState | - ApiUpdatePhoneCallConnectionState + ApiUpdatePhoneCallConnectionState | ApiUpdateBotMenuButton ); export type OnApiUpdate = (update: ApiUpdate) => void; diff --git a/src/api/types/users.ts b/src/api/types/users.ts index 6a15616af..e5e213acd 100644 --- a/src/api/types/users.ts +++ b/src/api/types/users.ts @@ -1,5 +1,5 @@ -import { ApiPhoto } from './messages'; -import { ApiBotCommand } from './bots'; +import { ApiDocument, ApiPhoto } from './messages'; +import { ApiBotInfo } from './bots'; export interface ApiUser { id: string; @@ -24,6 +24,7 @@ export interface ApiUser { isFullyLoaded: boolean; }; fakeType?: ApiFakeType; + isAttachMenuBot?: boolean; // Obtained from GetFullUser / UserFullInfo fullInfo?: ApiUserFullInfo; @@ -33,9 +34,8 @@ export interface ApiUserFullInfo { isBlocked?: boolean; bio?: string; commonChatsCount?: number; - botDescription?: string; pinnedMessageId?: number; - botCommands?: ApiBotCommand[]; + botInfo?: ApiBotInfo; } export type ApiFakeType = 'fake' | 'scam'; @@ -50,3 +50,14 @@ export interface ApiUserStatus { wasOnline?: number; expires?: number; } + +export interface ApiAttachMenuBot { + id: string; + shortName: string; + icons: ApiAttachMenuBotIcon[]; +} + +export interface ApiAttachMenuBotIcon { + name: string; + document: ApiDocument; +} diff --git a/src/assets/fonts/icomoon.woff b/src/assets/fonts/icomoon.woff index fce9bb749e3ed02a281f9a8770c56c20e87c698c..05dc2abc6b574ba7ff5cc36eca190a1a24e9d20f 100644 GIT binary patch delta 868 zcmYLIO-vI}5T4m>3x>aL>6UH~-R|-uv_i8$OIksrrX)bA8VW`XSfSDw3Ph^li4u(- zlti)|Kw=~jF<4@vO+28{#BlZk7&XRt;O@zj{5TIVyv+Az-uGs{w{LcLYL%P&%GpLj zfdC-TSdt-&`P3SqKE9YgS#BHq)Dw>k0bq>yHim-sRi!U7%((Zo&fJH&umAQn;#L6W z6AWj?>sMmYNH4%b9p!i!rU1Ak!!10M>>uo*yoN<9 ziJoS7a_Q#Xcx03cUtXj_c^9wn$3P?=C5{B}9>Yx2p}#|e!}JhKFu57RS-l0DaW*}T zi_!-v)Ug>%V4{F$WLZ5Qs(t9Gkb6RZUYlVDmO$G=N);y}gawd?3oDhzhd} zOsN{31g@Pg{2HHxE?QVk8M(M;s#023iF;OCJiID$t}w3>*k!m)bX ztrM+`uhv+4ZOy&_#YB>c{F=pL(HwGfh4XTiyVF(PEITx!%8%LoDhADVPBa_eW#*ZT z8Hkd_EFmiX0$K5SJrlv;L$Aj#2ts914rXPm*~N8M(TXXOsc33-$fT2>PwKLxslwIi z4&3*t^>!pZ!OoeC#@tNfvPH6)jqPajD((SaaKhvD?FyHNN{dexW-B?Q>j=7zN3^v4 z>7v2HgL7w{U#Hd-f*0s*pLe9U|GmEP%!_mB!}HyoBTU;aLI;7xP6F(EKlumgM+r4iNzqX zJwQGO6bq#1RHgyNjxaDR`~$*=+ZU^6q$Z{?Ff2X*RAUCh@}DHNGk}6Xu^&Lb3J9|w zVTsPjEvaB&SR%u~z*GXlS&U9sa`Kab>g<+H02*oo#tNEJxrr4(iR%D@!K+@n@Vd*@w}aamMBuj4NksPF#3{W%J(ID_E34 zeg$Ho4foXJ`E9;3aI*jf7#Qwlyvl*mlVg^dGtS*SWm!Cvs5MZ!8;HY!I24GrftX>l f$Vvl7A>Su{tNauE&jqXt_y-Qcxtrrw9byCk^Q~Qy diff --git a/src/assets/fonts/icomoon.woff2 b/src/assets/fonts/icomoon.woff2 index cdaca717b990d2b2969b6a6e540a1cda1a95c8de..160709892780cf5d052bda7923081b359241fad6 100644 GIT binary patch literal 20616 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 nh=_=Yh=_=Yn328wZRv8=i+1#)|aAR24nsTn)q} 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 = ({ const lang = useLang(); - const senderTitle = message?.forwardInfo?.hiddenUserName || (sender && getSenderTitle(lang, sender)); + const senderTitle = sender ? getSenderTitle(lang, sender) : message?.forwardInfo?.hiddenUserName; return (
= (props) => { + const { bot } = props; + const BotAttachModal = useModuleLoader(Bundles.Extra, 'BotAttachModal', !bot); + + // eslint-disable-next-line react/jsx-props-no-spreading + return BotAttachModal ? : undefined; +}; + +export default memo(BotAttachModalAsync); diff --git a/src/components/main/BotAttachModal.tsx b/src/components/main/BotAttachModal.tsx new file mode 100644 index 000000000..031d99cf3 --- /dev/null +++ b/src/components/main/BotAttachModal.tsx @@ -0,0 +1,34 @@ +import React, { FC } from '../../lib/teact/teact'; +import { getActions } from '../../global'; + +import { ApiUser } from '../../api/types'; + +import useLang from '../../hooks/useLang'; + +import ConfirmDialog from '../ui/ConfirmDialog'; + +export type OwnProps = { + bot?: ApiUser; +}; + +const BotAttachModal: FC = ({ + bot, +}) => { + const { closeBotAttachRequestModal, confirmBotAttachRequest } = getActions(); + + const lang = useLang(); + + const name = bot?.firstName; + + return ( + + ); +}; + +export default BotAttachModal; diff --git a/src/components/main/BotTrustModal.async.tsx b/src/components/main/BotTrustModal.async.tsx new file mode 100644 index 000000000..b5e4c2b91 --- /dev/null +++ b/src/components/main/BotTrustModal.async.tsx @@ -0,0 +1,16 @@ +import React, { FC, memo } from '../../lib/teact/teact'; +import { Bundles } from '../../util/moduleLoader'; + +import { OwnProps } from './BotTrustModal'; + +import useModuleLoader from '../../hooks/useModuleLoader'; + +const BotTrustModalAsync: FC = (props) => { + const { bot } = props; + const BotTrustModal = useModuleLoader(Bundles.Extra, 'BotTrustModal', !bot); + + // eslint-disable-next-line react/jsx-props-no-spreading + return BotTrustModal ? : undefined; +}; + +export default memo(BotTrustModalAsync); diff --git a/src/components/main/BotTrustModal.tsx b/src/components/main/BotTrustModal.tsx new file mode 100644 index 000000000..f26152ffd --- /dev/null +++ b/src/components/main/BotTrustModal.tsx @@ -0,0 +1,47 @@ +import React, { FC, memo, useCallback } from '../../lib/teact/teact'; +import { getActions } from '../../global'; + +import { ApiUser } from '../../api/types'; + +import { getUserFullName } from '../../global/helpers'; +import renderText from '../common/helpers/renderText'; + +import useLang from '../../hooks/useLang'; +import usePrevious from '../../hooks/usePrevious'; + +import ConfirmDialog from '../ui/ConfirmDialog'; + +export type OwnProps = { + bot?: ApiUser; + type?: 'game' | 'webApp'; +}; + +const BotTrustModal: FC = ({ bot, type }) => { + const { cancelBotTrustRequest, markBotTrusted } = getActions(); + const lang = useLang(); + // Keep props a little bit longer, to show correct text on closing animation + const previousBot = usePrevious(bot, false); + const previousType = usePrevious(type, false); + const currentBot = bot || previousBot; + const currentType = type || previousType; + + const handleBotTrustAccept = useCallback(() => { + markBotTrusted({ botId: bot!.id }); + }, [markBotTrusted, bot]); + + const title = currentType === 'game' ? lang('AppName') : lang('BotOpenPageTitle'); + const text = currentType === 'game' ? lang('BotPermissionGameAlert', getUserFullName(currentBot)) + : lang('BotOpenPageMessage', getUserFullName(currentBot)); + + return ( + + ); +}; + +export default memo(BotTrustModal); diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index c58f85b35..d8609f365 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -4,7 +4,9 @@ import React, { import { getActions, withGlobal } from '../../global'; import { LangCode } from '../../types'; -import { ApiMessage, ApiUpdateAuthorizationStateType, ApiUpdateConnectionStateType } from '../../api/types'; +import { + ApiChat, ApiMessage, ApiUpdateAuthorizationStateType, ApiUpdateConnectionStateType, +} from '../../api/types'; import { GlobalState } from '../../global/types'; import '../../global/actions/all'; @@ -54,10 +56,14 @@ import ActiveCallHeader from '../calls/ActiveCallHeader.async'; import PhoneCall from '../calls/phone/PhoneCall.async'; import NewContactModal from './NewContactModal.async'; import RatePhoneCallModal from '../calls/phone/RatePhoneCallModal.async'; +import WebAppModal from './WebAppModal.async'; +import BotTrustModal from './BotTrustModal.async'; +import BotAttachModal from './BotAttachModal.async'; import './Main.scss'; type StateProps = { + chat?: ApiChat; connectionState?: ApiUpdateConnectionStateType; authState?: ApiUpdateAuthorizationStateType; lastSyncTime?: number; @@ -84,6 +90,9 @@ type StateProps = { openedGame?: GlobalState['openedGame']; gameTitle?: string; isRatePhoneCallModalOpen?: boolean; + webApp?: GlobalState['webApp']; + botTrustRequest?: GlobalState['botTrustRequest']; + botAttachRequest?: GlobalState['botAttachRequest']; }; const NOTIFICATION_INTERVAL = 1000; @@ -120,6 +129,9 @@ const Main: FC = ({ openedGame, gameTitle, isRatePhoneCallModalOpen, + botTrustRequest, + botAttachRequest, + webApp, }) => { const { sync, @@ -138,6 +150,7 @@ const Main: FC = ({ openStickerSetShortName, checkVersionNotification, loadAppConfig, + loadAttachMenuBots, } = getActions(); if (DEBUG && !DEBUG_isLogged) { @@ -163,10 +176,11 @@ const Main: FC = ({ loadNotificationExceptions(); loadTopInlineBots(); loadEmojiKeywords({ language: BASE_EMOJI_KEYWORD_LANG }); + loadAttachMenuBots(); } }, [ lastSyncTime, loadAnimatedEmojis, loadEmojiKeywords, loadNotificationExceptions, loadNotificationSettings, - loadTopInlineBots, updateIsOnline, loadAvailableReactions, loadAppConfig, + loadTopInlineBots, updateIsOnline, loadAvailableReactions, loadAppConfig, loadAttachMenuBots, ]); // Language-based API calls @@ -366,10 +380,13 @@ const Main: FC = ({ isByPhoneNumber={newContactByPhoneNumber} /> + + +
); }; @@ -433,6 +450,9 @@ export default memo(withGlobal( openedGame, gameTitle, isRatePhoneCallModalOpen: Boolean(global.ratingPhoneCall), + botTrustRequest: global.botTrustRequest, + botAttachRequest: global.botAttachRequest, + webApp: global.webApp, }; }, )(Main)); diff --git a/src/components/main/WebAppModal.async.tsx b/src/components/main/WebAppModal.async.tsx new file mode 100644 index 000000000..9ffb9494b --- /dev/null +++ b/src/components/main/WebAppModal.async.tsx @@ -0,0 +1,16 @@ +import React, { FC, memo } from '../../lib/teact/teact'; +import { Bundles } from '../../util/moduleLoader'; + +import { OwnProps } from './WebAppModal'; + +import useModuleLoader from '../../hooks/useModuleLoader'; + +const WebAppModalAsync: FC = (props) => { + const { webApp } = props; + const WebAppModal = useModuleLoader(Bundles.Extra, 'WebAppModal', !webApp); + + // eslint-disable-next-line react/jsx-props-no-spreading + return WebAppModal ? : undefined; +}; + +export default memo(WebAppModalAsync); diff --git a/src/components/main/WebAppModal.scss b/src/components/main/WebAppModal.scss new file mode 100644 index 000000000..08436280d --- /dev/null +++ b/src/components/main/WebAppModal.scss @@ -0,0 +1,69 @@ +.WebAppModal { + .modal-header { + border-bottom: 1px solid var(--color-dividers); + padding: 0.5rem; + } + + .modal-dialog { + height: 75%; + justify-content: center; + border: none; + box-shadow: none; + margin: 0; + overflow: hidden; + } + + .modal-content { + display: flex; + flex-direction: column; + overflow: hidden; + padding: 0; + border-bottom-right-radius: var(--border-radius-default); + border-bottom-left-radius: var(--border-radius-default); + } + + .web-app-frame { + width: 100%; + height: 100%; + border: 0; + + &.with-button { + height: calc(100% - 56px); + } + } + + .web-app-button { + position: absolute; + bottom: 0; + border-radius: 0; + transform: translateY(100%); + transition: 0.25s ease-in-out transform; + + &.visible { + transform: translateY(0); + } + + &.hidden { + visibility: hidden; + } + } + + .Spinner { + position: absolute; + right: 1rem; + } + + @media (max-width: 600px) { + .modal-dialog { + background-color: var(--color-background); + border-radius: 0; + height: 100%; + max-width: 100% !important; + } + + .modal-content { + max-height: none; + border-radius: 0; + } + } +} diff --git a/src/components/main/WebAppModal.tsx b/src/components/main/WebAppModal.tsx new file mode 100644 index 000000000..c79d9bfa6 --- /dev/null +++ b/src/components/main/WebAppModal.tsx @@ -0,0 +1,292 @@ +import React, { + FC, memo, useCallback, useEffect, useMemo, useRef, useState, +} from '../../lib/teact/teact'; +import { getActions, withGlobal } from '../../global'; + +import { ApiChat } from '../../api/types'; +import { GlobalState } from '../../global/types'; +import { ThemeKey } from '../../types'; + +import windowSize from '../../util/windowSize'; +import { IS_SINGLE_COLUMN_LAYOUT } from '../../util/environment'; +import { selectCurrentChat, selectTheme } from '../../global/selectors'; +import buildClassName from '../../util/buildClassName'; +import { extractCurrentThemeParams, validateHexColor } from '../../util/themeStyle'; + +import useInterval from '../../hooks/useInterval'; +import useLang from '../../hooks/useLang'; +import useOnChange from '../../hooks/useOnChange'; +import useWebAppFrame, { WebAppInboundEvent } from './hooks/useWebAppFrame'; +import usePrevious from '../../hooks/usePrevious'; + +import Modal from '../ui/Modal'; +import Button from '../ui/Button'; +import DropdownMenu from '../ui/DropdownMenu'; +import MenuItem from '../ui/MenuItem'; +import Spinner from '../ui/Spinner'; + +import './WebAppModal.scss'; + +type WebAppButton = { + isVisible: boolean; + isActive: boolean; + text: string; + color: string; + textColor: string; + isProgressVisible: boolean; +}; + +export type OwnProps = { + webApp?: GlobalState['webApp']; +}; + +type StateProps = { + isInstalled?: boolean; + chat?: ApiChat; + theme?: ThemeKey; +}; + +const MAIN_BUTTON_ANIMATION_TIME = 250; +const PROLONG_INTERVAL = 45000; // 45s +const ANIMATION_WAIT = 400; + +const WebAppModal: FC = ({ + webApp, + chat, + isInstalled, + theme, +}) => { + const { + closeWebApp, sendWebViewData, prolongWebView, toggleBotInAttachMenu, + } = getActions(); + const [mainButton, setMainButton] = useState(); + const lang = useLang(); + const { + url, bot, buttonText, queryId, + } = webApp || {}; + const isOpen = Boolean(url); + const isSimple = !queryId; + + const handleEvent = useCallback((event: WebAppInboundEvent) => { + const { eventType } = event; + if (eventType === 'web_app_close') { + closeWebApp(); + } + + if (eventType === 'web_app_data_send') { + const { eventData } = event; + closeWebApp(); + sendWebViewData({ + bot: bot!, + buttonText: buttonText!, + data: eventData.data, + }); + } + + if (eventType === 'web_app_setup_main_button') { + const { eventData } = event; + const themeParams = extractCurrentThemeParams(); + // Validate colors if they are present + const color = !eventData.color || validateHexColor(eventData.color) ? eventData.color + : themeParams.button_color; + const textColor = !eventData.text_color || validateHexColor(eventData.text_color) ? eventData.text_color + : themeParams.text_color; + setMainButton({ + isVisible: eventData.is_visible && Boolean(eventData.text?.trim().length), + isActive: eventData.is_active, + text: eventData.text || '', + color, + textColor, + isProgressVisible: eventData.is_progress_visible, + }); + } + }, [bot, buttonText, closeWebApp, sendWebViewData]); + + const { + ref, reloadFrame, sendEvent, sendViewport, sendTheme, + } = useWebAppFrame(isOpen, isSimple, handleEvent); + + const shouldShowMainButton = mainButton?.isVisible && mainButton.text.trim().length > 0; + + useInterval(() => { + prolongWebView({ + bot: bot!, + queryId: queryId!, + peer: chat!, + }); + }, queryId ? PROLONG_INTERVAL : undefined, true); + + const handleMainButtonClick = useCallback(() => { + sendEvent({ + eventType: 'main_button_pressed', + }); + }, [sendEvent]); + + const handleRefreshClick = useCallback(() => { + reloadFrame(webApp!.url); + }, [reloadFrame, webApp]); + + // Notify view that height changed + useOnChange(() => { + setTimeout(() => { + sendViewport(); + }, ANIMATION_WAIT); + }, [mainButton?.isVisible, sendViewport]); + + // Notify view that theme changed + useOnChange(() => { + setTimeout(() => { + sendTheme(); + }, ANIMATION_WAIT); + }, [theme, sendTheme]); + + // Prevent refresh when rotating device + useEffect(() => { + if (!isOpen) return undefined; + windowSize.disableRefresh(); + + return () => { + windowSize.enableRefresh(); + }; + }, [isOpen]); + + const handleToggleClick = useCallback(() => { + toggleBotInAttachMenu({ + botId: bot!.id, + isEnabled: !isInstalled, + }); + }, [bot, isInstalled, toggleBotInAttachMenu]); + + const MoreMenuButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => { + return ({ onTrigger, isOpen: isMenuOpen }) => ( + + ); + }, []); + + const header = useMemo(() => { + return ( +
+ +
{bot?.firstName}
+ + {lang('WebApp.ReloadPage')} + {bot?.isAttachMenuBot && ( + + {lang(isInstalled ? 'WebApp.RemoveBot' : 'WebApp.AddToAttachmentAdd')} + + )} + +
+ ); + }, [ + MoreMenuButton, bot, closeWebApp, handleRefreshClick, handleToggleClick, lang, isInstalled, + ]); + + const prevMainButtonColor = usePrevious(mainButton?.color, true); + const prevMainButtonTextColor = usePrevious(mainButton?.textColor, true); + const prevMainButtonIsActive = usePrevious(mainButton && Boolean(mainButton.isActive), true); + const prevMainButtonText = usePrevious(mainButton?.text, true); + + const mainButtonCurrentColor = mainButton?.color || prevMainButtonColor; + const mainButtonCurrentTextColor = mainButton?.textColor || prevMainButtonTextColor; + const mainButtonCurrentIsActive = mainButton?.isActive !== undefined ? mainButton.isActive : prevMainButtonIsActive; + const mainButtonCurrentText = mainButton?.text || prevMainButtonText; + + useEffect(() => { + if (!isOpen) setMainButton(undefined); + }, [isOpen]); + + const [shouldDecreaseWebFrameSize, setShouldDecreaseWebFrameSize] = useState(false); + const [shouldHideButton, setShouldHideButton] = useState(true); + + const buttonChangeTimeout = useRef>(); + + useEffect(() => { + if (buttonChangeTimeout.current) clearTimeout(buttonChangeTimeout.current); + if (!shouldShowMainButton) { + setShouldDecreaseWebFrameSize(false); + buttonChangeTimeout.current = setTimeout(() => { + setShouldHideButton(true); + }, MAIN_BUTTON_ANIMATION_TIME); + } else { + setShouldHideButton(false); + buttonChangeTimeout.current = setTimeout(() => { + setShouldDecreaseWebFrameSize(true); + }, MAIN_BUTTON_ANIMATION_TIME); + } + }, [setShouldDecreaseWebFrameSize, shouldShowMainButton]); + + return ( + + {isOpen && ( + <> +