1362 lines
43 KiB
TypeScript
1362 lines
43 KiB
TypeScript
import type { FC } from '../../../lib/teact/teact';
|
|
import { memo, useEffect, useMemo, useRef, useState } from '../../../lib/teact/teact';
|
|
import { getActions, withGlobal } from '../../../global';
|
|
|
|
import type { ApiAttachBot, ApiBotAppSettings, ApiUser } from '../../../api/types';
|
|
import type { TabState } from '../../../global/types';
|
|
import type { BotAppPermissions, ThemeKey } from '../../../types';
|
|
import type {
|
|
PopupOptions,
|
|
WebApp,
|
|
WebAppInboundEvent,
|
|
WebAppModalStateType,
|
|
WebAppOutboundEvent,
|
|
} from '../../../types/webapp';
|
|
|
|
import { TME_LINK_PREFIX, VERIFY_AGE_MIN_DEFAULT } from '../../../config';
|
|
import { convertToApiChatType, getUserFullName } from '../../../global/helpers';
|
|
import { getWebAppKey } from '../../../global/helpers/bots';
|
|
import {
|
|
selectBotAppPermissions,
|
|
selectTabState,
|
|
selectTheme,
|
|
selectUser,
|
|
selectUserFullInfo,
|
|
selectWebApp,
|
|
} from '../../../global/selectors';
|
|
import { getGeolocationStatus, IS_GEOLOCATION_SUPPORTED } from '../../../util/browser/windowEnvironment';
|
|
import buildClassName from '../../../util/buildClassName';
|
|
import buildStyle from '../../../util/buildStyle.ts';
|
|
import download from '../../../util/download';
|
|
import { extractCurrentThemeParams, validateHexColor } from '../../../util/themeStyle';
|
|
import { callApi } from '../../../api/gramjs';
|
|
import { REM } from '../../common/helpers/mediaDimensions';
|
|
import renderText from '../../common/helpers/renderText';
|
|
|
|
import { getIsWebAppsFullscreenSupported } from '../../../hooks/useAppLayout';
|
|
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
|
|
import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps';
|
|
import useFlag from '../../../hooks/useFlag';
|
|
import useLang from '../../../hooks/useLang';
|
|
import useLastCallback from '../../../hooks/useLastCallback';
|
|
import useOldLang from '../../../hooks/useOldLang';
|
|
import useSyncEffect from '../../../hooks/useSyncEffect';
|
|
import useFullscreen, { checkIfFullscreen } from '../../../hooks/window/useFullscreen';
|
|
import usePopupLimit from './hooks/usePopupLimit';
|
|
import useWebAppFrame from './hooks/useWebAppFrame';
|
|
|
|
import CustomEmoji from '../../common/CustomEmoji';
|
|
import Icon from '../../common/icons/Icon';
|
|
import Button from '../../ui/Button';
|
|
import ConfirmDialog from '../../ui/ConfirmDialog';
|
|
import Modal from '../../ui/Modal';
|
|
import Spinner from '../../ui/Spinner';
|
|
import Transition from '../../ui/Transition';
|
|
|
|
import styles from './WebAppModalTabContent.module.scss';
|
|
|
|
type WebAppButton = {
|
|
isVisible: boolean;
|
|
isActive: boolean;
|
|
text: string;
|
|
color: string;
|
|
textColor: string;
|
|
isProgressVisible: boolean;
|
|
iconCustomEmojiId?: string;
|
|
hasShineEffect?: boolean;
|
|
position?: 'left' | 'right' | 'top' | 'bottom';
|
|
};
|
|
|
|
export type OwnProps = {
|
|
modal?: TabState['webApps'];
|
|
webApp?: WebApp;
|
|
registerSendEventCallback: (callback: (event: WebAppOutboundEvent) => void) => void;
|
|
registerReloadFrameCallback: (callback: (url: string) => void) => void;
|
|
onContextMenuButtonClick: (e: React.MouseEvent) => void;
|
|
isTransforming?: boolean;
|
|
isMultiTabSupported?: boolean;
|
|
modalHeight: number;
|
|
};
|
|
|
|
type StateProps = {
|
|
bot?: ApiUser;
|
|
currentUser?: ApiUser;
|
|
botAppSettings?: ApiBotAppSettings;
|
|
attachBot?: ApiAttachBot;
|
|
theme?: ThemeKey;
|
|
isPaymentModalOpen?: boolean;
|
|
paymentStatus?: TabState['payment']['status'];
|
|
modalState?: WebAppModalStateType;
|
|
botAppPermissions?: BotAppPermissions;
|
|
verifyAgeMin?: number;
|
|
verifyAgeBotUsername?: string;
|
|
};
|
|
|
|
const MAIN_BUTTON_ANIMATION_TIME = 250;
|
|
const ANIMATION_WAIT = 400;
|
|
const COLLAPSING_WAIT = 350;
|
|
const POPUP_SEQUENTIAL_LIMIT = 3;
|
|
const POPUP_RESET_DELAY = 2000; // 2s
|
|
const APP_NAME_DISPLAY_DURATION = 3800;
|
|
const SANDBOX_ATTRIBUTES = [
|
|
'allow-scripts',
|
|
'allow-popups',
|
|
'allow-forms',
|
|
'allow-modals',
|
|
'allow-same-origin',
|
|
'allow-storage-access-by-user-activation',
|
|
].join(' ');
|
|
|
|
const DEFAULT_BUTTON_TEXT: Record<string, string> = {
|
|
ok: 'OK',
|
|
cancel: 'Cancel',
|
|
close: 'Close',
|
|
};
|
|
|
|
const NBSP = '\u00A0';
|
|
|
|
const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
|
modal,
|
|
webApp,
|
|
bot,
|
|
theme,
|
|
isPaymentModalOpen,
|
|
paymentStatus,
|
|
isTransforming,
|
|
modalState,
|
|
isMultiTabSupported,
|
|
botAppPermissions,
|
|
botAppSettings,
|
|
modalHeight,
|
|
verifyAgeMin = VERIFY_AGE_MIN_DEFAULT,
|
|
verifyAgeBotUsername,
|
|
registerSendEventCallback,
|
|
registerReloadFrameCallback,
|
|
onContextMenuButtonClick,
|
|
}) => {
|
|
const {
|
|
closeActiveWebApp,
|
|
sendWebViewData,
|
|
toggleAttachBot,
|
|
openTelegramLink,
|
|
setWebAppPaymentSlug,
|
|
switchBotInline,
|
|
sharePhoneWithBot,
|
|
updateWebApp,
|
|
resetPaymentStatus,
|
|
openChatWithInfo,
|
|
showNotification,
|
|
openEmojiStatusAccessModal,
|
|
openLocationAccessModal,
|
|
changeWebAppModalState,
|
|
closeWebAppModal,
|
|
openPreparedInlineMessageModal,
|
|
updateContentSettings,
|
|
} = getActions();
|
|
const [mainButton, setMainButton] = useState<WebAppButton>();
|
|
const [secondaryButton, setSecondaryButton] = useState<WebAppButton>();
|
|
|
|
const [isLoaded, markLoaded, markUnloaded] = useFlag(false);
|
|
|
|
const [popupParameters, setPopupParameters] = useState<PopupOptions>();
|
|
|
|
const renderingPopupParameters = useCurrentOrPrev(popupParameters);
|
|
|
|
const [isRequestingPhone, setIsRequestingPhone] = useState(false);
|
|
const [isRequestingWriteAccess, setIsRequestingWriteAccess] = useState(false);
|
|
const [clipboardRequestId, setClipboardRequestId] = useState<string>();
|
|
const [requestedFileDownload, setRequestedFileDownload] = useState<{ url: string; fileName: string }>();
|
|
const [bottomBarColor, setBottomBarColor] = useState<string>();
|
|
const {
|
|
unlockPopupsAt, handlePopupOpened, handlePopupClosed,
|
|
} = usePopupLimit(POPUP_SEQUENTIAL_LIMIT, POPUP_RESET_DELAY);
|
|
|
|
const containerRef = useRef<HTMLDivElement>();
|
|
|
|
const headerButtonRef = useRef<HTMLDivElement>();
|
|
|
|
const headerButtonCaptionRef = useRef<HTMLDivElement>();
|
|
|
|
const isFullscreen = modalState === 'fullScreen';
|
|
const isMinimizedState = modalState === 'minimized';
|
|
|
|
const exitFullScreenCallback = useLastCallback(() => {
|
|
setTimeout(() => {
|
|
changeWebAppModalState({ state: 'maximized' });
|
|
}, COLLAPSING_WAIT);
|
|
});
|
|
|
|
const fullscreenElementRef = useRef<HTMLElement>();
|
|
|
|
useEffect(() => {
|
|
fullscreenElementRef.current = document.querySelector('#portals') as HTMLElement;
|
|
}, []);
|
|
|
|
const [, setFullscreen, exitFullscreen] = useFullscreen(fullscreenElementRef, exitFullScreenCallback);
|
|
|
|
const activeWebApp = modal?.activeWebAppKey ? modal.openedWebApps[modal.activeWebAppKey] : undefined;
|
|
const { appName: activeWebAppName, backgroundColor } = activeWebApp || {};
|
|
const {
|
|
url, buttonText, isBackButtonVisible,
|
|
} = webApp || {};
|
|
|
|
const {
|
|
placeholderPath,
|
|
} = botAppSettings || {};
|
|
|
|
const isCloseModalOpen = Boolean(webApp?.isCloseModalOpen);
|
|
const isRemoveModalOpen = Boolean(webApp?.isRemoveModalOpen);
|
|
|
|
const webAppKey = webApp && getWebAppKey(webApp);
|
|
const activeWebAppKey = activeWebApp && getWebAppKey(activeWebApp);
|
|
|
|
const isActive = (activeWebApp && webApp) && activeWebAppKey === webAppKey;
|
|
|
|
const isAvailable = IS_GEOLOCATION_SUPPORTED;
|
|
const isAccessRequested = botAppPermissions?.geolocation !== undefined;
|
|
const isAccessGranted = Boolean(botAppPermissions?.geolocation);
|
|
|
|
const updateCurrentWebApp = useLastCallback((updatedPartialWebApp: Partial<WebApp>) => {
|
|
if (!webAppKey) return;
|
|
updateWebApp({ key: webAppKey, update: updatedPartialWebApp });
|
|
});
|
|
|
|
const themeParams = useMemo(() => {
|
|
return extractCurrentThemeParams();
|
|
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
|
|
}, [theme]);
|
|
|
|
useEffect(() => {
|
|
setBottomBarColor(themeParams.secondary_bg_color);
|
|
}, [themeParams]);
|
|
|
|
const themeBackgroundColor = themeParams.bg_color;
|
|
const [backgroundColorFromEvent, setBackgroundColorFromEvent] = useState<string>();
|
|
const backgroundColorFromSettings = theme === 'light' ? botAppSettings?.backgroundColor
|
|
: botAppSettings?.backgroundDarkColor;
|
|
|
|
useEffect(() => {
|
|
const color = backgroundColorFromEvent || backgroundColorFromSettings || themeBackgroundColor;
|
|
|
|
updateCurrentWebApp({ backgroundColor: color });
|
|
}, [themeBackgroundColor, backgroundColorFromEvent, backgroundColorFromSettings]);
|
|
|
|
const themeHeaderColor = themeParams.bg_color;
|
|
const [headerColorFromEvent, setHeaderColorFromEvent] = useState<string>();
|
|
const headerColorFromSettings = theme === 'light' ? botAppSettings?.headerColor
|
|
: botAppSettings?.headerDarkColor;
|
|
|
|
useEffect(() => {
|
|
const color = headerColorFromEvent || headerColorFromSettings || themeHeaderColor;
|
|
|
|
updateCurrentWebApp({ headerColor: color });
|
|
}, [themeHeaderColor, headerColorFromEvent, headerColorFromSettings]);
|
|
|
|
const frameRef = useRef<HTMLIFrameElement>();
|
|
|
|
const oldLang = useOldLang();
|
|
const lang = useLang();
|
|
const isOpen = modal?.isModalOpen || false;
|
|
const isSimple = Boolean(buttonText);
|
|
|
|
const {
|
|
reloadFrame, sendEvent, sendFullScreenChanged, sendViewport, sendSafeArea, sendTheme,
|
|
} = useWebAppFrame(frameRef, isOpen, isFullscreen, isSimple, handleEvent, webApp, markLoaded);
|
|
|
|
useEffect(() => {
|
|
if (isActive) registerSendEventCallback(sendEvent);
|
|
}, [sendEvent, registerSendEventCallback, isActive]);
|
|
|
|
useEffect(() => {
|
|
if (isActive) registerReloadFrameCallback(reloadFrame);
|
|
}, [reloadFrame, registerReloadFrameCallback, isActive]);
|
|
|
|
function hasBottomButtonContent(text: string | undefined, iconCustomEmojiId?: string) {
|
|
return Boolean(text?.trim().length || iconCustomEmojiId);
|
|
}
|
|
|
|
const isMainButtonVisible = isLoaded && mainButton?.isVisible && hasBottomButtonContent(
|
|
mainButton.text,
|
|
mainButton.iconCustomEmojiId,
|
|
);
|
|
const isSecondaryButtonVisible = isLoaded && secondaryButton?.isVisible && hasBottomButtonContent(
|
|
secondaryButton.text,
|
|
secondaryButton.iconCustomEmojiId,
|
|
);
|
|
|
|
const handleHideCloseModal = useLastCallback(() => {
|
|
updateCurrentWebApp({ isCloseModalOpen: false });
|
|
});
|
|
const handleConfirmCloseModal = useLastCallback(() => {
|
|
updateCurrentWebApp({ shouldConfirmClosing: false, isCloseModalOpen: false });
|
|
setTimeout(() => {
|
|
closeActiveWebApp();
|
|
}, ANIMATION_WAIT);
|
|
});
|
|
|
|
const handleHideRemoveModal = useLastCallback(() => {
|
|
updateCurrentWebApp({ isRemoveModalOpen: false });
|
|
});
|
|
|
|
const handleMainButtonClick = useLastCallback(() => {
|
|
sendEvent({
|
|
eventType: 'main_button_pressed',
|
|
});
|
|
});
|
|
|
|
const handleSecondaryButtonClick = useLastCallback(() => {
|
|
sendEvent({
|
|
eventType: 'secondary_button_pressed',
|
|
});
|
|
});
|
|
|
|
const handleAppPopupClose = useLastCallback((buttonId?: string) => {
|
|
setPopupParameters(undefined);
|
|
handlePopupClosed();
|
|
sendEvent({
|
|
eventType: 'popup_closed',
|
|
eventData: {
|
|
button_id: buttonId,
|
|
},
|
|
});
|
|
});
|
|
|
|
const handleAppPopupModalClose = useLastCallback(() => {
|
|
handleAppPopupClose();
|
|
});
|
|
|
|
const sendThemeCallback = useLastCallback(() => {
|
|
sendTheme();
|
|
});
|
|
|
|
// Notify view that theme changed
|
|
useSyncEffect(() => {
|
|
setTimeout(() => {
|
|
sendThemeCallback();
|
|
}, ANIMATION_WAIT);
|
|
}, [theme]);
|
|
|
|
const setFullscreenCallback = useLastCallback(() => {
|
|
if (!checkIfFullscreen() && isActive) {
|
|
setFullscreen?.();
|
|
}
|
|
});
|
|
|
|
const exitIfFullscreenCallback = useLastCallback(() => {
|
|
if (checkIfFullscreen() && isActive) {
|
|
exitFullscreen?.();
|
|
}
|
|
});
|
|
|
|
const sendFullScreenChangedCallback = useLastCallback(
|
|
(value: boolean) => {
|
|
if (isActive) sendFullScreenChanged(value);
|
|
},
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (isFullscreen) {
|
|
setFullscreenCallback();
|
|
sendFullScreenChangedCallback(true);
|
|
} else {
|
|
exitIfFullscreenCallback();
|
|
sendFullScreenChangedCallback(false);
|
|
}
|
|
}, [isFullscreen]);
|
|
|
|
const visibilityChangedCallBack = useLastCallback((visibility: boolean) => {
|
|
sendEvent({
|
|
eventType: 'visibility_changed',
|
|
eventData: {
|
|
is_visible: visibility,
|
|
},
|
|
});
|
|
});
|
|
|
|
useEffect(() => {
|
|
if (isLoaded) {
|
|
visibilityChangedCallBack(Boolean(isActive));
|
|
}
|
|
}, [isActive, isLoaded]);
|
|
|
|
useEffectWithPrevDeps(([prevModalState]) => {
|
|
if (modalState === 'minimized') {
|
|
visibilityChangedCallBack(false);
|
|
}
|
|
if (modalState && prevModalState === 'minimized') {
|
|
visibilityChangedCallBack(true);
|
|
}
|
|
}, [modalState]);
|
|
|
|
useSyncEffect(([prevIsPaymentModalOpen]) => {
|
|
if (isPaymentModalOpen === prevIsPaymentModalOpen) return;
|
|
if (webApp?.slug && !isPaymentModalOpen && paymentStatus) {
|
|
sendEvent({
|
|
eventType: 'invoice_closed',
|
|
eventData: {
|
|
slug: webApp.slug,
|
|
status: paymentStatus,
|
|
},
|
|
});
|
|
setWebAppPaymentSlug({
|
|
slug: undefined,
|
|
});
|
|
resetPaymentStatus();
|
|
}
|
|
}, [isPaymentModalOpen, paymentStatus, sendEvent, webApp?.slug]);
|
|
|
|
const handleRemoveAttachBot = useLastCallback(() => {
|
|
toggleAttachBot({
|
|
botId: bot!.id,
|
|
isEnabled: false,
|
|
});
|
|
closeActiveWebApp();
|
|
});
|
|
|
|
const handleRejectPhone = useLastCallback(() => {
|
|
setIsRequestingPhone(false);
|
|
handlePopupClosed();
|
|
sendEvent({
|
|
eventType: 'phone_requested',
|
|
eventData: {
|
|
status: 'cancelled',
|
|
},
|
|
});
|
|
});
|
|
|
|
const handleAcceptPhone = useLastCallback(() => {
|
|
sharePhoneWithBot({ botId: bot!.id });
|
|
setIsRequestingPhone(false);
|
|
handlePopupClosed();
|
|
sendEvent({
|
|
eventType: 'phone_requested',
|
|
eventData: {
|
|
status: 'sent',
|
|
},
|
|
});
|
|
});
|
|
|
|
const handleRejectFileDownload = useLastCallback((shouldCloseActive?: boolean) => {
|
|
if (shouldCloseActive) {
|
|
setRequestedFileDownload(undefined);
|
|
handlePopupClosed();
|
|
}
|
|
|
|
sendEvent({
|
|
eventType: 'file_download_requested',
|
|
eventData: {
|
|
status: 'cancelled',
|
|
},
|
|
});
|
|
});
|
|
|
|
const handleRejectWriteAccess = useLastCallback(() => {
|
|
sendEvent({
|
|
eventType: 'write_access_requested',
|
|
eventData: {
|
|
status: 'cancelled',
|
|
},
|
|
});
|
|
setIsRequestingWriteAccess(false);
|
|
handlePopupClosed();
|
|
});
|
|
|
|
const handleAcceptWriteAccess = useLastCallback(async () => {
|
|
if (!bot) return;
|
|
const result = await callApi('allowBotSendMessages', { bot });
|
|
if (!result) {
|
|
handleRejectWriteAccess();
|
|
return;
|
|
}
|
|
|
|
sendEvent({
|
|
eventType: 'write_access_requested',
|
|
eventData: {
|
|
status: 'allowed',
|
|
},
|
|
});
|
|
setIsRequestingWriteAccess(false);
|
|
handlePopupClosed();
|
|
});
|
|
|
|
async function handleRequestWriteAccess() {
|
|
if (!bot) return;
|
|
const canWrite = await callApi('fetchBotCanSendMessage', {
|
|
bot,
|
|
});
|
|
|
|
if (canWrite) {
|
|
sendEvent({
|
|
eventType: 'write_access_requested',
|
|
eventData: {
|
|
status: 'allowed',
|
|
},
|
|
});
|
|
}
|
|
setIsRequestingWriteAccess(!canWrite);
|
|
}
|
|
|
|
const handleRejectClipboardText = useLastCallback(() => {
|
|
if (!clipboardRequestId) return;
|
|
setClipboardRequestId(undefined);
|
|
sendEvent({
|
|
eventType: 'clipboard_text_received',
|
|
eventData: {
|
|
req_id: clipboardRequestId,
|
|
// eslint-disable-next-line no-null/no-null
|
|
data: null,
|
|
},
|
|
});
|
|
});
|
|
|
|
const handleConfirmClipboardText = useLastCallback(() => {
|
|
const reqId = clipboardRequestId;
|
|
if (!reqId) return;
|
|
|
|
setClipboardRequestId(undefined);
|
|
window.navigator.clipboard.readText().then((clipboardText) => {
|
|
sendEvent({
|
|
eventType: 'clipboard_text_received',
|
|
eventData: {
|
|
req_id: reqId,
|
|
data: clipboardText,
|
|
},
|
|
});
|
|
}).catch(() => {
|
|
sendEvent({
|
|
eventType: 'clipboard_text_received',
|
|
eventData: {
|
|
req_id: reqId,
|
|
// eslint-disable-next-line no-null/no-null
|
|
data: null,
|
|
},
|
|
});
|
|
});
|
|
});
|
|
|
|
async function handleCheckDownloadFile(fileUrl: string, fileName: string) {
|
|
const canDownload = await callApi('checkBotDownloadFileParams', {
|
|
bot: bot!,
|
|
url: fileUrl,
|
|
fileName,
|
|
});
|
|
|
|
if (!canDownload) {
|
|
sendEvent({
|
|
eventType: 'file_download_requested',
|
|
eventData: {
|
|
status: 'cancelled',
|
|
},
|
|
});
|
|
return;
|
|
}
|
|
|
|
setRequestedFileDownload({ url: fileUrl, fileName });
|
|
handlePopupOpened();
|
|
}
|
|
|
|
const handleDownloadFile = useLastCallback(() => {
|
|
if (!requestedFileDownload) return;
|
|
setRequestedFileDownload(undefined);
|
|
handlePopupClosed();
|
|
|
|
download(requestedFileDownload.url, requestedFileDownload.fileName);
|
|
sendEvent({
|
|
eventType: 'file_download_requested',
|
|
eventData: {
|
|
status: 'downloading',
|
|
},
|
|
});
|
|
});
|
|
|
|
async function handleInvokeCustomMethod(requestId: string, method: string, parameters: string) {
|
|
const result = await callApi('invokeWebViewCustomMethod', {
|
|
bot: bot!,
|
|
customMethod: method,
|
|
parameters,
|
|
});
|
|
|
|
sendEvent({
|
|
eventType: 'custom_method_invoked',
|
|
eventData: {
|
|
req_id: requestId,
|
|
...result,
|
|
},
|
|
});
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (!isOpen) {
|
|
setPopupParameters(undefined);
|
|
setIsRequestingPhone(false);
|
|
setIsRequestingWriteAccess(false);
|
|
setMainButton(undefined);
|
|
setSecondaryButton(undefined);
|
|
setRequestedFileDownload(undefined);
|
|
setClipboardRequestId(undefined);
|
|
updateCurrentWebApp({
|
|
isSettingsButtonVisible: false,
|
|
shouldConfirmClosing: false,
|
|
isBackButtonVisible: false,
|
|
isCloseModalOpen: false,
|
|
isRemoveModalOpen: false,
|
|
});
|
|
markUnloaded();
|
|
}
|
|
}, [isOpen]);
|
|
|
|
const handleOpenChat = useLastCallback(() => {
|
|
openChatWithInfo({ id: bot!.id });
|
|
});
|
|
|
|
function handleEvent(event: WebAppInboundEvent) {
|
|
const { eventType, eventData } = event;
|
|
|
|
if (eventType === 'web_app_request_fullscreen') {
|
|
if (getIsWebAppsFullscreenSupported()) {
|
|
changeWebAppModalState({ state: 'fullScreen' });
|
|
} else {
|
|
sendEvent({
|
|
eventType: 'fullscreen_failed',
|
|
eventData: {
|
|
error: 'UNSUPPORTED',
|
|
},
|
|
});
|
|
}
|
|
}
|
|
|
|
if (eventType === 'web_app_exit_fullscreen') {
|
|
exitIfFullscreenCallback();
|
|
}
|
|
|
|
if (eventType === 'web_app_open_tg_link') {
|
|
changeWebAppModalState({ state: 'minimized' });
|
|
|
|
const linkUrl = TME_LINK_PREFIX + eventData.path_full;
|
|
openTelegramLink({ url: linkUrl, shouldIgnoreCache: eventData.force_request });
|
|
}
|
|
|
|
if (eventType === 'web_app_setup_back_button') {
|
|
updateCurrentWebApp({ isBackButtonVisible: eventData.is_visible });
|
|
}
|
|
|
|
if (eventType === 'web_app_setup_settings_button') {
|
|
updateCurrentWebApp({ isSettingsButtonVisible: eventData.is_visible });
|
|
}
|
|
|
|
if (eventType === 'web_app_set_background_color') {
|
|
setBackgroundColorFromEvent(validateHexColor(eventData.color) ? eventData.color : undefined);
|
|
}
|
|
|
|
if (eventType === 'web_app_set_header_color') {
|
|
const key = eventData.color_key;
|
|
setHeaderColorFromEvent(eventData.color || (key ? themeParams[key] : undefined));
|
|
}
|
|
|
|
if (eventType === 'web_app_set_bottom_bar_color') {
|
|
setBottomBarColor(eventData.color);
|
|
}
|
|
|
|
if (eventType === 'web_app_data_send') {
|
|
closeActiveWebApp();
|
|
sendWebViewData({
|
|
bot: bot!,
|
|
buttonText: buttonText!,
|
|
data: eventData.data,
|
|
});
|
|
}
|
|
|
|
if (eventType === 'web_app_setup_main_button') {
|
|
const color = eventData.color;
|
|
const textColor = eventData.text_color;
|
|
setMainButton({
|
|
isVisible: eventData.is_visible && hasBottomButtonContent(eventData.text, eventData.icon_custom_emoji_id),
|
|
isActive: eventData.is_active,
|
|
text: eventData.text,
|
|
color,
|
|
textColor,
|
|
isProgressVisible: eventData.is_progress_visible,
|
|
iconCustomEmojiId: eventData.icon_custom_emoji_id,
|
|
hasShineEffect: eventData.has_shine_effect,
|
|
});
|
|
}
|
|
|
|
if (eventType === 'web_app_setup_secondary_button') {
|
|
const color = eventData.color;
|
|
const textColor = eventData.text_color;
|
|
setSecondaryButton({
|
|
isVisible: eventData.is_visible && hasBottomButtonContent(eventData.text, eventData.icon_custom_emoji_id),
|
|
isActive: eventData.is_active,
|
|
text: eventData.text,
|
|
color,
|
|
textColor,
|
|
isProgressVisible: eventData.is_progress_visible,
|
|
iconCustomEmojiId: eventData.icon_custom_emoji_id,
|
|
hasShineEffect: eventData.has_shine_effect,
|
|
position: eventData.position,
|
|
});
|
|
}
|
|
|
|
if (eventType === 'web_app_setup_closing_behavior') {
|
|
updateCurrentWebApp({ shouldConfirmClosing: eventData.need_confirmation });
|
|
}
|
|
|
|
if (eventType === 'web_app_open_popup') {
|
|
if (popupParameters || !eventData.message.trim().length || !eventData.buttons?.length
|
|
|| eventData.buttons.length > 3 || isRequestingPhone || isRequestingWriteAccess
|
|
|| unlockPopupsAt > Date.now()) {
|
|
handleAppPopupClose(undefined);
|
|
return;
|
|
}
|
|
|
|
setPopupParameters(eventData);
|
|
handlePopupOpened();
|
|
}
|
|
|
|
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,
|
|
});
|
|
|
|
closeActiveWebApp();
|
|
}
|
|
|
|
if (eventType === 'web_app_request_phone') {
|
|
if (popupParameters || isRequestingWriteAccess || unlockPopupsAt > Date.now()) {
|
|
handleRejectPhone();
|
|
return;
|
|
}
|
|
|
|
setIsRequestingPhone(true);
|
|
handlePopupOpened();
|
|
}
|
|
|
|
if (eventType === 'web_app_request_write_access') {
|
|
if (popupParameters || isRequestingPhone || unlockPopupsAt > Date.now()) {
|
|
handleRejectWriteAccess();
|
|
return;
|
|
}
|
|
|
|
handleRequestWriteAccess();
|
|
handlePopupOpened();
|
|
}
|
|
|
|
if (eventType === 'web_app_invoke_custom_method') {
|
|
const { method, params, req_id: requestId } = eventData;
|
|
handleInvokeCustomMethod(requestId, method, JSON.stringify(params));
|
|
}
|
|
|
|
if (eventType === 'web_app_request_file_download') {
|
|
if (requestedFileDownload || unlockPopupsAt > Date.now()) {
|
|
handleRejectFileDownload();
|
|
return;
|
|
}
|
|
handleCheckDownloadFile(eventData.url, eventData.file_name);
|
|
}
|
|
|
|
if (eventType === 'web_app_send_prepared_message') {
|
|
if (!bot || !webAppKey) return;
|
|
const { id } = eventData;
|
|
openPreparedInlineMessageModal({ botId: bot.id, messageId: id, webAppKey });
|
|
}
|
|
|
|
if (eventType === 'web_app_request_emoji_status_access') {
|
|
if (!bot) return;
|
|
openEmojiStatusAccessModal({ bot, webAppKey });
|
|
}
|
|
|
|
if (eventType === 'web_app_check_location') {
|
|
const handleGeolocationCheck = () => {
|
|
sendEvent({
|
|
eventType: 'location_checked',
|
|
eventData: {
|
|
available: isAvailable,
|
|
access_requested: isAccessRequested,
|
|
access_granted: isAccessGranted,
|
|
},
|
|
});
|
|
};
|
|
|
|
handleGeolocationCheck();
|
|
}
|
|
|
|
if (eventType === 'web_app_request_location') {
|
|
const handleRequestLocation = async () => {
|
|
const geolocationData = await getGeolocationStatus();
|
|
const { accessRequested, accessGranted, geolocation } = geolocationData;
|
|
|
|
if (!accessGranted || !accessRequested) {
|
|
sendEvent({
|
|
eventType: 'location_requested',
|
|
eventData: {
|
|
available: false,
|
|
},
|
|
});
|
|
showNotification({ message: oldLang('PermissionNoLocationPosition') });
|
|
handleAppPopupClose(undefined);
|
|
return;
|
|
}
|
|
|
|
if (isAvailable) {
|
|
if (isAccessRequested) {
|
|
sendEvent({
|
|
eventType: 'location_requested',
|
|
eventData: {
|
|
available: Boolean(botAppPermissions?.geolocation),
|
|
latitude: geolocation?.latitude,
|
|
longitude: geolocation?.longitude,
|
|
altitude: geolocation?.altitude,
|
|
course: geolocation?.heading,
|
|
speed: geolocation?.speed,
|
|
horizontal_accuracy: geolocation?.accuracy,
|
|
vertical_accuracy: geolocation?.altitudeAccuracy,
|
|
},
|
|
});
|
|
} else {
|
|
openLocationAccessModal({ bot, webAppKey });
|
|
}
|
|
} else {
|
|
showNotification({ message: oldLang('PermissionNoLocationPosition') });
|
|
handleAppPopupClose(undefined);
|
|
}
|
|
};
|
|
|
|
handleRequestLocation();
|
|
}
|
|
|
|
if (eventType === 'web_app_open_location_settings') {
|
|
handleOpenChat();
|
|
}
|
|
|
|
if (eventType === 'web_app_read_text_from_clipboard') {
|
|
setClipboardRequestId(eventData.req_id);
|
|
}
|
|
|
|
if (eventType === 'web_app_verify_age') {
|
|
if (!bot?.usernames?.some((username) => username.username === verifyAgeBotUsername)) return;
|
|
|
|
const { passed } = eventData;
|
|
const minAge = verifyAgeMin;
|
|
const ageFromParam = eventData.age || 0;
|
|
|
|
if (passed && ageFromParam >= minAge) {
|
|
showNotification({
|
|
message: {
|
|
key: 'TitleAgeCheckSuccess',
|
|
},
|
|
});
|
|
updateContentSettings({ isSensitiveEnabled: true });
|
|
} else {
|
|
showNotification({
|
|
message: {
|
|
key: 'TitleAgeCheckFailed',
|
|
},
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
const mainButtonCurrentColor = useCurrentOrPrev(mainButton?.color, true);
|
|
const mainButtonCurrentTextColor = useCurrentOrPrev(mainButton?.textColor, true);
|
|
const mainButtonCurrentIsActive = useCurrentOrPrev(mainButton && Boolean(mainButton.isActive), true);
|
|
const mainButtonCurrentText = useCurrentOrPrev(mainButton?.text, true);
|
|
|
|
const secondaryButtonCurrentPosition = useCurrentOrPrev(secondaryButton?.position, true);
|
|
const secondaryButtonCurrentColor = useCurrentOrPrev(secondaryButton?.color, true);
|
|
const secondaryButtonCurrentTextColor = useCurrentOrPrev(secondaryButton?.textColor, true);
|
|
const secondaryButtonCurrentIsActive = useCurrentOrPrev(secondaryButton && Boolean(secondaryButton.isActive), true);
|
|
const secondaryButtonCurrentText = useCurrentOrPrev(secondaryButton?.text, true);
|
|
|
|
const [shouldDecreaseWebFrameSize, setShouldDecreaseWebFrameSize] = useState(false);
|
|
const [shouldHideMainButton, setShouldHideMainButton] = useState(true);
|
|
const [shouldHideSecondaryButton, setShouldHideSecondaryButton] = useState(true);
|
|
const [shouldShowMainButton, setShouldShowMainButton] = useState(false);
|
|
const [shouldShowSecondaryButton, setShouldShowSecondaryButton] = useState(false);
|
|
|
|
const [shouldShowAppNameInFullscreen, setShouldShowAppNameInFullscreen] = useState(false);
|
|
|
|
const [backButtonTextWidth, setBackButtonTextWidth] = useState(0);
|
|
|
|
// Notify view that height changed
|
|
useSyncEffect(() => {
|
|
setTimeout(() => {
|
|
sendViewport();
|
|
sendSafeArea();
|
|
}, isTransforming ? 0 : ANIMATION_WAIT);
|
|
}, [shouldShowSecondaryButton, shouldHideSecondaryButton,
|
|
shouldShowMainButton, shouldShowMainButton,
|
|
secondaryButton?.position, sendViewport, isTransforming, modalHeight,
|
|
sendSafeArea]);
|
|
|
|
const isVerticalLayout = secondaryButtonCurrentPosition === 'top' || secondaryButtonCurrentPosition === 'bottom';
|
|
const isHorizontalLayout = !isVerticalLayout;
|
|
|
|
const rowsCount = (isVerticalLayout && shouldShowMainButton && shouldShowSecondaryButton) ? 2
|
|
: shouldShowMainButton || shouldShowSecondaryButton ? 1 : 0;
|
|
|
|
const hideDirection = (isHorizontalLayout
|
|
&& (!shouldHideMainButton && !shouldHideSecondaryButton)) ? 'horizontal' : 'vertical';
|
|
|
|
const mainButtonChangeTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
|
const mainButtonFastTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
|
const secondaryButtonChangeTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
|
const secondaryButtonFastTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
|
const appNameDisplayTimeoutRef = useRef<ReturnType<typeof setTimeout>>();
|
|
|
|
useEffect(() => {
|
|
if (isFullscreen && isOpen && Boolean(activeWebAppName)) {
|
|
setShouldShowAppNameInFullscreen(true);
|
|
|
|
if (appNameDisplayTimeoutRef.current) {
|
|
clearTimeout(appNameDisplayTimeoutRef.current);
|
|
}
|
|
|
|
appNameDisplayTimeoutRef.current = setTimeout(() => {
|
|
setShouldShowAppNameInFullscreen(false);
|
|
appNameDisplayTimeoutRef.current = undefined;
|
|
}, APP_NAME_DISPLAY_DURATION);
|
|
} else {
|
|
setShouldShowAppNameInFullscreen(false);
|
|
|
|
if (appNameDisplayTimeoutRef.current) {
|
|
clearTimeout(appNameDisplayTimeoutRef.current);
|
|
appNameDisplayTimeoutRef.current = undefined;
|
|
}
|
|
}
|
|
|
|
return () => {
|
|
if (appNameDisplayTimeoutRef.current) {
|
|
clearTimeout(appNameDisplayTimeoutRef.current);
|
|
}
|
|
};
|
|
}, [isFullscreen, isOpen, activeWebAppName]);
|
|
|
|
useEffect(() => {
|
|
if (mainButtonChangeTimeoutRef.current) clearTimeout(mainButtonChangeTimeoutRef.current);
|
|
if (mainButtonFastTimeoutRef.current) clearTimeout(mainButtonFastTimeoutRef.current);
|
|
|
|
if (isMainButtonVisible) {
|
|
mainButtonFastTimeoutRef.current = setTimeout(() => {
|
|
setShouldShowMainButton(true);
|
|
}, 35);
|
|
setShouldHideMainButton(false);
|
|
mainButtonChangeTimeoutRef.current = setTimeout(() => {
|
|
setShouldDecreaseWebFrameSize(true);
|
|
}, MAIN_BUTTON_ANIMATION_TIME);
|
|
}
|
|
|
|
if (!isMainButtonVisible) {
|
|
setShouldShowMainButton(false);
|
|
mainButtonChangeTimeoutRef.current = setTimeout(() => {
|
|
setShouldHideMainButton(true);
|
|
}, MAIN_BUTTON_ANIMATION_TIME);
|
|
}
|
|
}, [isMainButtonVisible]);
|
|
|
|
useEffect(() => {
|
|
if (secondaryButtonChangeTimeoutRef.current) clearTimeout(secondaryButtonChangeTimeoutRef.current);
|
|
if (secondaryButtonFastTimeoutRef.current) clearTimeout(secondaryButtonFastTimeoutRef.current);
|
|
|
|
if (isSecondaryButtonVisible) {
|
|
secondaryButtonFastTimeoutRef.current = setTimeout(() => {
|
|
setShouldShowSecondaryButton(true);
|
|
}, 35);
|
|
setShouldHideSecondaryButton(false);
|
|
secondaryButtonChangeTimeoutRef.current = setTimeout(() => {
|
|
setShouldDecreaseWebFrameSize(true);
|
|
}, MAIN_BUTTON_ANIMATION_TIME);
|
|
}
|
|
|
|
if (!isSecondaryButtonVisible) {
|
|
setShouldShowSecondaryButton(false);
|
|
secondaryButtonChangeTimeoutRef.current = setTimeout(() => {
|
|
setShouldHideSecondaryButton(true);
|
|
}, MAIN_BUTTON_ANIMATION_TIME);
|
|
}
|
|
}, [isSecondaryButtonVisible]);
|
|
|
|
useEffect(() => {
|
|
if (!shouldShowSecondaryButton && !shouldShowMainButton) {
|
|
setShouldDecreaseWebFrameSize(false);
|
|
}
|
|
}, [setShouldDecreaseWebFrameSize, shouldShowSecondaryButton, shouldShowMainButton]);
|
|
|
|
const frameStyle = buildStyle(
|
|
`background-color: ${backgroundColor || 'var(--color-background)'}`,
|
|
isTransforming && 'pointer-events: none;',
|
|
);
|
|
|
|
const handleBackClick = useLastCallback(() => {
|
|
if (isBackButtonVisible) {
|
|
sendEvent({
|
|
eventType: 'back_button_pressed',
|
|
});
|
|
} else {
|
|
exitIfFullscreenCallback();
|
|
sendFullScreenChanged(false);
|
|
changeWebAppModalState({ state: 'maximized' });
|
|
closeWebAppModal();
|
|
}
|
|
});
|
|
|
|
const handleCollapseClick = useLastCallback(() => {
|
|
exitIfFullscreenCallback();
|
|
});
|
|
|
|
const handleShowContextMenu = useLastCallback((e: React.MouseEvent) => {
|
|
onContextMenuButtonClick(e);
|
|
});
|
|
|
|
const backIconClass = buildClassName(
|
|
styles.closeIcon,
|
|
isBackButtonVisible && styles.stateBack,
|
|
);
|
|
const backButtonCaption = shouldShowAppNameInFullscreen ? activeWebAppName
|
|
: oldLang(isBackButtonVisible ? 'Back' : 'Close');
|
|
|
|
const hasHeaderElement = headerButtonCaptionRef?.current;
|
|
|
|
useEffect(() => {
|
|
const width = headerButtonCaptionRef?.current?.clientWidth || 0;
|
|
setBackButtonTextWidth(width);
|
|
}, [backButtonCaption, hasHeaderElement]);
|
|
|
|
function getBackButtonActiveKey() {
|
|
if (shouldShowAppNameInFullscreen) return 0;
|
|
return isBackButtonVisible ? 1 : 2;
|
|
}
|
|
|
|
function renderFullscreenBackButtonCaption() {
|
|
return (
|
|
<span
|
|
className={styles.buttonCaptionContainer}
|
|
style={
|
|
`width: ${backButtonTextWidth}px;`
|
|
}
|
|
>
|
|
<Transition
|
|
activeKey={getBackButtonActiveKey()}
|
|
name="slideFade"
|
|
>
|
|
<div
|
|
ref={headerButtonCaptionRef}
|
|
className={styles.backButtonCaption}
|
|
>
|
|
{backButtonCaption}
|
|
</div>
|
|
</Transition>
|
|
</span>
|
|
);
|
|
}
|
|
|
|
function renderFullscreenHeaderPanel() {
|
|
return (
|
|
<div className={styles.headerPanel}>
|
|
<div ref={headerButtonRef} className={styles.headerButton} onClick={handleBackClick}>
|
|
<div className={styles.backIconContainer}>
|
|
<div className={backIconClass} />
|
|
</div>
|
|
{renderFullscreenBackButtonCaption()}
|
|
</div>
|
|
<div className={styles.headerSplitButton}>
|
|
<div
|
|
className={buildClassName(
|
|
styles.headerButton,
|
|
styles.left,
|
|
)}
|
|
tabIndex={0}
|
|
role="button"
|
|
aria-label={lang('WebAppCollapse')}
|
|
onClick={handleCollapseClick}
|
|
>
|
|
<Icon
|
|
name="down"
|
|
className={styles.icon}
|
|
/>
|
|
</div>
|
|
<div
|
|
className={buildClassName(
|
|
styles.headerButton,
|
|
styles.right,
|
|
)}
|
|
tabIndex={0}
|
|
role="button"
|
|
aria-haspopup="menu"
|
|
aria-label={lang('AriaMoreButton')}
|
|
onClick={handleShowContextMenu}
|
|
>
|
|
<Icon
|
|
name="more"
|
|
className={buildClassName(
|
|
styles.icon,
|
|
styles.moreIcon,
|
|
)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function renderDefaultPlaceholder() {
|
|
const className = buildClassName(styles.loadingPlaceholder, styles.defaultPlaceholderGrid, isLoaded && styles.hide);
|
|
return (
|
|
<div className={className}>
|
|
<div className={styles.placeholderSquare} />
|
|
<div className={styles.placeholderSquare} />
|
|
<div className={styles.placeholderSquare} />
|
|
<div className={styles.placeholderSquare} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function renderPlaceholder() {
|
|
if (!placeholderPath) {
|
|
return renderDefaultPlaceholder();
|
|
}
|
|
return (
|
|
<svg
|
|
className={buildClassName(styles.loadingPlaceholder, isLoaded && styles.hide)}
|
|
viewBox="0 0 512 512"
|
|
>
|
|
<path className={styles.placeholderPath} d={placeholderPath} />
|
|
</svg>
|
|
);
|
|
}
|
|
|
|
function renderBottomButtonContent(text: string | undefined, iconCustomEmojiId?: string) {
|
|
const hasText = Boolean(text?.trim().length);
|
|
if (!hasText && !iconCustomEmojiId) return undefined;
|
|
|
|
const textContent = hasText ? renderText(text, ['emoji']) : undefined;
|
|
if (!iconCustomEmojiId) {
|
|
return textContent;
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<CustomEmoji
|
|
className={buildClassName(styles.buttonEmoji, hasText && styles.buttonEmojiWithLabel)}
|
|
documentId={iconCustomEmojiId}
|
|
size={1.25 * REM}
|
|
forceAlways
|
|
/>
|
|
{hasText && (
|
|
<span className={styles.buttonLabel}>
|
|
{textContent}
|
|
</span>
|
|
)}
|
|
</>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div
|
|
ref={containerRef}
|
|
className={buildClassName(
|
|
styles.root,
|
|
!isActive && styles.hidden,
|
|
isMultiTabSupported && styles.multiTab,
|
|
)}
|
|
>
|
|
{isFullscreen && getIsWebAppsFullscreenSupported() && renderFullscreenHeaderPanel()}
|
|
{!isMinimizedState && renderPlaceholder()}
|
|
<iframe
|
|
className={buildClassName(
|
|
styles.frame,
|
|
shouldDecreaseWebFrameSize && styles.withButton,
|
|
!isLoaded && styles.hide,
|
|
)}
|
|
style={frameStyle}
|
|
src={url}
|
|
title={lang('AriaMiniApp', { bot: bot?.firstName })}
|
|
sandbox={SANDBOX_ATTRIBUTES}
|
|
allow="camera; microphone; geolocation; clipboard-write;"
|
|
allowFullScreen
|
|
ref={frameRef}
|
|
/>
|
|
{!isMinimizedState && (
|
|
<div
|
|
style={`background-color: ${bottomBarColor};`}
|
|
className={buildClassName(
|
|
styles.buttonsContainer,
|
|
secondaryButtonCurrentPosition === 'left' && styles.leftToRight,
|
|
secondaryButtonCurrentPosition === 'right' && styles.rightToLeft,
|
|
secondaryButtonCurrentPosition === 'top' && styles.topToBottom,
|
|
secondaryButtonCurrentPosition === 'bottom' && styles.bottomToTop,
|
|
hideDirection === 'horizontal' && styles.hideHorizontal,
|
|
rowsCount === 1 && styles.oneRow,
|
|
rowsCount === 2 && styles.twoRows,
|
|
)}
|
|
>
|
|
<Button
|
|
className={buildClassName(
|
|
styles.secondaryButton,
|
|
shouldShowSecondaryButton && !shouldHideSecondaryButton && styles.visible,
|
|
shouldHideSecondaryButton && styles.hidden,
|
|
)}
|
|
fluid
|
|
style={`background-color: ${secondaryButtonCurrentColor}; color: ${secondaryButtonCurrentTextColor}`}
|
|
disabled={!secondaryButtonCurrentIsActive && !secondaryButton?.isProgressVisible}
|
|
nonInteractive={secondaryButton?.isProgressVisible}
|
|
isShiny={secondaryButton?.hasShineEffect && !secondaryButton?.isProgressVisible}
|
|
onClick={handleSecondaryButtonClick}
|
|
>
|
|
{!secondaryButton?.isProgressVisible && renderBottomButtonContent(
|
|
secondaryButtonCurrentText,
|
|
secondaryButton?.iconCustomEmojiId,
|
|
)}
|
|
{secondaryButton?.isProgressVisible
|
|
&& <Spinner className={styles.mainButtonSpinner} color="blue" />}
|
|
</Button>
|
|
<Button
|
|
className={buildClassName(
|
|
styles.mainButton,
|
|
shouldShowMainButton && !shouldHideMainButton && styles.visible,
|
|
shouldHideMainButton && styles.hidden,
|
|
)}
|
|
fluid
|
|
style={`background-color: ${mainButtonCurrentColor}; color: ${mainButtonCurrentTextColor}`}
|
|
disabled={!mainButtonCurrentIsActive && !mainButton?.isProgressVisible}
|
|
nonInteractive={mainButton?.isProgressVisible}
|
|
isShiny={mainButton?.hasShineEffect && !mainButton?.isProgressVisible}
|
|
onClick={handleMainButtonClick}
|
|
>
|
|
{!mainButton?.isProgressVisible && renderBottomButtonContent(
|
|
mainButtonCurrentText,
|
|
mainButton?.iconCustomEmojiId,
|
|
)}
|
|
{mainButton?.isProgressVisible && <Spinner className={styles.mainButtonSpinner} color="white" />}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
<Modal
|
|
isOpen={Boolean(popupParameters)}
|
|
title={renderingPopupParameters?.title || NBSP}
|
|
className={
|
|
buildClassName(styles.webAppPopup, !renderingPopupParameters?.title?.trim().length && styles.withoutTitle)
|
|
}
|
|
hasCloseButton
|
|
onClose={handleAppPopupModalClose}
|
|
>
|
|
{renderingPopupParameters?.message}
|
|
<div className="dialog-buttons mt-2">
|
|
{renderingPopupParameters?.buttons.map((button) => (
|
|
<Button
|
|
key={button.id || button.type}
|
|
className="confirm-dialog-button"
|
|
color={button.type === 'destructive' ? 'danger' : 'primary'}
|
|
isText
|
|
onClick={() => handleAppPopupClose(button.id)}
|
|
>
|
|
{button.text || oldLang(DEFAULT_BUTTON_TEXT[button.type])}
|
|
</Button>
|
|
))}
|
|
</div>
|
|
</Modal>
|
|
|
|
<ConfirmDialog
|
|
isOpen={isRequestingPhone}
|
|
onClose={handleRejectPhone}
|
|
title={lang('ShareYouPhoneNumberTitle')}
|
|
textParts={lang(
|
|
'AreYouSureShareMyContactInfoBot',
|
|
undefined,
|
|
{ withNodes: true, withMarkdown: true, renderTextFilters: ['br'],
|
|
})}
|
|
confirmHandler={handleAcceptPhone}
|
|
confirmLabel={lang('ContactShare')}
|
|
/>
|
|
<ConfirmDialog
|
|
isOpen={isRequestingWriteAccess}
|
|
onClose={handleRejectWriteAccess}
|
|
title={oldLang('lng_bot_allow_write_title')}
|
|
text={oldLang('lng_bot_allow_write')}
|
|
confirmHandler={handleAcceptWriteAccess}
|
|
confirmLabel={oldLang('lng_bot_allow_write_confirm')}
|
|
/>
|
|
<ConfirmDialog
|
|
isOpen={Boolean(requestedFileDownload)}
|
|
title={lang('BotDownloadFileTitle')}
|
|
textParts={lang('BotDownloadFileDescription', {
|
|
bot: bot?.firstName,
|
|
filename: requestedFileDownload?.fileName,
|
|
}, {
|
|
withNodes: true,
|
|
withMarkdown: true,
|
|
})}
|
|
confirmLabel={lang('BotDownloadFileButton')}
|
|
onClose={handleRejectFileDownload}
|
|
confirmHandler={handleDownloadFile}
|
|
/>
|
|
|
|
<ConfirmDialog
|
|
isOpen={isCloseModalOpen}
|
|
onClose={handleHideCloseModal}
|
|
title={oldLang('lng_bot_close_warning_title')}
|
|
text={oldLang('lng_bot_close_warning')}
|
|
confirmHandler={handleConfirmCloseModal}
|
|
confirmIsDestructive
|
|
confirmLabel={oldLang('lng_bot_close_warning_sure')}
|
|
/>
|
|
<ConfirmDialog
|
|
isOpen={isRemoveModalOpen}
|
|
onClose={handleHideRemoveModal}
|
|
title={oldLang('BotRemoveFromMenuTitle')}
|
|
textParts={renderText(oldLang('BotRemoveFromMenu', bot?.firstName), ['simple_markdown'])}
|
|
confirmHandler={handleRemoveAttachBot}
|
|
confirmIsDestructive
|
|
/>
|
|
<ConfirmDialog
|
|
isOpen={Boolean(clipboardRequestId)}
|
|
title={lang('BotReadTextFromClipboardTitle')}
|
|
text={lang('BotReadTextFromClipboardDescription', { bot: getUserFullName(bot) })}
|
|
confirmLabel={lang('BotReadTextFromClipboardConfirm')}
|
|
onClose={handleRejectClipboardText}
|
|
confirmHandler={handleConfirmClipboardText}
|
|
/>
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default memo(withGlobal<OwnProps>(
|
|
(global, { modal }): Complete<StateProps> => {
|
|
const activeWebApp = modal?.activeWebAppKey ? selectWebApp(global, modal.activeWebAppKey) : undefined;
|
|
const { botId: activeBotId } = activeWebApp || {};
|
|
const modalState = modal?.modalState;
|
|
const { verifyAgeMin, verifyAgeBotUsername } = global.appConfig;
|
|
|
|
const attachBot = activeBotId ? global.attachMenu.bots[activeBotId] : undefined;
|
|
const bot = activeBotId ? selectUser(global, activeBotId) : undefined;
|
|
const userFullInfo = activeBotId ? selectUserFullInfo(global, activeBotId) : undefined;
|
|
const botAppSettings = userFullInfo?.botInfo?.appSettings;
|
|
const currentUser = global.currentUserId ? selectUser(global, global.currentUserId) : undefined;
|
|
const theme = selectTheme(global);
|
|
const { isPaymentModalOpen, status: regularPaymentStatus } = selectTabState(global).payment;
|
|
const { status: starsPaymentStatus, inputInvoice: starsInputInvoice } = selectTabState(global).starsPayment;
|
|
const botAppPermissions = bot ? selectBotAppPermissions(global, bot.id) : undefined;
|
|
|
|
const paymentStatus = starsPaymentStatus || regularPaymentStatus;
|
|
|
|
return {
|
|
attachBot,
|
|
bot,
|
|
currentUser,
|
|
theme,
|
|
isPaymentModalOpen: isPaymentModalOpen || Boolean(starsInputInvoice),
|
|
paymentStatus,
|
|
modalState,
|
|
botAppPermissions,
|
|
botAppSettings,
|
|
verifyAgeMin,
|
|
verifyAgeBotUsername,
|
|
};
|
|
},
|
|
)(WebAppModalTabContent));
|