Support Bot Apps (#3041)

This commit is contained in:
Alexander Zinchuk 2023-04-25 17:24:03 +04:00
parent ac28fe0162
commit 920b046d2e
25 changed files with 415 additions and 60 deletions

View File

@ -3,11 +3,13 @@ import type {
ApiAttachBot, ApiAttachBot,
ApiAttachBotIcon, ApiAttachBotIcon,
ApiAttachMenuPeerType, ApiAttachMenuPeerType,
ApiBotApp,
ApiBotCommand, ApiBotCommand,
ApiBotInfo, ApiBotInfo,
ApiBotInlineMediaResult, ApiBotInlineMediaResult,
ApiBotInlineResult, ApiBotInlineResult,
ApiBotInlineSwitchPm, ApiBotInlineSwitchPm,
ApiBotInlineSwitchWebview,
ApiBotMenuButton, ApiBotMenuButton,
ApiInlineResultType, ApiInlineResultType,
} from '../../types'; } from '../../types';
@ -62,6 +64,10 @@ export function buildBotSwitchPm(switchPm?: GramJs.InlineBotSwitchPM) {
return switchPm ? pick(switchPm, ['text', 'startParam']) as ApiBotInlineSwitchPm : undefined; return switchPm ? pick(switchPm, ['text', 'startParam']) as ApiBotInlineSwitchPm : undefined;
} }
export function buildBotSwitchWebview(switchWebview?: GramJs.InlineBotWebView) {
return switchWebview ? pick(switchWebview, ['text', 'url']) as ApiBotInlineSwitchWebview : undefined;
}
export function buildApiAttachBot(bot: GramJs.AttachMenuBot): ApiAttachBot { export function buildApiAttachBot(bot: GramJs.AttachMenuBot): ApiAttachBot {
return { return {
id: bot.botId.toString(), id: bot.botId.toString(),
@ -138,3 +144,26 @@ export function buildApiBotMenuButton(menuButton?: GramJs.TypeBotMenuButton): Ap
type: 'commands', type: 'commands',
}; };
} }
export function buildApiBotApp(botApp: GramJs.messages.BotApp): ApiBotApp | undefined {
const { app, inactive, requestWriteAccess } = botApp;
if (app instanceof GramJs.BotAppNotModified) return undefined;
const {
id, accessHash, title, description, shortName, photo, document,
} = app;
const apiPhoto = photo instanceof GramJs.Photo ? buildApiPhoto(photo) : undefined;
const apiDocument = document instanceof GramJs.Document ? buildApiDocument(document) : undefined;
return {
id: id.toString(),
accessHash: accessHash.toString(),
title,
description,
shortName,
photo: apiPhoto,
document: apiDocument,
isInactive: inactive,
shouldRequestWriteAccess: requestWriteAccess,
};
}

View File

@ -1145,6 +1145,15 @@ function buildReplyButtons(message: UniversalMessage, shouldSkipBuyButton?: bool
}]], }]],
}; };
} }
if (media.webpage.type === 'telegram_botapp') {
return {
inlineButtons: [[{
type: 'url',
text: 'Open App',
url: media.webpage.url,
}]],
};
}
} }
return undefined; return undefined;

View File

@ -23,6 +23,7 @@ import type {
ApiChatReactions, ApiChatReactions,
ApiReaction, ApiReaction,
ApiFormattedText, ApiFormattedText,
ApiBotApp,
} from '../../types'; } from '../../types';
import { import {
ApiMessageEntityTypes, ApiMessageEntityTypes,
@ -606,3 +607,10 @@ export function buildInputTextWithEntities(formatted: ApiFormattedText) {
entities: formatted.entities?.map(buildMtpMessageEntity) || [], entities: formatted.entities?.map(buildMtpMessageEntity) || [],
}); });
} }
export function buildInputBotApp(app: ApiBotApp) {
return new GramJs.InputBotAppID({
id: BigInt(app.id),
accessHash: BigInt(app.accessHash),
});
}

View File

@ -2,15 +2,24 @@ import BigInt from 'big-integer';
import { Api as GramJs } from '../../../lib/gramjs'; import { Api as GramJs } from '../../../lib/gramjs';
import type { import type {
ApiBotApp,
ApiChat, ApiThemeParameters, ApiUser, OnApiUpdate, ApiChat, ApiThemeParameters, ApiUser, OnApiUpdate,
} from '../../types'; } from '../../types';
import localDb from '../localDb'; import localDb from '../localDb';
import { WEB_APP_PLATFORM } from '../../../config';
import { invokeRequest } from './client'; import { invokeRequest } from './client';
import { buildInputPeer, buildInputThemeParams, generateRandomBigInt } from '../gramjsBuilders'; import {
buildInputBotApp, buildInputEntity, buildInputPeer, buildInputThemeParams, generateRandomBigInt,
} from '../gramjsBuilders';
import { buildApiUser } from '../apiBuilders/users'; import { buildApiUser } from '../apiBuilders/users';
import { import {
buildApiAttachBot, buildApiBotInlineMediaResult, buildApiBotInlineResult, buildBotSwitchPm, buildApiAttachBot,
buildApiBotApp,
buildApiBotInlineMediaResult,
buildApiBotInlineResult,
buildBotSwitchPm,
buildBotSwitchWebview,
} from '../apiBuilders/bots'; } from '../apiBuilders/bots';
import { buildApiChatFromPreview } from '../apiBuilders/chats'; import { buildApiChatFromPreview } from '../apiBuilders/chats';
import { addEntitiesWithPhotosToLocalDb, addUserToLocalDb, deserializeBytes } from '../helpers'; import { addEntitiesWithPhotosToLocalDb, addUserToLocalDb, deserializeBytes } from '../helpers';
@ -102,6 +111,7 @@ export async function fetchInlineBotResults({
help: bot.botPlaceholder, help: bot.botPlaceholder,
nextOffset: getInlineBotResultsNextOffset(bot.usernames![0].username, result.nextOffset), nextOffset: getInlineBotResultsNextOffset(bot.usernames![0].username, result.nextOffset),
switchPm: buildBotSwitchPm(result.switchPm), switchPm: buildBotSwitchPm(result.switchPm),
switchWebview: buildBotSwitchWebview(result.switchWebview),
users: result.users.map(buildApiUser).filter(Boolean), users: result.users.map(buildApiUser).filter(Boolean),
results: processInlineBotResult(String(result.queryId), result.results), results: processInlineBotResult(String(result.queryId), result.results),
cacheTime: result.cacheTime, cacheTime: result.cacheTime,
@ -184,7 +194,7 @@ export async function requestWebView({
startParam, startParam,
themeParams: theme ? buildInputThemeParams(theme) : undefined, themeParams: theme ? buildInputThemeParams(theme) : undefined,
fromBotMenu: isFromBotMenu || undefined, fromBotMenu: isFromBotMenu || undefined,
platform: 'weba', platform: WEB_APP_PLATFORM,
...(threadId && { topMsgId: threadId }), ...(threadId && { topMsgId: threadId }),
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }), ...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
})); }));
@ -210,7 +220,53 @@ export async function requestSimpleWebView({
url, url,
bot: buildInputPeer(bot.id, bot.accessHash), bot: buildInputPeer(bot.id, bot.accessHash),
themeParams: theme ? buildInputThemeParams(theme) : undefined, themeParams: theme ? buildInputThemeParams(theme) : undefined,
platform: 'weba', platform: WEB_APP_PLATFORM,
}));
return result?.url;
}
export async function fetchBotApp({
bot,
appName,
}: {
bot: ApiUser;
appName: string;
}) {
const result = await invokeRequest(new GramJs.messages.GetBotApp({
app: new GramJs.InputBotAppShortName({
botId: buildInputEntity(bot.id, bot.accessHash) as GramJs.InputUser,
shortName: appName,
}),
}));
if (!result || result instanceof GramJs.BotAppNotModified) {
return undefined;
}
return buildApiBotApp(result);
}
export async function requestAppWebView({
peer,
app,
startParam,
theme,
isWriteAllowed,
}: {
peer: ApiChat | ApiUser;
app: ApiBotApp;
startParam?: string;
theme?: ApiThemeParameters;
isWriteAllowed?: boolean;
}) {
const result = await invokeRequest(new GramJs.messages.RequestAppWebView({
peer: buildInputPeer(peer.id, peer.accessHash),
app: buildInputBotApp(app),
startParam,
themeParams: theme ? buildInputThemeParams(theme) : undefined,
platform: WEB_APP_PLATFORM,
writeAllowed: isWriteAllowed || undefined,
})); }));
return result?.url; return result?.url;

View File

@ -71,8 +71,8 @@ export {
export { export {
answerCallbackButton, fetchTopInlineBots, fetchInlineBot, fetchInlineBotResults, sendInlineBotResult, startBot, answerCallbackButton, fetchTopInlineBots, fetchInlineBot, fetchInlineBotResults, sendInlineBotResult, startBot,
requestWebView, requestSimpleWebView, sendWebViewData, prolongWebView, loadAttachBots, toggleAttachBot, requestWebView, requestSimpleWebView, sendWebViewData, prolongWebView, loadAttachBots, toggleAttachBot, fetchBotApp,
requestBotUrlAuth, requestLinkUrlAuth, acceptBotUrlAuth, acceptLinkUrlAuth, loadAttachBot, requestBotUrlAuth, requestLinkUrlAuth, acceptBotUrlAuth, acceptLinkUrlAuth, loadAttachBot, requestAppWebView,
} from './bots'; } from './bots';
export { export {

View File

@ -43,6 +43,11 @@ export interface ApiBotInlineSwitchPm {
startParam: string; startParam: string;
} }
export interface ApiBotInlineSwitchWebview {
text: string;
url: string;
}
export interface ApiBotCommand { export interface ApiBotCommand {
botId: string; botId: string;
command: string; command: string;

View File

@ -637,6 +637,18 @@ export type ApiThemeParameters = {
secondary_bg_color: string; secondary_bg_color: string;
}; };
export type ApiBotApp = {
id: string;
accessHash: string;
title: string;
shortName: string;
description: string;
photo?: ApiPhoto;
document?: ApiDocument;
isInactive?: boolean;
shouldRequestWriteAccess?: boolean;
};
export const MAIN_THREAD_ID = -1; export const MAIN_THREAD_ID = -1;
// `Symbol` can not be transferred from worker // `Symbol` can not be transferred from worker

View File

@ -1,7 +1,9 @@
import type { FC } from '../../lib/teact/teact'; import React, {
import React, { memo, useCallback } from '../../lib/teact/teact'; memo, useCallback, useMemo, useState,
} from '../../lib/teact/teact';
import { getActions } from '../../global'; import { getActions } from '../../global';
import type { FC } from '../../lib/teact/teact';
import type { ApiUser } from '../../api/types'; import type { ApiUser } from '../../api/types';
import { getUserFullName } from '../../global/helpers'; import { getUserFullName } from '../../global/helpers';
@ -10,15 +12,20 @@ import renderText from '../common/helpers/renderText';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
import usePrevious from '../../hooks/usePrevious'; import usePrevious from '../../hooks/usePrevious';
import Checkbox from '../ui/Checkbox';
import ConfirmDialog from '../ui/ConfirmDialog'; import ConfirmDialog from '../ui/ConfirmDialog';
export type OwnProps = { export type OwnProps = {
bot?: ApiUser; bot?: ApiUser;
type?: 'game' | 'webApp'; type?: 'game' | 'webApp' | 'botApp';
shouldRequestWriteAccess?: boolean;
}; };
const BotTrustModal: FC<OwnProps> = ({ bot, type }) => { const BotTrustModal: FC<OwnProps> = ({ bot, type, shouldRequestWriteAccess }) => {
const { cancelBotTrustRequest, markBotTrusted } = getActions(); const { cancelBotTrustRequest, markBotTrusted } = getActions();
const [isWriteAllowed, setIsWriteAllowed] = useState(shouldRequestWriteAccess || false);
const lang = useLang(); const lang = useLang();
// Keep props a little bit longer, to show correct text on closing animation // Keep props a little bit longer, to show correct text on closing animation
const previousBot = usePrevious(bot, false); const previousBot = usePrevious(bot, false);
@ -27,21 +34,46 @@ const BotTrustModal: FC<OwnProps> = ({ bot, type }) => {
const currentType = type || previousType; const currentType = type || previousType;
const handleBotTrustAccept = useCallback(() => { const handleBotTrustAccept = useCallback(() => {
markBotTrusted({ botId: bot!.id }); markBotTrusted({ botId: bot!.id, isWriteAllowed });
}, [markBotTrusted, bot]); }, [markBotTrusted, isWriteAllowed, bot]);
const handleBotTrustDecline = useCallback(() => {
cancelBotTrustRequest();
}, []);
const title = currentType === 'game' ? lang('AppName') : lang('BotOpenPageTitle'); const title = currentType === 'game' ? lang('AppName') : lang('BotOpenPageTitle');
const text = currentType === 'game' ? lang('BotPermissionGameAlert', getUserFullName(currentBot)) const text = useMemo(() => {
: lang('BotOpenPageMessage', getUserFullName(currentBot)); switch (currentType) {
case 'game':
return lang('BotPermissionGameAlert', getUserFullName(currentBot));
case 'webApp':
return lang('BotOpenPageMessage', getUserFullName(currentBot));
case 'botApp':
default:
return lang('BotWebViewStartPermission');
}
}, [currentBot, currentType, lang]);
return ( return (
<ConfirmDialog <ConfirmDialog
isOpen={Boolean(bot)} isOpen={Boolean(bot)}
onClose={cancelBotTrustRequest} onClose={handleBotTrustDecline}
confirmHandler={handleBotTrustAccept}
title={title} title={title}
textParts={renderText(text, ['br', 'simple_markdown', 'emoji'])} confirmHandler={handleBotTrustAccept}
/> >
{text}
{shouldRequestWriteAccess && (
<Checkbox
className="dialog-checkbox"
checked={isWriteAllowed}
label={renderText(
lang('WebApp.AddToAttachmentAllowMessages', currentBot?.firstName),
['simple_markdown'],
)}
onCheck={setIsWriteAllowed}
/>
)}
</ConfirmDialog>
); );
}; };

View File

@ -54,6 +54,7 @@ const DraftRecipientPicker: FC<OwnProps> = ({
<RecipientPicker <RecipientPicker
isOpen={isOpen} isOpen={isOpen}
searchPlaceholder={lang('ForwardTo')} searchPlaceholder={lang('ForwardTo')}
filter={requestedDraft?.filter}
onSelectRecipient={handleSelectRecipient} onSelectRecipient={handleSelectRecipient}
onClose={handleClose} onClose={handleClose}
onCloseAnimationEnd={unmarkIsShown} onCloseAnimationEnd={unmarkIsShown}

View File

@ -511,7 +511,11 @@ const Main: FC<OwnProps & StateProps> = ({
<PhoneCall isActive={isPhoneCallActive} /> <PhoneCall isActive={isPhoneCallActive} />
<UnreadCount isForAppBadge /> <UnreadCount isForAppBadge />
<RatePhoneCallModal isOpen={isRatePhoneCallModalOpen} /> <RatePhoneCallModal isOpen={isRatePhoneCallModalOpen} />
<BotTrustModal bot={botTrustRequestBot} type={botTrustRequest?.type} /> <BotTrustModal
bot={botTrustRequestBot}
type={botTrustRequest?.type}
shouldRequestWriteAccess={botTrustRequest?.shouldRequestWriteAccess}
/>
<AttachBotInstallModal bot={attachBotToInstall} /> <AttachBotInstallModal bot={attachBotToInstall} />
<AttachBotRecipientPicker requestedAttachBotInChat={requestedAttachBotInChat} /> <AttachBotRecipientPicker requestedAttachBotInChat={requestedAttachBotInChat} />
<MessageListHistoryHandler /> <MessageListHistoryHandler />

View File

@ -15,6 +15,7 @@ import {
} from '../../global/selectors'; } from '../../global/selectors';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import { extractCurrentThemeParams, validateHexColor } from '../../util/themeStyle'; import { extractCurrentThemeParams, validateHexColor } from '../../util/themeStyle';
import { convertToApiChatType } from '../../global/helpers';
import useInterval from '../../hooks/useInterval'; import useInterval from '../../hooks/useInterval';
import useLang from '../../hooks/useLang'; import useLang from '../../hooks/useLang';
@ -94,19 +95,26 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
openInvoice, openInvoice,
setWebAppPaymentSlug, setWebAppPaymentSlug,
showNotification, showNotification,
switchBotInline,
} = getActions(); } = getActions();
const [mainButton, setMainButton] = useState<WebAppButton | undefined>(); const [mainButton, setMainButton] = useState<WebAppButton | undefined>();
const [isBackButtonVisible, setIsBackButtonVisible] = useState(false); const [isBackButtonVisible, setIsBackButtonVisible] = useState(false);
const [backgroundColor, setBackgroundColor] = useState(extractCurrentThemeParams().bg_color); const [backgroundColor, setBackgroundColor] = useState<string>();
const [headerColor, setHeaderColor] = useState(extractCurrentThemeParams().bg_color); const [headerColor, setHeaderColor] = useState<string>();
const [confirmClose, setConfirmClose] = useState(false); const [confirmClose, setConfirmClose] = useState(false);
const [isCloseModalOpen, openCloseModal, closeCloseModal] = useFlag(false); const [isCloseModalOpen, openCloseModal, closeModal] = useFlag(false);
const [isLoaded, markLoaded, markUnloaded] = useFlag(false); const [isLoaded, markLoaded, markUnloaded] = useFlag(false);
const [popupParams, setPopupParams] = useState<PopupOptions | undefined>(); const [popupParams, setPopupParams] = useState<PopupOptions | undefined>();
const { isMobile } = useAppLayout(); const { isMobile } = useAppLayout();
const prevPopupParams = usePrevious(popupParams); const prevPopupParams = usePrevious(popupParams);
const renderingPopupParams = popupParams || prevPopupParams; const renderingPopupParams = popupParams || prevPopupParams;
useEffect(() => {
const themeParams = extractCurrentThemeParams();
setBackgroundColor(themeParams.bg_color);
setHeaderColor(themeParams.bg_color);
}, []);
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const frameRef = useRef<HTMLIFrameElement>(null); const frameRef = useRef<HTMLIFrameElement>(null);
@ -115,7 +123,7 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
url, buttonText, queryId, replyToMessageId, threadId, url, buttonText, queryId, replyToMessageId, threadId,
} = webApp || {}; } = webApp || {};
const isOpen = Boolean(url); const isOpen = Boolean(url);
const isSimple = !queryId; const isSimple = Boolean(buttonText);
const handleEvent = useCallback((event: WebAppInboundEvent) => { const handleEvent = useCallback((event: WebAppInboundEvent) => {
const { eventType, eventData } = event; const { eventType, eventData } = event;
@ -198,6 +206,20 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
message: 'Scan QR code is not supported in this client yet', message: 'Scan QR code is not supported in this client yet',
}); });
} }
if (eventType === 'web_app_switch_inline_query') {
const filter = eventData.chat_types?.map(convertToApiChatType).filter(Boolean);
const isSamePeer = !filter?.length;
switchBotInline({
botId: bot!.id,
query: eventData.query,
filter,
isSamePeer,
});
closeWebApp();
}
}, [ }, [
bot, buttonText, closeWebApp, openInvoice, openTelegramLink, sendWebViewData, setWebAppPaymentSlug, bot, buttonText, closeWebApp, openInvoice, openTelegramLink, sendWebViewData, setWebAppPaymentSlug,
isPaymentModalOpen, showNotification, isPaymentModalOpen, showNotification,
@ -314,11 +336,11 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
useEffect(() => { useEffect(() => {
if (!isOpen) { if (!isOpen) {
setConfirmClose(false); setConfirmClose(false);
closeCloseModal(); closeModal();
setPopupParams(undefined); setPopupParams(undefined);
markUnloaded(); markUnloaded();
} }
}, [closeCloseModal, isOpen, markUnloaded]); }, [closeModal, isOpen, markUnloaded]);
const MoreMenuButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => { const MoreMenuButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => {
return ({ onTrigger, isOpen: isMenuOpen }) => ( return ({ onTrigger, isOpen: isMenuOpen }) => (
@ -460,7 +482,7 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
{confirmClose && ( {confirmClose && (
<ConfirmDialog <ConfirmDialog
isOpen={isCloseModalOpen} isOpen={isCloseModalOpen}
onClose={closeCloseModal} onClose={closeModal}
title={lang('lng_bot_close_warning_title')} title={lang('lng_bot_close_warning_title')}
text={lang('lng_bot_close_warning')} text={lang('lng_bot_close_warning')}
confirmHandler={closeWebApp} confirmHandler={closeWebApp}

View File

@ -1,4 +1,5 @@
import { useCallback, useEffect, useRef } from '../../../lib/teact/teact'; import { useCallback, useEffect, useRef } from '../../../lib/teact/teact';
import { getActions } from '../../../lib/teact/teactn';
import { extractCurrentThemeParams } from '../../../util/themeStyle'; import { extractCurrentThemeParams } from '../../../util/themeStyle';
import useWindowSize from '../../../hooks/useWindowSize'; import useWindowSize from '../../../hooks/useWindowSize';
@ -83,6 +84,12 @@ export type WebAppInboundEvent = {
eventData: { eventData: {
req_id: string; req_id: string;
}; };
} | {
eventType: 'web_app_switch_inline_query';
eventData: {
query: string;
chat_types: ('users' | 'bots' | 'groups' | 'channels')[];
};
} | { } | {
eventType: 'web_app_request_viewport' | 'web_app_request_theme' | 'web_app_ready' | 'web_app_expand' eventType: 'web_app_request_viewport' | 'web_app_request_theme' | 'web_app_ready' | 'web_app_expand'
| 'web_app_request_phone' | 'web_app_close' | 'iframe_ready' | 'web_app_close_scan_qr_popup'; | 'web_app_request_phone' | 'web_app_close' | 'iframe_ready' | 'web_app_close_scan_qr_popup';
@ -171,6 +178,10 @@ const useWebAppFrame = (
onEvent: (event: WebAppInboundEvent) => void, onEvent: (event: WebAppInboundEvent) => void,
onLoad?: () => void, onLoad?: () => void,
) => { ) => {
const {
showNotification,
} = getActions();
const ignoreEventsRef = useRef<boolean>(false); const ignoreEventsRef = useRef<boolean>(false);
const lastFrameSizeRef = useRef<{ width: number; height: number; isResizing?: boolean }>(); const lastFrameSizeRef = useRef<{ width: number; height: number; isResizing?: boolean }>();
const windowSize = useWindowSize(); const windowSize = useWindowSize();
@ -266,23 +277,24 @@ const useWebAppFrame = (
} }
if (data.eventType === 'web_app_read_text_from_clipboard') { if (data.eventType === 'web_app_read_text_from_clipboard') {
const { req_id: requestId } = data.eventData; sendEvent({
// eslint-disable-next-line no-null/no-null -- Required by spec eventType: 'clipboard_text_received',
window.navigator.clipboard.readText().catch(() => null).then((text) => { eventData: {
sendEvent({ req_id: data.eventData.req_id,
eventType: 'clipboard_text_received', // eslint-disable-next-line no-null/no-null
eventData: { data: null,
req_id: requestId, },
data: text, });
},
}); showNotification({
message: 'Clipboard access is not supported in this client yet',
}); });
} }
onEvent(data); onEvent(data);
} catch (err) { } catch (err) {
// Ignore other messages // Ignore other messages
} }
}, [isSimpleView, onEvent, sendCustomStyle, sendEvent, sendTheme, sendViewport, onLoad, windowSize.isResizing]); }, [isSimpleView, sendEvent, onEvent, sendCustomStyle, sendTheme, sendViewport, onLoad, windowSize.isResizing]);
useEffect(() => { useEffect(() => {
const { width, height, isResizing } = windowSize; const { width, height, isResizing } = windowSize;

View File

@ -496,6 +496,7 @@ const Composer: FC<OwnProps & StateProps> = ({
botId: inlineBotId, botId: inlineBotId,
isGallery: isInlineBotTooltipGallery, isGallery: isInlineBotTooltipGallery,
switchPm: inlineBotSwitchPm, switchPm: inlineBotSwitchPm,
switchWebview: inlineBotSwitchWebview,
results: inlineBotResults, results: inlineBotResults,
closeTooltip: closeInlineBotTooltip, closeTooltip: closeInlineBotTooltip,
help: inlineBotHelp, help: inlineBotHelp,
@ -1322,6 +1323,7 @@ const Composer: FC<OwnProps & StateProps> = ({
isGallery={isInlineBotTooltipGallery} isGallery={isInlineBotTooltipGallery}
inlineBotResults={inlineBotResults} inlineBotResults={inlineBotResults}
switchPm={inlineBotSwitchPm} switchPm={inlineBotSwitchPm}
switchWebview={inlineBotSwitchWebview}
loadMore={loadMoreForInlineBot} loadMore={loadMoreForInlineBot}
isSavedMessages={isChatWithSelf} isSavedMessages={isChatWithSelf}
canSendGifs={canSendGifs} canSendGifs={canSendGifs}

View File

@ -1,17 +1,21 @@
import type { FC } from '../../../lib/teact/teact';
import React, { import React, {
memo, useCallback, useEffect, useRef, memo, useCallback, useEffect, useRef,
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
import { getActions } from '../../../global'; import { getActions } from '../../../global';
import type { ApiBotInlineMediaResult, ApiBotInlineResult, ApiBotInlineSwitchPm } from '../../../api/types'; import type { FC } from '../../../lib/teact/teact';
import type {
ApiBotInlineMediaResult, ApiBotInlineResult, ApiBotInlineSwitchPm, ApiBotInlineSwitchWebview,
} from '../../../api/types';
import { LoadMoreDirection } from '../../../types'; import { LoadMoreDirection } from '../../../types';
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment'; import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
import setTooltipItemVisible from '../../../util/setTooltipItemVisible'; import setTooltipItemVisible from '../../../util/setTooltipItemVisible';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import useShowTransition from '../../../hooks/useShowTransition'; import { extractCurrentThemeParams } from '../../../util/themeStyle';
import { throttle } from '../../../util/schedulers'; import { throttle } from '../../../util/schedulers';
import useShowTransition from '../../../hooks/useShowTransition';
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver'; import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
import usePrevious from '../../../hooks/usePrevious'; import usePrevious from '../../../hooks/usePrevious';
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev'; import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
@ -35,6 +39,7 @@ export type OwnProps = {
isGallery?: boolean; isGallery?: boolean;
inlineBotResults?: (ApiBotInlineResult | ApiBotInlineMediaResult)[]; inlineBotResults?: (ApiBotInlineResult | ApiBotInlineMediaResult)[];
switchPm?: ApiBotInlineSwitchPm; switchPm?: ApiBotInlineSwitchPm;
switchWebview?: ApiBotInlineSwitchWebview;
isSavedMessages?: boolean; isSavedMessages?: boolean;
canSendGifs?: boolean; canSendGifs?: boolean;
onSelectResult: ( onSelectResult: (
@ -51,6 +56,7 @@ const InlineBotTooltip: FC<OwnProps> = ({
isGallery, isGallery,
inlineBotResults, inlineBotResults,
switchPm, switchPm,
switchWebview,
isSavedMessages, isSavedMessages,
canSendGifs, canSendGifs,
loadMore, loadMore,
@ -61,6 +67,7 @@ const InlineBotTooltip: FC<OwnProps> = ({
const { const {
openChat, openChat,
startBot, startBot,
requestSimpleWebView,
} = getActions(); } = getActions();
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
@ -99,6 +106,17 @@ const InlineBotTooltip: FC<OwnProps> = ({
startBot({ botId: botId!, param: switchPm!.startParam }); startBot({ botId: botId!, param: switchPm!.startParam });
}, [botId, openChat, startBot, switchPm]); }, [botId, openChat, startBot, switchPm]);
const handleOpenWebview = useCallback(() => {
const theme = extractCurrentThemeParams();
requestSimpleWebView({
botId: botId!,
url: switchWebview!.url,
buttonText: switchWebview!.text,
theme,
});
}, [botId, switchWebview]);
const prevInlineBotResults = usePrevious( const prevInlineBotResults = usePrevious(
inlineBotResults?.length inlineBotResults?.length
? inlineBotResults ? inlineBotResults
@ -126,6 +144,14 @@ const InlineBotTooltip: FC<OwnProps> = ({
); );
} }
function renderSwitchWebview() {
return (
<ListItem ripple className="switch-pm scroll-item" onClick={handleOpenWebview}>
<span className="title">{switchWebview!.text}</span>
</ListItem>
);
}
function renderContent() { function renderContent() {
return renderedInlineBotResults!.map((inlineBotResult, index) => { return renderedInlineBotResults!.map((inlineBotResult, index) => {
switch (inlineBotResult.type) { switch (inlineBotResult.type) {
@ -203,6 +229,7 @@ const InlineBotTooltip: FC<OwnProps> = ({
sensitiveArea={160} sensitiveArea={160}
> >
{switchPm && renderSwitchPm()} {switchPm && renderSwitchPm()}
{switchWebview && renderSwitchWebview()}
{Boolean(renderedInlineBotResults?.length) && renderContent()} {Boolean(renderedInlineBotResults?.length) && renderContent()}
</InfiniteScroll> </InfiniteScroll>
); );

View File

@ -60,6 +60,7 @@ export default function useInlineBotTooltip(
const { const {
id: botId, id: botId,
switchPm, switchPm,
switchWebview,
offset, offset,
results, results,
isGallery, isGallery,
@ -87,6 +88,7 @@ export default function useInlineBotTooltip(
botId, botId,
isGallery, isGallery,
switchPm, switchPm,
switchWebview,
results, results,
closeTooltip: markManuallyClosed, closeTooltip: markManuallyClosed,
help: canShowHelp && help ? `@${username} ${help}` : undefined, help: canShowHelp && help ? `@${username} ${help}` : undefined,

View File

@ -45,7 +45,7 @@ export const CUSTOM_EMOJI_PREVIEW_CACHE_DISABLED = false;
export const CUSTOM_EMOJI_PREVIEW_CACHE_NAME = 'tt-custom-emoji-preview'; export const CUSTOM_EMOJI_PREVIEW_CACHE_NAME = 'tt-custom-emoji-preview';
export const MEDIA_CACHE_MAX_BYTES = 512 * 1024; // 512 KB export const MEDIA_CACHE_MAX_BYTES = 512 * 1024; // 512 KB
export const CUSTOM_BG_CACHE_NAME = 'tt-custom-bg'; export const CUSTOM_BG_CACHE_NAME = 'tt-custom-bg';
export const LANG_CACHE_NAME = 'tt-lang-packs-v17'; export const LANG_CACHE_NAME = 'tt-lang-packs-v18';
export const ASSET_CACHE_NAME = 'tt-assets'; export const ASSET_CACHE_NAME = 'tt-assets';
export const AUTODOWNLOAD_FILESIZE_MB_LIMITS = [1, 5, 10, 50, 100, 500]; export const AUTODOWNLOAD_FILESIZE_MB_LIMITS = [1, 5, 10, 50, 100, 500];
export const DATA_BROADCAST_CHANNEL_NAME = 'tt-global'; export const DATA_BROADCAST_CHANNEL_NAME = 'tt-global';
@ -233,7 +233,7 @@ export const SUPPORTED_TRANSLATION_LANGUAGES = [
// Official // Official
'en', 'ar', 'be', 'ca', 'zh', 'nl', 'fr', 'de', 'id', 'en', 'ar', 'be', 'ca', 'zh', 'nl', 'fr', 'de', 'id',
'it', 'ja', 'ko', 'pl', 'pt', 'ru', 'es', 'uk', 'it', 'ja', 'ko', 'pl', 'pt', 'ru', 'es', 'uk',
// Unnofficial // Unofficial
'af', 'sq', 'am', 'hy', 'az', 'eu', 'bn', 'bs', 'bg', 'af', 'sq', 'am', 'hy', 'az', 'eu', 'bn', 'bs', 'bg',
'ceb', 'zh-CN', 'zh-TW', 'co', 'hr', 'cs', 'da', 'eo', 'ceb', 'zh-CN', 'zh-TW', 'co', 'hr', 'cs', 'da', 'eo',
'et', 'fi', 'fy', 'gl', 'ka', 'el', 'gu', 'ht', 'ha', 'et', 'fi', 'fy', 'gl', 'ka', 'el', 'gu', 'ht', 'ha',
@ -257,6 +257,7 @@ export const TME_LINK_PREFIX = 'https://t.me/';
export const USERNAME_PURCHASE_ERROR = 'USERNAME_PURCHASE_AVAILABLE'; export const USERNAME_PURCHASE_ERROR = 'USERNAME_PURCHASE_AVAILABLE';
export const PURCHASE_USERNAME = 'auction'; export const PURCHASE_USERNAME = 'auction';
export const TME_WEB_DOMAINS = new Set(['t.me', 'web.t.me', 'a.t.me', 'k.t.me', 'z.t.me']); export const TME_WEB_DOMAINS = new Set(['t.me', 'web.t.me', 'a.t.me', 'k.t.me', 'z.t.me']);
export const WEB_APP_PLATFORM = 'weba';
// eslint-disable-next-line max-len // eslint-disable-next-line max-len
export const COUNTRIES_WITH_12H_TIME_FORMAT = new Set(['AU', 'BD', 'CA', 'CO', 'EG', 'HN', 'IE', 'IN', 'JO', 'MX', 'MY', 'NI', 'NZ', 'PH', 'PK', 'SA', 'SV', 'US']); export const COUNTRIES_WITH_12H_TIME_FORMAT = new Set(['AU', 'BD', 'CA', 'CO', 'EG', 'HN', 'IE', 'IN', 'JO', 'MX', 'MY', 'NI', 'NZ', 'PH', 'PK', 'SA', 'SV', 'US']);

View File

@ -24,6 +24,7 @@ import { extractCurrentThemeParams } from '../../../util/themeStyle';
import PopupManager from '../../../util/PopupManager'; import PopupManager from '../../../util/PopupManager';
import { updateTabState } from '../../reducers/tabs'; import { updateTabState } from '../../reducers/tabs';
import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { translate } from '../../../util/langProvider';
const GAMEE_URL = 'https://prizes.gamee.com/'; const GAMEE_URL = 'https://prizes.gamee.com/';
const TOP_PEERS_REQUEST_COOLDOWN = 60; // 1 min const TOP_PEERS_REQUEST_COOLDOWN = 60; // 1 min
@ -290,18 +291,29 @@ addActionHandler('queryInlineBot', async (global, actions, payload): Promise<voi
addActionHandler('switchBotInline', (global, actions, payload): ActionReturnType => { addActionHandler('switchBotInline', (global, actions, payload): ActionReturnType => {
const { const {
query, isSamePeer, messageId, tabId = getCurrentTabId(), query, isSamePeer, messageId, filter, tabId = getCurrentTabId(),
} = payload;
let {
botId,
} = payload; } = payload;
const chat = selectCurrentChat(global, tabId); const chat = selectCurrentChat(global, tabId);
if (!chat) { if (!chat) {
return undefined; return undefined;
} }
const message = selectChatMessage(global, chat.id, messageId);
if (!message) { if (!botId && messageId) {
const message = selectChatMessage(global, chat.id, messageId);
if (!message) {
return undefined;
}
botId = message.viaBotId || message.senderId;
}
if (!botId) {
return undefined; return undefined;
} }
const botSender = selectUser(global, message.viaBotId || message.senderId!); const botSender = selectUser(global, botId);
if (!botSender) { if (!botSender) {
return undefined; return undefined;
} }
@ -309,6 +321,7 @@ addActionHandler('switchBotInline', (global, actions, payload): ActionReturnType
actions.openChatWithDraft({ actions.openChatWithDraft({
text: `@${botSender.usernames![0].username} ${query}`, text: `@${botSender.usernames![0].username} ${query}`,
chatId: isSamePeer ? chat.id : undefined, chatId: isSamePeer ? chat.id : undefined,
filter,
tabId, tabId,
}); });
return undefined; return undefined;
@ -513,6 +526,65 @@ addActionHandler('requestWebView', async (global, actions, payload): Promise<voi
setGlobal(global); setGlobal(global);
}); });
addActionHandler('requestAppWebView', async (global, actions, payload): Promise<void> => {
const {
botId, appName, startApp, theme, isWriteAllowed,
tabId = getCurrentTabId(),
} = payload;
const bot = selectUser(global, botId);
if (!bot) return;
const botApp = await callApi('fetchBotApp', {
bot,
appName,
});
global = getGlobal();
if (!botApp) {
actions.showNotification({ message: translate('lng_username_app_not_found'), tabId });
return;
}
if (botApp.isInactive && !selectIsTrustedBot(global, botId)) {
global = updateTabState(global, {
botTrustRequest: {
botId,
shouldRequestWriteAccess: botApp.shouldRequestWriteAccess,
type: 'botApp',
onConfirm: {
action: 'requestAppWebView',
payload,
},
},
}, tabId);
setGlobal(global);
return;
}
const peer = selectCurrentChat(global, tabId);
const url = await callApi('requestAppWebView', {
peer: peer || bot,
app: botApp,
startParam: startApp,
isWriteAllowed,
theme,
});
global = getGlobal();
if (!url) return;
global = updateTabState(global, {
webApp: {
url,
botId,
buttonText: '',
},
}, tabId);
setGlobal(global);
});
addActionHandler('prolongWebView', async (global, actions, payload): Promise<void> => { addActionHandler('prolongWebView', async (global, actions, payload): Promise<void> => {
const { const {
botId, peerId, isSilent, replyToMessageId, queryId, threadId, botId, peerId, isSilent, replyToMessageId, queryId, threadId,
@ -582,7 +654,7 @@ addActionHandler('cancelBotTrustRequest', (global, actions, payload): ActionRetu
}); });
addActionHandler('markBotTrusted', (global, actions, payload): ActionReturnType => { addActionHandler('markBotTrusted', (global, actions, payload): ActionReturnType => {
const { botId, tabId = getCurrentTabId() } = payload; const { botId, isWriteAllowed, tabId = getCurrentTabId() } = payload;
const { trustedBotIds } = global; const { trustedBotIds } = global;
const newTrustedBotIds = new Set(trustedBotIds); const newTrustedBotIds = new Set(trustedBotIds);
@ -597,7 +669,10 @@ addActionHandler('markBotTrusted', (global, actions, payload): ActionReturnType
if (tabState.botTrustRequest?.onConfirm) { if (tabState.botTrustRequest?.onConfirm) {
const { action, payload: callbackPayload } = tabState.botTrustRequest.onConfirm; const { action, payload: callbackPayload } = tabState.botTrustRequest.onConfirm;
// @ts-ignore // @ts-ignore
actions[action](callbackPayload); actions[action]({
...(callbackPayload as {}),
isWriteAllowed,
});
} }
global = updateTabState(global, { global = updateTabState(global, {
@ -921,6 +996,7 @@ async function searchInlineBot<T extends GlobalState>(global: T, {
cacheTime: Date.now() + result.cacheTime * 1000, cacheTime: Date.now() + result.cacheTime * 1000,
...(newResults.length && { isGallery: result.isGallery }), ...(newResults.length && { isGallery: result.isGallery }),
...(result.switchPm && { switchPm: result.switchPm }), ...(result.switchPm && { switchPm: result.switchPm }),
...(result.switchWebview && { switchWebview: result.switchWebview }),
canLoadMore: result.results.length > 0 && Boolean(result.nextOffset), canLoadMore: result.results.length > 0 && Boolean(result.nextOffset),
results: newInlineBotData.offset === '' || newInlineBotData.offset === result.nextOffset results: newInlineBotData.offset === '' || newInlineBotData.offset === result.nextOffset
? result.results ? result.results

View File

@ -77,6 +77,7 @@ import * as langProvider from '../../../util/langProvider';
import { selectCurrentLimit } from '../../selectors/limits'; import { selectCurrentLimit } from '../../selectors/limits';
import { updateTabState } from '../../reducers/tabs'; import { updateTabState } from '../../reducers/tabs';
import { getCurrentTabId } from '../../../util/establishMultitabRole'; import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { extractCurrentThemeParams } from '../../../util/themeStyle';
const TOP_CHAT_MESSAGES_PRELOAD_INTERVAL = 100; const TOP_CHAT_MESSAGES_PRELOAD_INTERVAL = 100;
const INFINITE_LOOP_MARKER = 100; const INFINITE_LOOP_MARKER = 100;
@ -1005,6 +1006,8 @@ addActionHandler('openTelegramLink', (global, actions, payload): ActionReturnTyp
startParam: params.start, startParam: params.start,
startAttach, startAttach,
attach: params.attach, attach: params.attach,
startApp: params.startapp,
originalParts: [part1, part2, part3],
tabId, tabId,
}); });
} }
@ -1022,11 +1025,13 @@ addActionHandler('acceptInviteConfirmation', async (global, actions, payload): P
addActionHandler('openChatByUsername', async (global, actions, payload): Promise<void> => { addActionHandler('openChatByUsername', async (global, actions, payload): Promise<void> => {
const { const {
username, messageId, commentId, startParam, startAttach, attach, threadId, username, messageId, commentId, startParam, startAttach, attach, threadId, originalParts, startApp,
tabId = getCurrentTabId(), tabId = getCurrentTabId(),
} = payload!; } = payload!;
const chat = selectCurrentChat(global, tabId); const chat = selectCurrentChat(global, tabId);
const webAppName = originalParts?.[1];
const isWebApp = webAppName && !Number(webAppName);
if (!commentId) { if (!commentId) {
if (!startAttach && messageId && !startParam && chat?.usernames?.some((c) => c.username === username)) { if (!startAttach && messageId && !startParam && chat?.usernames?.some((c) => c.username === username)) {
@ -1035,13 +1040,15 @@ addActionHandler('openChatByUsername', async (global, actions, payload): Promise
}); });
return; return;
} }
await openChatByUsername(global, actions, username, threadId, messageId, startParam, startAttach, attach, tabId); if (!isWebApp) {
return; await openChatByUsername(global, actions, username, threadId, messageId, startParam, startAttach, attach, tabId);
return;
}
} }
const { chatId, type } = selectCurrentMessageList(global, tabId) || {}; const { chatId, type } = selectCurrentMessageList(global, tabId) || {};
const usernameChat = selectChatByUsername(global, username); const usernameChat = selectChatByUsername(global, username);
if (chatId && messageId && usernameChat && type === 'thread') { if (chatId && commentId && messageId && usernameChat && type === 'thread') {
const threadInfo = selectThreadInfo(global, chatId, messageId); const threadInfo = selectThreadInfo(global, chatId, messageId);
if (threadInfo && threadInfo.chatId === chatId) { if (threadInfo && threadInfo.chatId === chatId) {
@ -1055,9 +1062,7 @@ addActionHandler('openChatByUsername', async (global, actions, payload): Promise
} }
} }
if (!messageId) return; if (!isWebApp) actions.openChat({ id: TMP_CHAT_ID, tabId });
actions.openChat({ id: TMP_CHAT_ID, tabId });
const chatByUsername = await fetchChatByUsername(global, username); const chatByUsername = await fetchChatByUsername(global, username);
@ -1065,6 +1070,21 @@ addActionHandler('openChatByUsername', async (global, actions, payload): Promise
global = getGlobal(); global = getGlobal();
if (isWebApp && chatByUsername) {
const theme = extractCurrentThemeParams();
actions.requestAppWebView({
appName: webAppName,
botId: chatByUsername.id,
tabId,
startApp,
theme,
});
return;
}
if (!messageId) return;
const threadInfo = selectThreadInfo(global, chatByUsername.id, messageId); const threadInfo = selectThreadInfo(global, chatByUsername.id, messageId);
let discussionChatId: string | undefined; let discussionChatId: string | undefined;

View File

@ -102,7 +102,7 @@ addActionHandler('openChatWithInfo', (global, actions, payload): ActionReturnTyp
addActionHandler('openChatWithDraft', (global, actions, payload): ActionReturnType => { addActionHandler('openChatWithDraft', (global, actions, payload): ActionReturnType => {
const { const {
chatId, text, threadId, files, tabId = getCurrentTabId(), chatId, text, threadId, files, filter, tabId = getCurrentTabId(),
} = payload; } = payload;
if (chatId) { if (chatId) {
@ -114,6 +114,7 @@ addActionHandler('openChatWithDraft', (global, actions, payload): ActionReturnTy
chatId, chatId,
text, text,
files, files,
filter,
}, },
}, tabId); }, tabId);
}); });

View File

@ -1,5 +1,13 @@
import type { ApiPhoto } from '../../api/types'; import type { ApiChatType, ApiPhoto } from '../../api/types';
export function getBotCoverMediaHash(photo: ApiPhoto) { export function getBotCoverMediaHash(photo: ApiPhoto) {
return `photo${photo.id}?size=x`; return `photo${photo.id}?size=x`;
} }
export function convertToApiChatType(type: string): ApiChatType | undefined {
if (type === 'channels') return 'channels';
if (type === 'chats' || type === 'groups') return 'chats';
if (type === 'users') return 'users';
if (type === 'bots') return 'bots';
return undefined;
}

View File

@ -452,6 +452,7 @@ export type TabState = {
chatId?: string; chatId?: string;
text: string; text: string;
files?: File[]; files?: File[];
filter?: ApiChatType[];
}; };
pollModal: { pollModal: {
@ -471,7 +472,8 @@ export type TabState = {
botTrustRequest?: { botTrustRequest?: {
botId: string; botId: string;
type: 'game' | 'webApp'; type: 'game' | 'webApp' | 'botApp';
shouldRequestWriteAccess?: boolean;
onConfirm?: CallbackAction; onConfirm?: CallbackAction;
}; };
requestedAttachBotInstall?: { requestedAttachBotInstall?: {
@ -1260,6 +1262,8 @@ export interface ActionPayloads {
startParam?: string; startParam?: string;
startAttach?: string | boolean; startAttach?: string | boolean;
attach?: string; attach?: string;
startApp?: string;
originalParts?: string[];
} & WithTabId; } & WithTabId;
requestThreadInfoUpdate: { requestThreadInfoUpdate: {
chatId: string; chatId: string;
@ -1644,6 +1648,7 @@ export interface ActionPayloads {
threadId?: number; threadId?: number;
text: string; text: string;
files?: File[]; files?: File[];
filter?: ApiChatType[];
} & WithTabId; } & WithTabId;
resetOpenChatWithDraft: WithTabId | undefined; resetOpenChatWithDraft: WithTabId | undefined;
toggleJoinToSend: { toggleJoinToSend: {
@ -2019,9 +2024,11 @@ export interface ActionPayloads {
} & WithTabId; } & WithTabId;
switchBotInline: { switchBotInline: {
messageId: number; messageId?: number;
botId?: string;
query: string; query: string;
isSamePeer?: boolean; isSamePeer?: boolean;
filter?: ApiChatType[];
} & WithTabId; } & WithTabId;
openGame: { openGame: {
@ -2055,6 +2062,13 @@ export interface ActionPayloads {
buttonText: string; buttonText: string;
theme?: ApiThemeParameters; theme?: ApiThemeParameters;
} & WithTabId; } & WithTabId;
requestAppWebView: {
botId: string;
appName: string;
theme?: ApiThemeParameters;
startApp?: string;
isWriteAllowed?: boolean;
} & WithTabId;
setWebAppPaymentSlug: { setWebAppPaymentSlug: {
slug?: string; slug?: string;
} & WithTabId; } & WithTabId;
@ -2062,6 +2076,7 @@ export interface ActionPayloads {
cancelBotTrustRequest: WithTabId | undefined; cancelBotTrustRequest: WithTabId | undefined;
markBotTrusted: { markBotTrusted: {
botId: string; botId: string;
isWriteAllowed?: boolean;
} & WithTabId; } & WithTabId;
cancelAttachBotInstall: WithTabId | undefined; cancelAttachBotInstall: WithTabId | undefined;

View File

@ -1304,6 +1304,8 @@ messages.getTopReactions#bb8125ba limit:int hash:long = messages.Reactions;
messages.getRecentReactions#39461db2 limit:int hash:long = messages.Reactions; messages.getRecentReactions#39461db2 limit:int hash:long = messages.Reactions;
messages.clearRecentReactions#9dfeefb4 = Bool; messages.clearRecentReactions#9dfeefb4 = Bool;
messages.getExtendedMedia#84f80814 peer:InputPeer id:Vector<int> = Updates; messages.getExtendedMedia#84f80814 peer:InputPeer id:Vector<int> = Updates;
messages.getBotApp#34fdc5c3 app:InputBotApp hash:long = messages.BotApp;
messages.requestAppWebView#8c5a3b3c flags:# write_allowed:flags.0?true peer:InputPeer app:InputBotApp start_param:flags.1?string theme_params:flags.2?DataJSON platform:string = AppWebViewResult;
updates.getState#edd4882a = updates.State; updates.getState#edd4882a = updates.State;
updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference; updates.getDifference#25939651 flags:# pts:int pts_total_limit:flags.0?int date:int qts:int = updates.Difference;
updates.getChannelDifference#3173d78 flags:# force:flags.0?true channel:InputChannel filter:ChannelMessagesFilter pts:int limit:int = updates.ChannelDifference; updates.getChannelDifference#3173d78 flags:# force:flags.0?true channel:InputChannel filter:ChannelMessagesFilter pts:int limit:int = updates.ChannelDifference;

View File

@ -163,6 +163,8 @@
"messages.toggleNoForwards", "messages.toggleNoForwards",
"messages.saveDefaultSendAs", "messages.saveDefaultSendAs",
"messages.getExtendedMedia", "messages.getExtendedMedia",
"messages.getBotApp",
"messages.requestAppWebView",
"updates.getState", "updates.getState",
"updates.getDifference", "updates.getDifference",
"updates.getChannelDifference", "updates.getChannelDifference",

View File

@ -1,6 +1,7 @@
import type { TeactNode } from '../lib/teact/teact'; import type { TeactNode } from '../lib/teact/teact';
import type { import type {
ApiBotInlineMediaResult, ApiBotInlineResult, ApiBotInlineSwitchPm, ApiBotInlineMediaResult, ApiBotInlineResult, ApiBotInlineSwitchPm,
ApiBotInlineSwitchWebview,
ApiChatInviteImporter, ApiChatInviteImporter,
ApiExportedInvite, ApiExportedInvite,
ApiLanguage, ApiMessage, ApiReaction, ApiStickerSet, ApiLanguage, ApiMessage, ApiReaction, ApiStickerSet,
@ -396,5 +397,6 @@ export type InlineBotSettings = {
results?: (ApiBotInlineResult | ApiBotInlineMediaResult)[]; results?: (ApiBotInlineResult | ApiBotInlineMediaResult)[];
isGallery?: boolean; isGallery?: boolean;
switchPm?: ApiBotInlineSwitchPm; switchPm?: ApiBotInlineSwitchPm;
switchWebview?: ApiBotInlineSwitchWebview;
cacheTime: number; cacheTime: number;
}; };

View File

@ -36,6 +36,7 @@ export const processDeepLink = (url: string) => {
case 'resolve': { case 'resolve': {
const { const {
domain, phone, post, comment, voicechat, livestream, start, startattach, attach, thread, topic, domain, phone, post, comment, voicechat, livestream, start, startattach, attach, thread, topic,
appname, startapp,
} = params; } = params;
const startAttach = params.hasOwnProperty('startattach') && !startattach ? true : startattach; const startAttach = params.hasOwnProperty('startattach') && !startattach ? true : startattach;
@ -43,7 +44,13 @@ export const processDeepLink = (url: string) => {
const threadId = Number(thread) || Number(topic) || undefined; const threadId = Number(thread) || Number(topic) || undefined;
if (domain !== 'telegrampassport') { if (domain !== 'telegrampassport') {
if (startAttach && choose) { if (appname) {
openChatByUsername({
username: domain,
startApp: startapp,
originalParts: [domain, appname],
});
} else if (startAttach && choose) {
processAttachBotParameters({ processAttachBotParameters({
username: domain, username: domain,
filter: choose, filter: choose,