TelegramPWA/src/components/main/WebAppModal.tsx
2022-04-19 15:12:16 +02:00

293 lines
8.8 KiB
TypeScript

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<OwnProps & StateProps> = ({
webApp,
chat,
isInstalled,
theme,
}) => {
const {
closeWebApp, sendWebViewData, prolongWebView, toggleBotInAttachMenu,
} = getActions();
const [mainButton, setMainButton] = useState<WebAppButton | undefined>();
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 }) => (
<Button
round
ripple={!IS_SINGLE_COLUMN_LAYOUT}
size="smaller"
color="translucent"
className={isMenuOpen ? 'active' : ''}
onClick={onTrigger}
ariaLabel="More actions"
>
<i className="icon-more" />
</Button>
);
}, []);
const header = useMemo(() => {
return (
<div className="modal-header">
<Button
round
color="translucent"
size="smaller"
ariaLabel={lang('Close')}
onClick={closeWebApp}
>
<i className="icon-close" />
</Button>
<div className="modal-title">{bot?.firstName}</div>
<DropdownMenu
className="web-app-more-menu"
trigger={MoreMenuButton}
positionX="right"
>
<MenuItem icon="reload" onClick={handleRefreshClick}>{lang('WebApp.ReloadPage')}</MenuItem>
{bot?.isAttachMenuBot && (
<MenuItem icon={isInstalled ? 'stop' : 'install'} onClick={handleToggleClick} destructive={isInstalled}>
{lang(isInstalled ? 'WebApp.RemoveBot' : 'WebApp.AddToAttachmentAdd')}
</MenuItem>
)}
</DropdownMenu>
</div>
);
}, [
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<ReturnType<typeof setTimeout>>();
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 (
<Modal
className="WebAppModal"
isOpen={isOpen}
onClose={closeWebApp}
header={header}
hasCloseButton
>
{isOpen && (
<>
<iframe
ref={ref}
className={buildClassName('web-app-frame', shouldDecreaseWebFrameSize && 'with-button')}
src={url}
title={`${bot?.firstName} Web App`}
sandbox="allow-scripts allow-same-origin allow-popups allow-forms"
allow="camera; microphone; geolocation;"
allowFullScreen
/>
<Button
className={buildClassName(
'web-app-button',
shouldShowMainButton && 'visible',
shouldHideButton && 'hidden',
)}
style={`background-color: ${mainButtonCurrentColor}; color: ${mainButtonCurrentTextColor}`}
disabled={!mainButtonCurrentIsActive}
onClick={handleMainButtonClick}
>
{mainButtonCurrentText}
{mainButton?.isProgressVisible && <Spinner color="white" />}
</Button>
</>
)}
</Modal>
);
};
export default memo(withGlobal<OwnProps>(
(global, { webApp }): StateProps => {
const { bot } = webApp || {};
const isInstalled = Boolean(bot && global.attachMenu.bots[bot.id]);
const chat = selectCurrentChat(global);
const theme = selectTheme(global);
return {
isInstalled,
chat,
theme,
};
},
)(WebAppModal));