Mini Apps: Support fullscreen (#5230)
This commit is contained in:
parent
9dca147164
commit
a992820d8a
@ -194,6 +194,7 @@ export async function requestWebView({
|
|||||||
theme,
|
theme,
|
||||||
sendAs,
|
sendAs,
|
||||||
isFromBotMenu,
|
isFromBotMenu,
|
||||||
|
isFullscreen,
|
||||||
}: {
|
}: {
|
||||||
isSilent?: boolean;
|
isSilent?: boolean;
|
||||||
peer: ApiPeer;
|
peer: ApiPeer;
|
||||||
@ -204,6 +205,7 @@ export async function requestWebView({
|
|||||||
theme?: ApiThemeParameters;
|
theme?: ApiThemeParameters;
|
||||||
sendAs?: ApiPeer;
|
sendAs?: ApiPeer;
|
||||||
isFromBotMenu?: boolean;
|
isFromBotMenu?: boolean;
|
||||||
|
isFullscreen?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const result = await invokeRequest(new GramJs.messages.RequestWebView({
|
const result = await invokeRequest(new GramJs.messages.RequestWebView({
|
||||||
silent: isSilent || undefined,
|
silent: isSilent || undefined,
|
||||||
@ -215,6 +217,7 @@ export async function requestWebView({
|
|||||||
fromBotMenu: isFromBotMenu || undefined,
|
fromBotMenu: isFromBotMenu || undefined,
|
||||||
platform: WEB_APP_PLATFORM,
|
platform: WEB_APP_PLATFORM,
|
||||||
replyTo: replyInfo && buildInputReplyTo(replyInfo),
|
replyTo: replyInfo && buildInputReplyTo(replyInfo),
|
||||||
|
fullscreen: isFullscreen ? true : undefined,
|
||||||
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
...(sendAs && { sendAs: buildInputPeer(sendAs.id, sendAs.accessHash) }),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@ -222,6 +225,7 @@ export async function requestWebView({
|
|||||||
return {
|
return {
|
||||||
url: result.url,
|
url: result.url,
|
||||||
queryId: result.queryId?.toString(),
|
queryId: result.queryId?.toString(),
|
||||||
|
isFullScreen: Boolean(result.fullscreen),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -232,17 +236,20 @@ export async function requestMainWebView({
|
|||||||
peer,
|
peer,
|
||||||
bot,
|
bot,
|
||||||
startParam,
|
startParam,
|
||||||
|
mode,
|
||||||
theme,
|
theme,
|
||||||
}: {
|
}: {
|
||||||
peer: ApiPeer;
|
peer: ApiPeer;
|
||||||
bot: ApiUser;
|
bot: ApiUser;
|
||||||
startParam?: string;
|
startParam?: string;
|
||||||
|
mode?: string;
|
||||||
theme?: ApiThemeParameters;
|
theme?: ApiThemeParameters;
|
||||||
}) {
|
}) {
|
||||||
const result = await invokeRequest(new GramJs.messages.RequestMainWebView({
|
const result = await invokeRequest(new GramJs.messages.RequestMainWebView({
|
||||||
peer: buildInputPeer(peer.id, peer.accessHash),
|
peer: buildInputPeer(peer.id, peer.accessHash),
|
||||||
bot: buildInputPeer(bot.id, bot.accessHash),
|
bot: buildInputPeer(bot.id, bot.accessHash),
|
||||||
startParam,
|
startParam,
|
||||||
|
fullscreen: mode === 'fullscreen' || undefined,
|
||||||
themeParams: theme ? buildInputThemeParams(theme) : undefined,
|
themeParams: theme ? buildInputThemeParams(theme) : undefined,
|
||||||
platform: WEB_APP_PLATFORM,
|
platform: WEB_APP_PLATFORM,
|
||||||
}));
|
}));
|
||||||
@ -254,6 +261,7 @@ export async function requestMainWebView({
|
|||||||
return {
|
return {
|
||||||
url: result.url,
|
url: result.url,
|
||||||
queryId: result.queryId?.toString(),
|
queryId: result.queryId?.toString(),
|
||||||
|
isFullscreen: Boolean(result.fullscreen),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -310,12 +318,14 @@ export async function requestAppWebView({
|
|||||||
peer,
|
peer,
|
||||||
app,
|
app,
|
||||||
startParam,
|
startParam,
|
||||||
|
mode,
|
||||||
theme,
|
theme,
|
||||||
isWriteAllowed,
|
isWriteAllowed,
|
||||||
}: {
|
}: {
|
||||||
peer: ApiPeer;
|
peer: ApiPeer;
|
||||||
app: ApiBotApp;
|
app: ApiBotApp;
|
||||||
startParam?: string;
|
startParam?: string;
|
||||||
|
mode?: string;
|
||||||
theme?: ApiThemeParameters;
|
theme?: ApiThemeParameters;
|
||||||
isWriteAllowed?: boolean;
|
isWriteAllowed?: boolean;
|
||||||
}) {
|
}) {
|
||||||
@ -326,9 +336,10 @@ export async function requestAppWebView({
|
|||||||
themeParams: theme ? buildInputThemeParams(theme) : undefined,
|
themeParams: theme ? buildInputThemeParams(theme) : undefined,
|
||||||
platform: WEB_APP_PLATFORM,
|
platform: WEB_APP_PLATFORM,
|
||||||
writeAllowed: isWriteAllowed || undefined,
|
writeAllowed: isWriteAllowed || undefined,
|
||||||
|
fullscreen: mode === 'fullscreen' || undefined,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
return result?.url;
|
return { url: result?.url, isFullscreen: Boolean(result?.fullscreen) };
|
||||||
}
|
}
|
||||||
|
|
||||||
export function prolongWebView({
|
export function prolongWebView({
|
||||||
|
|||||||
@ -61,7 +61,7 @@ const MinimizedWebAppModal = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleExpandClick = useLastCallback(() => {
|
const handleExpandClick = useLastCallback(() => {
|
||||||
changeWebAppModalState();
|
changeWebAppModalState({ state: 'maximized' });
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!isMinimizedState) return undefined;
|
if (!isMinimizedState) return undefined;
|
||||||
|
|||||||
@ -58,12 +58,21 @@
|
|||||||
.multi-tab {
|
.multi-tab {
|
||||||
:global {
|
:global {
|
||||||
.modal-dialog {
|
.modal-dialog {
|
||||||
height: min(42.5rem, 85vh);
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
max-height: min(42.5rem, 85vh);
|
||||||
|
|
||||||
max-width: 26.25rem;
|
max-width: 26.25rem;
|
||||||
background-color: var(--color-web-app-browser);
|
background-color: var(--color-web-app-browser);
|
||||||
backdrop-filter: blur(1.5625rem);
|
backdrop-filter: blur(1.5625rem);
|
||||||
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
|
|
||||||
transition: height var(--state-transition), width var(--state-transition), transform 0.2s ease, opacity 0.2s ease;
|
/* stylelint-disable @stylistic/value-list-comma-newline-after */
|
||||||
|
/* stylelint-disable plugin/no-low-performance-animation-properties */
|
||||||
|
transition:
|
||||||
|
max-height var(--state-transition), max-width var(--state-transition),
|
||||||
|
left var(--state-transition), top var(--state-transition),
|
||||||
|
transform 0.2s ease, opacity 0.2s ease;
|
||||||
|
|
||||||
box-shadow: var(--modal-shadow);
|
box-shadow: var(--modal-shadow);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -105,13 +114,26 @@
|
|||||||
:global {
|
:global {
|
||||||
.modal-dialog {
|
.modal-dialog {
|
||||||
cursor: grab !important;
|
cursor: grab !important;
|
||||||
width: 300px;
|
max-width: 300px;
|
||||||
height: 2.5rem;
|
max-height: 2.5rem;
|
||||||
min-width: 0;
|
min-width: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fullScreen {
|
||||||
|
:global {
|
||||||
|
.modal-dialog {
|
||||||
|
max-width: 100%;
|
||||||
|
max-height: 100%;
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
.modal-content {
|
||||||
|
border-radius: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.tabs {
|
.tabs {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@ -20,6 +20,7 @@ import buildClassName from '../../../util/buildClassName';
|
|||||||
import buildStyle from '../../../util/buildStyle';
|
import buildStyle from '../../../util/buildStyle';
|
||||||
import { getColorLuma } from '../../../util/colors';
|
import { getColorLuma } from '../../../util/colors';
|
||||||
import { hexToRgb } from '../../../util/switchTheme';
|
import { hexToRgb } from '../../../util/switchTheme';
|
||||||
|
import windowSize from '../../../util/windowSize';
|
||||||
|
|
||||||
import useInterval from '../../../hooks/schedulers/useInterval';
|
import useInterval from '../../../hooks/schedulers/useInterval';
|
||||||
import useAppLayout from '../../../hooks/useAppLayout';
|
import useAppLayout from '../../../hooks/useAppLayout';
|
||||||
@ -89,12 +90,16 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
|
|||||||
const minimizedStateSize = useMemo(() => {
|
const minimizedStateSize = useMemo(() => {
|
||||||
return { width: 300, height: 40 };
|
return { width: 300, height: 40 };
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const [getFrameSize, setFrameSize] = useSignal(
|
const [getFrameSize, setFrameSize] = useSignal(
|
||||||
{ width: maximizedStateSize.width, height: maximizedStateSize.height - minimizedStateSize.height },
|
{ width: maximizedStateSize.width, height: maximizedStateSize.height - minimizedStateSize.height },
|
||||||
);
|
);
|
||||||
|
|
||||||
function getSize() {
|
function getSize() {
|
||||||
return modal?.modalState === 'maximized' ? maximizedStateSize : minimizedStateSize;
|
if (modal?.modalState === 'fullScreen') return windowSize.get();
|
||||||
|
if (modal?.modalState === 'maximized') return maximizedStateSize;
|
||||||
|
return minimizedStateSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@ -118,6 +123,8 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
|
|||||||
const { isMobile } = useAppLayout();
|
const { isMobile } = useAppLayout();
|
||||||
const isOpen = modal?.isModalOpen || false;
|
const isOpen = modal?.isModalOpen || false;
|
||||||
const isMaximizedState = modal?.modalState === 'maximized';
|
const isMaximizedState = modal?.modalState === 'maximized';
|
||||||
|
const isMinimizedState = modal?.modalState === 'minimized';
|
||||||
|
const isFullScreen = modal?.modalState === 'fullScreen';
|
||||||
|
|
||||||
const supportMultiTabMode = !isMobile;
|
const supportMultiTabMode = !isMobile;
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
@ -151,14 +158,24 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
|
|||||||
const containerElement = ref.current;
|
const containerElement = ref.current;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsDraggingEnabled(Boolean(supportMultiTabMode && headerElement && containerElement));
|
setIsDraggingEnabled(Boolean(supportMultiTabMode && headerElement && containerElement && !isFullScreen));
|
||||||
}, [supportMultiTabMode, headerElement, containerElement]);
|
}, [supportMultiTabMode, headerElement, containerElement, isFullScreen]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
changeWebAppModalState({ state: 'maximized' });
|
||||||
|
}, [supportMultiTabMode]);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isDragging,
|
isDragging,
|
||||||
style: draggableStyle,
|
style: draggableStyle,
|
||||||
size,
|
size,
|
||||||
} = useDraggable(ref, headerRef, isDraggingEnabled, getSize());
|
} = useDraggable(
|
||||||
|
ref,
|
||||||
|
headerRef,
|
||||||
|
isDraggingEnabled,
|
||||||
|
getSize(),
|
||||||
|
isFullScreen,
|
||||||
|
);
|
||||||
|
|
||||||
const currentSize = size || getSize();
|
const currentSize = size || getSize();
|
||||||
|
|
||||||
@ -170,7 +187,16 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
|
|||||||
const height = currentHeight - minimizedStateSize.height;
|
const height = currentHeight - minimizedStateSize.height;
|
||||||
setFrameSize({ width: currentWidth, height });
|
setFrameSize({ width: currentWidth, height });
|
||||||
}
|
}
|
||||||
}, [currentWidth, currentHeight, isMaximizedState, minimizedStateSize, setFrameSize]);
|
if (isFullScreen) {
|
||||||
|
setFrameSize({ width: window.innerWidth, height: window.innerHeight });
|
||||||
|
}
|
||||||
|
}, [currentWidth,
|
||||||
|
currentHeight,
|
||||||
|
isMaximizedState,
|
||||||
|
minimizedStateSize,
|
||||||
|
setFrameSize,
|
||||||
|
isFullScreen,
|
||||||
|
isMinimizedState]);
|
||||||
|
|
||||||
const oldLang = useOldLang();
|
const oldLang = useOldLang();
|
||||||
const lang = useLang();
|
const lang = useLang();
|
||||||
@ -275,7 +301,7 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleCollapseClick = useLastCallback(() => {
|
const handleCollapseClick = useLastCallback(() => {
|
||||||
changeWebAppModalState();
|
changeWebAppModalState({ state: 'minimized' });
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleOpenMoreAppsTabClick = useLastCallback(() => {
|
const handleOpenMoreAppsTabClick = useLastCallback(() => {
|
||||||
@ -497,7 +523,7 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
useHorizontalScroll(containerRef, !isOpen || !isMaximizedState || !(containerRef.current));
|
useHorizontalScroll(containerRef, !isOpen || isMinimizedState || !(containerRef.current));
|
||||||
|
|
||||||
function renderTabs() {
|
function renderTabs() {
|
||||||
return (
|
return (
|
||||||
@ -606,7 +632,8 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
|
|||||||
className={buildClassName(
|
className={buildClassName(
|
||||||
styles.root,
|
styles.root,
|
||||||
supportMultiTabMode && styles.multiTab,
|
supportMultiTabMode && styles.multiTab,
|
||||||
!isMaximizedState && styles.minimized,
|
isMinimizedState && styles.minimized,
|
||||||
|
isFullScreen && styles.fullScreen,
|
||||||
)}
|
)}
|
||||||
dialogStyle={supportMultiTabMode ? draggableStyle : undefined}
|
dialogStyle={supportMultiTabMode ? draggableStyle : undefined}
|
||||||
isOpen={isOpen}
|
isOpen={isOpen}
|
||||||
@ -617,6 +644,7 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
|
|||||||
noBackdrop
|
noBackdrop
|
||||||
noBackdropClose
|
noBackdropClose
|
||||||
>
|
>
|
||||||
|
{isFullScreen && renderMoreMenu()}
|
||||||
{openedWebApps && sessionKeys?.map((key) => (
|
{openedWebApps && sessionKeys?.map((key) => (
|
||||||
<WebAppModalTabContent
|
<WebAppModalTabContent
|
||||||
key={key}
|
key={key}
|
||||||
@ -625,7 +653,7 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
|
|||||||
registerReloadFrameCallback={registerReloadFrameCallback}
|
registerReloadFrameCallback={registerReloadFrameCallback}
|
||||||
webApp={openedWebApps[key]}
|
webApp={openedWebApps[key]}
|
||||||
isDragging={isDragging}
|
isDragging={isDragging}
|
||||||
frameSize={supportMultiTabMode ? getFrameSize() : undefined}
|
onContextMenuButtonClick={handleContextMenu}
|
||||||
isMultiTabSupported={supportMultiTabMode}
|
isMultiTabSupported={supportMultiTabMode}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@ -1,9 +1,12 @@
|
|||||||
|
@use "../../../styles/mixins";
|
||||||
|
|
||||||
.root {
|
.root {
|
||||||
height: 100%;
|
height: 100%;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.multi-tab {
|
.multi-tab {
|
||||||
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@ -157,3 +160,124 @@
|
|||||||
padding-left: 2rem;
|
padding-left: 2rem;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.closeIcon {
|
||||||
|
--color-header-text: white;
|
||||||
|
position: relative;
|
||||||
|
transform: rotate(-45deg);
|
||||||
|
|
||||||
|
&,
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
width: 0.875rem;
|
||||||
|
height: 0.125rem;
|
||||||
|
border-radius: 0.125rem;
|
||||||
|
background-color: var(--color-header-text);
|
||||||
|
transition: var(--slide-transition) transform, var(--color-transition) background-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before,
|
||||||
|
&::after {
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
content: "";
|
||||||
|
}
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.state-back {
|
||||||
|
transform: rotate(180deg);
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
transform: rotate(45deg) scaleX(0.75) translate(0, -0.3125rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
transform: rotate(-45deg) scaleX(0.75) translate(0, 0.3125rem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.backIconContainer {
|
||||||
|
padding-right: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.moreIcon {
|
||||||
|
transform: rotate(90deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerPanel {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
z-index: 100;
|
||||||
|
padding: 1rem;
|
||||||
|
padding-inline-end: calc(1rem + var(--scrollbar-width));
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
|
|
||||||
|
.icon {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerSplitButton {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.headerButton {
|
||||||
|
height: 1.75rem;
|
||||||
|
width: fit-content;
|
||||||
|
font-size: 0.875rem;
|
||||||
|
font-weight: 500;
|
||||||
|
position: relative;
|
||||||
|
cursor: var(--custom-cursor, pointer);
|
||||||
|
flex-shrink: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
outline: none !important;
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
color: white;
|
||||||
|
border-radius: 1rem;
|
||||||
|
background-color: rgba(0, 0, 0, 0.2);
|
||||||
|
backdrop-filter: blur(25px);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
padding: 0.25rem;
|
||||||
|
padding-inline: 0.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.left {
|
||||||
|
border-bottom-right-radius: 0;
|
||||||
|
border-top-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right {
|
||||||
|
border-bottom-left-radius: 0;
|
||||||
|
border-top-left-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttonCaptionContainer {
|
||||||
|
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
|
||||||
|
transition: width 0.25s ease-in-out;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.backButtonCaption {
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 20rem;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import React, {
|
|||||||
import { getActions, withGlobal } from '../../../global';
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
import type { ApiAttachBot, ApiChat, ApiUser } from '../../../api/types';
|
import type { ApiAttachBot, ApiChat, ApiUser } from '../../../api/types';
|
||||||
import type { TabState, WebApp } from '../../../global/types';
|
import type { TabState, WebApp, WebAppModalStateType } from '../../../global/types';
|
||||||
import type { ThemeKey } from '../../../types';
|
import type { ThemeKey } from '../../../types';
|
||||||
import type { PopupOptions, WebAppInboundEvent, WebAppOutboundEvent } from '../../../types/webapp';
|
import type { PopupOptions, WebAppInboundEvent, WebAppOutboundEvent } from '../../../types/webapp';
|
||||||
|
|
||||||
@ -23,18 +23,22 @@ import { callApi } from '../../../api/gramjs';
|
|||||||
import { REM } from '../../common/helpers/mediaDimensions';
|
import { REM } from '../../common/helpers/mediaDimensions';
|
||||||
import renderText from '../../common/helpers/renderText';
|
import renderText from '../../common/helpers/renderText';
|
||||||
|
|
||||||
|
import { getIsWebAppsFullscreenSupported } from '../../../hooks/useAppLayout';
|
||||||
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
|
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
|
||||||
import useFlag from '../../../hooks/useFlag';
|
import useFlag from '../../../hooks/useFlag';
|
||||||
import useLastCallback from '../../../hooks/useLastCallback';
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
import useOldLang from '../../../hooks/useOldLang';
|
import useOldLang from '../../../hooks/useOldLang';
|
||||||
import useSyncEffect from '../../../hooks/useSyncEffect';
|
import useSyncEffect from '../../../hooks/useSyncEffect';
|
||||||
|
import useFullscreen, { checkIfFullscreen } from '../../../hooks/window/useFullscreen';
|
||||||
import usePopupLimit from './hooks/usePopupLimit';
|
import usePopupLimit from './hooks/usePopupLimit';
|
||||||
import useWebAppFrame from './hooks/useWebAppFrame';
|
import useWebAppFrame from './hooks/useWebAppFrame';
|
||||||
|
|
||||||
|
import Icon from '../../common/icons/Icon';
|
||||||
import Button from '../../ui/Button';
|
import Button from '../../ui/Button';
|
||||||
import ConfirmDialog from '../../ui/ConfirmDialog';
|
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||||
import Modal from '../../ui/Modal';
|
import Modal from '../../ui/Modal';
|
||||||
import Spinner from '../../ui/Spinner';
|
import Spinner from '../../ui/Spinner';
|
||||||
|
import Transition from '../../ui/Transition';
|
||||||
|
|
||||||
import styles from './WebAppModalTabContent.module.scss';
|
import styles from './WebAppModalTabContent.module.scss';
|
||||||
|
|
||||||
@ -53,6 +57,7 @@ export type OwnProps = {
|
|||||||
webApp?: WebApp;
|
webApp?: WebApp;
|
||||||
registerSendEventCallback: (callback: (event: WebAppOutboundEvent) => void) => void;
|
registerSendEventCallback: (callback: (event: WebAppOutboundEvent) => void) => void;
|
||||||
registerReloadFrameCallback: (callback: (url: string) => void) => void;
|
registerReloadFrameCallback: (callback: (url: string) => void) => void;
|
||||||
|
onContextMenuButtonClick: (e: React.MouseEvent) => void;
|
||||||
isDragging?: boolean;
|
isDragging?: boolean;
|
||||||
frameSize?: { width: number; height: number };
|
frameSize?: { width: number; height: number };
|
||||||
isMultiTabSupported? : boolean;
|
isMultiTabSupported? : boolean;
|
||||||
@ -65,15 +70,17 @@ type StateProps = {
|
|||||||
theme?: ThemeKey;
|
theme?: ThemeKey;
|
||||||
isPaymentModalOpen?: boolean;
|
isPaymentModalOpen?: boolean;
|
||||||
paymentStatus?: TabState['payment']['status'];
|
paymentStatus?: TabState['payment']['status'];
|
||||||
isMaximizedState: boolean;
|
modalState?: WebAppModalStateType;
|
||||||
};
|
};
|
||||||
|
|
||||||
const NBSP = '\u00A0';
|
const NBSP = '\u00A0';
|
||||||
|
|
||||||
const MAIN_BUTTON_ANIMATION_TIME = 250;
|
const MAIN_BUTTON_ANIMATION_TIME = 250;
|
||||||
const ANIMATION_WAIT = 400;
|
const ANIMATION_WAIT = 400;
|
||||||
|
const COLLAPSING_WAIT = 350;
|
||||||
const POPUP_SEQUENTIAL_LIMIT = 3;
|
const POPUP_SEQUENTIAL_LIMIT = 3;
|
||||||
const POPUP_RESET_DELAY = 2000; // 2s
|
const POPUP_RESET_DELAY = 2000; // 2s
|
||||||
|
const APP_NAME_DISPLAY_DURATION = 3800;
|
||||||
const SANDBOX_ATTRIBUTES = [
|
const SANDBOX_ATTRIBUTES = [
|
||||||
'allow-scripts',
|
'allow-scripts',
|
||||||
'allow-same-origin',
|
'allow-same-origin',
|
||||||
@ -99,9 +106,10 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
|||||||
registerSendEventCallback,
|
registerSendEventCallback,
|
||||||
registerReloadFrameCallback,
|
registerReloadFrameCallback,
|
||||||
isDragging,
|
isDragging,
|
||||||
isMaximizedState,
|
modalState,
|
||||||
frameSize,
|
frameSize,
|
||||||
isMultiTabSupported,
|
isMultiTabSupported,
|
||||||
|
onContextMenuButtonClick,
|
||||||
}) => {
|
}) => {
|
||||||
const {
|
const {
|
||||||
closeActiveWebApp,
|
closeActiveWebApp,
|
||||||
@ -113,6 +121,8 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
|||||||
sharePhoneWithBot,
|
sharePhoneWithBot,
|
||||||
updateWebApp,
|
updateWebApp,
|
||||||
resetPaymentStatus,
|
resetPaymentStatus,
|
||||||
|
changeWebAppModalState,
|
||||||
|
closeWebAppModal,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
const [mainButton, setMainButton] = useState<WebAppButton | undefined>();
|
const [mainButton, setMainButton] = useState<WebAppButton | undefined>();
|
||||||
const [secondaryButton, setSecondaryButton] = useState<WebAppButton | undefined>();
|
const [secondaryButton, setSecondaryButton] = useState<WebAppButton | undefined>();
|
||||||
@ -127,9 +137,35 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
|||||||
unlockPopupsAt, handlePopupOpened, handlePopupClosed,
|
unlockPopupsAt, handlePopupOpened, handlePopupClosed,
|
||||||
} = usePopupLimit(POPUP_SEQUENTIAL_LIMIT, POPUP_RESET_DELAY);
|
} = usePopupLimit(POPUP_SEQUENTIAL_LIMIT, POPUP_RESET_DELAY);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const headerButtonRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const headerButtonCaptionRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
const isFullscreen = modalState === 'fullScreen';
|
||||||
|
const isMinimizedState = modalState === 'minimized';
|
||||||
|
|
||||||
|
const exitFullScreenCallback = useLastCallback(() => {
|
||||||
|
setTimeout(() => { changeWebAppModalState({ state: 'maximized' }); }, COLLAPSING_WAIT);
|
||||||
|
});
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
const fullscreenElementRef = useRef<HTMLElement | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
fullscreenElementRef.current = document.querySelector('#portals') as HTMLElement;
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const [, setFullscreen, exitFullscreen] = useFullscreen(fullscreenElementRef, exitFullScreenCallback);
|
||||||
|
|
||||||
const activeWebApp = modal?.activeWebApp;
|
const activeWebApp = modal?.activeWebApp;
|
||||||
|
const activeWebAppName = activeWebApp?.appName;
|
||||||
const {
|
const {
|
||||||
url, buttonText, headerColor, serverHeaderColorKey, serverHeaderColor,
|
url, buttonText, headerColor, serverHeaderColorKey, serverHeaderColor, isBackButtonVisible,
|
||||||
} = webApp || {};
|
} = webApp || {};
|
||||||
const isCloseModalOpen = Boolean(webApp?.isCloseModalOpen);
|
const isCloseModalOpen = Boolean(webApp?.isCloseModalOpen);
|
||||||
const isRemoveModalOpen = Boolean(webApp?.isRemoveModalOpen);
|
const isRemoveModalOpen = Boolean(webApp?.isRemoveModalOpen);
|
||||||
@ -158,8 +194,8 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
|||||||
const isSimple = Boolean(buttonText);
|
const isSimple = Boolean(buttonText);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
reloadFrame, sendEvent, sendViewport, sendTheme,
|
reloadFrame, sendEvent, sendFullScreenChanged, sendViewport, sendSafeArea, sendTheme,
|
||||||
} = useWebAppFrame(frameRef, isOpen, isSimple, handleEvent, webApp, markLoaded);
|
} = useWebAppFrame(frameRef, isOpen, isFullscreen, isSimple, handleEvent, webApp, markLoaded);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isActive) registerSendEventCallback(sendEvent);
|
if (isActive) registerSendEventCallback(sendEvent);
|
||||||
@ -248,6 +284,32 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
|||||||
}, ANIMATION_WAIT);
|
}, ANIMATION_WAIT);
|
||||||
}, [theme]);
|
}, [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]);
|
||||||
|
|
||||||
useSyncEffect(([prevIsPaymentModalOpen]) => {
|
useSyncEffect(([prevIsPaymentModalOpen]) => {
|
||||||
if (isPaymentModalOpen === prevIsPaymentModalOpen) return;
|
if (isPaymentModalOpen === prevIsPaymentModalOpen) return;
|
||||||
if (webApp?.slug && !isPaymentModalOpen && paymentStatus) {
|
if (webApp?.slug && !isPaymentModalOpen && paymentStatus) {
|
||||||
@ -377,6 +439,24 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
function handleEvent(event: WebAppInboundEvent) {
|
function handleEvent(event: WebAppInboundEvent) {
|
||||||
const { eventType, eventData } = event;
|
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') {
|
if (eventType === 'web_app_open_tg_link') {
|
||||||
const linkUrl = TME_LINK_PREFIX + eventData.path_full;
|
const linkUrl = TME_LINK_PREFIX + eventData.path_full;
|
||||||
openTelegramLink({ url: linkUrl, shouldIgnoreCache: eventData.force_request });
|
openTelegramLink({ url: linkUrl, shouldIgnoreCache: eventData.force_request });
|
||||||
@ -514,14 +594,19 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
|||||||
const [shouldShowMainButton, setShouldShowMainButton] = useState(false);
|
const [shouldShowMainButton, setShouldShowMainButton] = useState(false);
|
||||||
const [shouldShowSecondaryButton, setShouldShowSecondaryButton] = useState(false);
|
const [shouldShowSecondaryButton, setShouldShowSecondaryButton] = useState(false);
|
||||||
|
|
||||||
|
const [shouldShowAppNameInFullscreen, setShouldShowAppNameInFullscreen] = useState(false);
|
||||||
|
|
||||||
|
const [backButtonTextWidth, setBackButtonTextWidth] = useState(0);
|
||||||
|
|
||||||
// Notify view that height changed
|
// Notify view that height changed
|
||||||
useSyncEffect(() => {
|
useSyncEffect(() => {
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
sendViewport();
|
sendViewport();
|
||||||
|
sendSafeArea();
|
||||||
}, ANIMATION_WAIT);
|
}, ANIMATION_WAIT);
|
||||||
}, [shouldShowSecondaryButton, shouldHideSecondaryButton,
|
}, [shouldShowSecondaryButton, shouldHideSecondaryButton,
|
||||||
shouldShowMainButton, shouldShowMainButton,
|
shouldShowMainButton, shouldShowMainButton,
|
||||||
secondaryButton?.position, sendViewport]);
|
secondaryButton?.position, sendViewport, sendSafeArea]);
|
||||||
|
|
||||||
const isVerticalLayout = secondaryButtonCurrentPosition === 'top' || secondaryButtonCurrentPosition === 'bottom';
|
const isVerticalLayout = secondaryButtonCurrentPosition === 'top' || secondaryButtonCurrentPosition === 'bottom';
|
||||||
const isHorizontalLayout = !isVerticalLayout;
|
const isHorizontalLayout = !isVerticalLayout;
|
||||||
@ -536,6 +621,35 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
|||||||
const mainButtonFastTimeout = useRef<ReturnType<typeof setTimeout>>();
|
const mainButtonFastTimeout = useRef<ReturnType<typeof setTimeout>>();
|
||||||
const secondaryButtonChangeTimeout = useRef<ReturnType<typeof setTimeout>>();
|
const secondaryButtonChangeTimeout = useRef<ReturnType<typeof setTimeout>>();
|
||||||
const secondaryButtonFastTimeout = useRef<ReturnType<typeof setTimeout>>();
|
const secondaryButtonFastTimeout = useRef<ReturnType<typeof setTimeout>>();
|
||||||
|
const appNameDisplayTimeout = useRef<ReturnType<typeof setTimeout>>();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (isFullscreen && isOpen && Boolean(activeWebAppName)) {
|
||||||
|
setShouldShowAppNameInFullscreen(true);
|
||||||
|
|
||||||
|
if (appNameDisplayTimeout.current) {
|
||||||
|
clearTimeout(appNameDisplayTimeout.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
appNameDisplayTimeout.current = setTimeout(() => {
|
||||||
|
setShouldShowAppNameInFullscreen(false);
|
||||||
|
appNameDisplayTimeout.current = undefined;
|
||||||
|
}, APP_NAME_DISPLAY_DURATION);
|
||||||
|
} else {
|
||||||
|
setShouldShowAppNameInFullscreen(false);
|
||||||
|
|
||||||
|
if (appNameDisplayTimeout.current) {
|
||||||
|
clearTimeout(appNameDisplayTimeout.current);
|
||||||
|
appNameDisplayTimeout.current = undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (appNameDisplayTimeout.current) {
|
||||||
|
clearTimeout(appNameDisplayTimeout.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [isFullscreen, isOpen, activeWebAppName]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mainButtonChangeTimeout.current) clearTimeout(mainButtonChangeTimeout.current);
|
if (mainButtonChangeTimeout.current) clearTimeout(mainButtonChangeTimeout.current);
|
||||||
@ -590,30 +704,140 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
|||||||
const frameWidth = frameSize?.width || 0;
|
const frameWidth = frameSize?.width || 0;
|
||||||
let frameHeight = frameSize?.height || 0;
|
let frameHeight = frameSize?.height || 0;
|
||||||
if (shouldDecreaseWebFrameSize) { frameHeight -= 4 * REM; }
|
if (shouldDecreaseWebFrameSize) { frameHeight -= 4 * REM; }
|
||||||
const frameStyle = buildStyle(
|
const frameStyle = frameSize ? buildStyle(
|
||||||
`left: ${0}px;`,
|
`left: ${0}px;`,
|
||||||
`top: ${0}px;`,
|
`top: ${0}px;`,
|
||||||
`width: ${frameWidth}px;`,
|
`width: ${frameWidth}px;`,
|
||||||
`height: ${frameHeight}px;`,
|
`height: ${frameHeight}px;`,
|
||||||
isDragging ? 'pointer-events: none;' : '',
|
isDragging ? 'pointer-events: none;' : '',
|
||||||
|
) : isDragging ? '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
|
||||||
|
: lang(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,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="down"
|
||||||
|
className={buildClassName(
|
||||||
|
styles.icon,
|
||||||
|
styles.collapseIcon,
|
||||||
|
)}
|
||||||
|
onClick={handleCollapseClick}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={buildClassName(
|
||||||
|
styles.headerButton,
|
||||||
|
styles.right,
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<Icon
|
||||||
|
name="more"
|
||||||
|
className={buildClassName(
|
||||||
|
styles.icon,
|
||||||
|
styles.moreIcon,
|
||||||
|
)}
|
||||||
|
onClick={handleShowContextMenu}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
ref={containerRef}
|
||||||
className={buildClassName(
|
className={buildClassName(
|
||||||
styles.root,
|
styles.root,
|
||||||
!isActive && styles.hidden,
|
!isActive && styles.hidden,
|
||||||
isMultiTabSupported && styles.multiTab,
|
isMultiTabSupported && styles.multiTab,
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{isMaximizedState && <Spinner className={buildClassName(styles.loadingSpinner, isLoaded && styles.hide)} />}
|
{isFullscreen && getIsWebAppsFullscreenSupported() && renderFullscreenHeaderPanel()}
|
||||||
|
{!isMinimizedState && <Spinner className={buildClassName(styles.loadingSpinner, isLoaded && styles.hide)} />}
|
||||||
<iframe
|
<iframe
|
||||||
className={buildClassName(
|
className={buildClassName(
|
||||||
styles.frame,
|
styles.frame,
|
||||||
shouldDecreaseWebFrameSize && styles.withButton,
|
shouldDecreaseWebFrameSize && styles.withButton,
|
||||||
!isLoaded && styles.hide,
|
!isLoaded && styles.hide,
|
||||||
)}
|
)}
|
||||||
style={frameSize ? frameStyle : undefined}
|
style={frameStyle}
|
||||||
src={url}
|
src={url}
|
||||||
title={`${bot?.firstName} Web App`}
|
title={`${bot?.firstName} Web App`}
|
||||||
sandbox={SANDBOX_ATTRIBUTES}
|
sandbox={SANDBOX_ATTRIBUTES}
|
||||||
@ -621,7 +845,7 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
|||||||
allowFullScreen
|
allowFullScreen
|
||||||
ref={frameRef}
|
ref={frameRef}
|
||||||
/>
|
/>
|
||||||
{isMaximizedState && (
|
{!isMinimizedState && (
|
||||||
<div
|
<div
|
||||||
style={`background-color: ${bottomBarColor};`}
|
style={`background-color: ${bottomBarColor};`}
|
||||||
className={buildClassName(
|
className={buildClassName(
|
||||||
@ -739,7 +963,7 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
|||||||
export default memo(withGlobal<OwnProps>(
|
export default memo(withGlobal<OwnProps>(
|
||||||
(global, { modal }): StateProps => {
|
(global, { modal }): StateProps => {
|
||||||
const { botId: activeBotId } = modal?.activeWebApp || {};
|
const { botId: activeBotId } = modal?.activeWebApp || {};
|
||||||
const isMaximizedState = modal?.modalState === 'maximized';
|
const modalState = modal?.modalState;
|
||||||
|
|
||||||
const attachBot = activeBotId ? global.attachMenu.bots[activeBotId] : undefined;
|
const attachBot = activeBotId ? global.attachMenu.bots[activeBotId] : undefined;
|
||||||
const bot = activeBotId ? selectUser(global, activeBotId) : undefined;
|
const bot = activeBotId ? selectUser(global, activeBotId) : undefined;
|
||||||
@ -757,7 +981,7 @@ export default memo(withGlobal<OwnProps>(
|
|||||||
theme,
|
theme,
|
||||||
isPaymentModalOpen: isPaymentModalOpen || Boolean(starsInputInvoice),
|
isPaymentModalOpen: isPaymentModalOpen || Boolean(starsInputInvoice),
|
||||||
paymentStatus,
|
paymentStatus,
|
||||||
isMaximizedState,
|
modalState,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
)(WebAppModalTabContent));
|
)(WebAppModalTabContent));
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import type { WebAppInboundEvent, WebAppOutboundEvent } from '../../../../types/
|
|||||||
|
|
||||||
import { getWebAppKey } from '../../../../global/helpers';
|
import { getWebAppKey } from '../../../../global/helpers';
|
||||||
import { extractCurrentThemeParams } from '../../../../util/themeStyle';
|
import { extractCurrentThemeParams } from '../../../../util/themeStyle';
|
||||||
|
import { REM } from '../../../common/helpers/mediaDimensions';
|
||||||
|
|
||||||
import useLastCallback from '../../../../hooks/useLastCallback';
|
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||||
import useWindowSize from '../../../../hooks/window/useWindowSize';
|
import useWindowSize from '../../../../hooks/window/useWindowSize';
|
||||||
@ -32,10 +33,12 @@ const SCROLLBAR_STYLE = `* {
|
|||||||
}`;
|
}`;
|
||||||
|
|
||||||
const RELOAD_TIMEOUT = 500;
|
const RELOAD_TIMEOUT = 500;
|
||||||
|
const SAFE_AREA_HEIGHT = 3.675 * REM;
|
||||||
|
|
||||||
const useWebAppFrame = (
|
const useWebAppFrame = (
|
||||||
ref: React.RefObject<HTMLIFrameElement>,
|
ref: React.RefObject<HTMLIFrameElement>,
|
||||||
isOpen: boolean,
|
isOpen: boolean,
|
||||||
|
isFullscreen: boolean,
|
||||||
isSimpleView: boolean,
|
isSimpleView: boolean,
|
||||||
onEvent: (event: WebAppInboundEvent) => void,
|
onEvent: (event: WebAppInboundEvent) => void,
|
||||||
webApp?: WebApp,
|
webApp?: WebApp,
|
||||||
@ -75,6 +78,15 @@ const useWebAppFrame = (
|
|||||||
ref.current.contentWindow.postMessage(JSON.stringify(event), '*');
|
ref.current.contentWindow.postMessage(JSON.stringify(event), '*');
|
||||||
}, [ref]);
|
}, [ref]);
|
||||||
|
|
||||||
|
const sendFullScreenChanged = useCallback((value: boolean) => {
|
||||||
|
sendEvent({
|
||||||
|
eventType: 'fullscreen_changed',
|
||||||
|
eventData: {
|
||||||
|
is_fullscreen: value,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [sendEvent]);
|
||||||
|
|
||||||
const forceReloadFrame = useLastCallback((url: string) => {
|
const forceReloadFrame = useLastCallback((url: string) => {
|
||||||
if (!ref.current) return;
|
if (!ref.current) return;
|
||||||
const frame = ref.current;
|
const frame = ref.current;
|
||||||
@ -114,6 +126,33 @@ const useWebAppFrame = (
|
|||||||
});
|
});
|
||||||
}, [sendEvent, ref]);
|
}, [sendEvent, ref]);
|
||||||
|
|
||||||
|
const sendSafeArea = useCallback(() => {
|
||||||
|
if (!ref.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { height } = ref.current.getBoundingClientRect();
|
||||||
|
const safeAreaHeight = isFullscreen ? SAFE_AREA_HEIGHT : 0;
|
||||||
|
sendEvent({
|
||||||
|
eventType: 'safe_area_changed',
|
||||||
|
eventData: {
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: 0,
|
||||||
|
bottom: height - safeAreaHeight,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
sendEvent({
|
||||||
|
eventType: 'content_safe_area_changed',
|
||||||
|
eventData: {
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
top: safeAreaHeight,
|
||||||
|
bottom: 0,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [sendEvent, isFullscreen, ref]);
|
||||||
|
|
||||||
const sendTheme = useCallback(() => {
|
const sendTheme = useCallback(() => {
|
||||||
sendEvent({
|
sendEvent({
|
||||||
eventType: 'theme_changed',
|
eventType: 'theme_changed',
|
||||||
@ -160,6 +199,14 @@ const useWebAppFrame = (
|
|||||||
sendViewport(windowSize.isResizing);
|
sendViewport(windowSize.isResizing);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (eventType === 'web_app_request_safe_area') {
|
||||||
|
sendSafeArea();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (eventType === 'web_app_request_content_safe_area') {
|
||||||
|
sendSafeArea();
|
||||||
|
}
|
||||||
|
|
||||||
if (eventType === 'web_app_request_theme') {
|
if (eventType === 'web_app_request_theme') {
|
||||||
sendTheme();
|
sendTheme();
|
||||||
}
|
}
|
||||||
@ -272,7 +319,8 @@ const useWebAppFrame = (
|
|||||||
}
|
}
|
||||||
}, [
|
}, [
|
||||||
isSimpleView, sendEvent, onEvent, sendCustomStyle, webApp,
|
isSimpleView, sendEvent, onEvent, sendCustomStyle, webApp,
|
||||||
sendTheme, sendViewport, onLoad, windowSize.isResizing, ref,
|
sendTheme, sendViewport, sendSafeArea, onLoad, windowSize.isResizing,
|
||||||
|
ref,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -281,7 +329,8 @@ const useWebAppFrame = (
|
|||||||
&& lastFrameSizeRef.current.height === height && !lastFrameSizeRef.current.isResizing) return;
|
&& lastFrameSizeRef.current.height === height && !lastFrameSizeRef.current.isResizing) return;
|
||||||
lastFrameSizeRef.current = { width, height, isResizing };
|
lastFrameSizeRef.current = { width, height, isResizing };
|
||||||
sendViewport(isResizing);
|
sendViewport(isResizing);
|
||||||
}, [sendViewport, windowSize]);
|
sendSafeArea();
|
||||||
|
}, [sendViewport, sendSafeArea, windowSize]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!webApp?.plannedEvents?.length) return;
|
if (!webApp?.plannedEvents?.length) return;
|
||||||
@ -306,14 +355,15 @@ const useWebAppFrame = (
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen && ref.current?.contentWindow) {
|
if (isOpen && ref.current?.contentWindow) {
|
||||||
sendViewport();
|
sendViewport();
|
||||||
|
sendSafeArea();
|
||||||
ignoreEventsRef.current = false;
|
ignoreEventsRef.current = false;
|
||||||
} else {
|
} else {
|
||||||
lastFrameSizeRef.current = undefined;
|
lastFrameSizeRef.current = undefined;
|
||||||
}
|
}
|
||||||
}, [isOpen, sendViewport, ref]);
|
}, [isOpen, isFullscreen, sendViewport, sendSafeArea, ref]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
sendEvent, reloadFrame, sendViewport, sendTheme,
|
sendEvent, sendFullScreenChanged, reloadFrame, sendViewport, sendSafeArea, sendTheme,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@ -54,6 +54,8 @@ import {
|
|||||||
} from '../../selectors';
|
} from '../../selectors';
|
||||||
import { fetchChatByUsername } from './chats';
|
import { fetchChatByUsername } from './chats';
|
||||||
|
|
||||||
|
import { getIsWebAppsFullscreenSupported } from '../../../hooks/useAppLayout';
|
||||||
|
|
||||||
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
|
||||||
const runDebouncedForSearch = debounce((cb) => cb(), 500, false);
|
const runDebouncedForSearch = debounce((cb) => cb(), 500, false);
|
||||||
@ -533,6 +535,7 @@ addActionHandler('requestSimpleWebView', async (global, actions, payload): Promi
|
|||||||
global = getGlobal();
|
global = getGlobal();
|
||||||
const newActiveApp: WebApp = {
|
const newActiveApp: WebApp = {
|
||||||
requestUrl: url,
|
requestUrl: url,
|
||||||
|
appName: bot.firstName,
|
||||||
url: webViewUrl,
|
url: webViewUrl,
|
||||||
botId,
|
botId,
|
||||||
buttonText,
|
buttonText,
|
||||||
@ -543,7 +546,7 @@ addActionHandler('requestSimpleWebView', async (global, actions, payload): Promi
|
|||||||
|
|
||||||
addActionHandler('requestWebView', async (global, actions, payload): Promise<void> => {
|
addActionHandler('requestWebView', async (global, actions, payload): Promise<void> => {
|
||||||
const {
|
const {
|
||||||
url, botId, peerId, theme, isSilent, buttonText, isFromBotMenu, startParam,
|
url, botId, peerId, theme, isSilent, buttonText, isFromBotMenu, startParam, isFullscreen,
|
||||||
tabId = getCurrentTabId(),
|
tabId = getCurrentTabId(),
|
||||||
} = payload;
|
} = payload;
|
||||||
|
|
||||||
@ -586,17 +589,19 @@ addActionHandler('requestWebView', async (global, actions, payload): Promise<voi
|
|||||||
isFromBotMenu,
|
isFromBotMenu,
|
||||||
startParam,
|
startParam,
|
||||||
sendAs,
|
sendAs,
|
||||||
|
isFullscreen,
|
||||||
});
|
});
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { url: webViewUrl, queryId } = result;
|
const { url: webViewUrl, queryId, isFullScreen } = result;
|
||||||
|
|
||||||
global = getGlobal();
|
global = getGlobal();
|
||||||
const newActiveApp: WebApp = {
|
const newActiveApp: WebApp = {
|
||||||
requestUrl: url,
|
requestUrl: url,
|
||||||
url: webViewUrl,
|
url: webViewUrl,
|
||||||
|
appName: bot.firstName,
|
||||||
botId,
|
botId,
|
||||||
peerId,
|
peerId,
|
||||||
queryId,
|
queryId,
|
||||||
@ -605,11 +610,15 @@ addActionHandler('requestWebView', async (global, actions, payload): Promise<voi
|
|||||||
};
|
};
|
||||||
global = addWebAppToOpenList(global, newActiveApp, true, true, tabId);
|
global = addWebAppToOpenList(global, newActiveApp, true, true, tabId);
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
|
|
||||||
|
if (isFullScreen && getIsWebAppsFullscreenSupported()) {
|
||||||
|
actions.changeWebAppModalState({ state: 'fullScreen', tabId });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('requestMainWebView', async (global, actions, payload): Promise<void> => {
|
addActionHandler('requestMainWebView', async (global, actions, payload): Promise<void> => {
|
||||||
const {
|
const {
|
||||||
botId, peerId, theme, startParam, shouldMarkBotTrusted,
|
botId, peerId, theme, startParam, mode, shouldMarkBotTrusted,
|
||||||
tabId = getCurrentTabId(),
|
tabId = getCurrentTabId(),
|
||||||
} = payload;
|
} = payload;
|
||||||
|
|
||||||
@ -644,16 +653,18 @@ addActionHandler('requestMainWebView', async (global, actions, payload): Promise
|
|||||||
peer,
|
peer,
|
||||||
theme,
|
theme,
|
||||||
startParam,
|
startParam,
|
||||||
|
mode,
|
||||||
});
|
});
|
||||||
if (!result) {
|
if (!result) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { url: webViewUrl, queryId } = result;
|
const { url: webViewUrl, queryId, isFullscreen } = result;
|
||||||
|
|
||||||
global = getGlobal();
|
global = getGlobal();
|
||||||
const newActiveApp: WebApp = {
|
const newActiveApp: WebApp = {
|
||||||
url: webViewUrl,
|
url: webViewUrl,
|
||||||
|
appName: bot.firstName,
|
||||||
botId,
|
botId,
|
||||||
peerId,
|
peerId,
|
||||||
queryId,
|
queryId,
|
||||||
@ -661,6 +672,10 @@ addActionHandler('requestMainWebView', async (global, actions, payload): Promise
|
|||||||
};
|
};
|
||||||
global = addWebAppToOpenList(global, newActiveApp, true, true, tabId);
|
global = addWebAppToOpenList(global, newActiveApp, true, true, tabId);
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
|
|
||||||
|
if (isFullscreen && getIsWebAppsFullscreenSupported()) {
|
||||||
|
actions.changeWebAppModalState({ state: 'fullScreen', tabId });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('loadPreviewMedias', async (global, actions, payload): Promise<void> => {
|
addActionHandler('loadPreviewMedias', async (global, actions, payload): Promise<void> => {
|
||||||
@ -722,7 +737,7 @@ addActionHandler('closeWebAppsCloseConfirmationModal', (global, actions, payload
|
|||||||
|
|
||||||
addActionHandler('requestAppWebView', async (global, actions, payload): Promise<void> => {
|
addActionHandler('requestAppWebView', async (global, actions, payload): Promise<void> => {
|
||||||
const {
|
const {
|
||||||
botId, appName, startApp, theme, isWriteAllowed, isFromConfirm, shouldSkipBotTrustRequest,
|
botId, appName, startApp, mode, theme, isWriteAllowed, isFromConfirm, shouldSkipBotTrustRequest,
|
||||||
tabId = getCurrentTabId(),
|
tabId = getCurrentTabId(),
|
||||||
} = payload;
|
} = payload;
|
||||||
|
|
||||||
@ -794,10 +809,11 @@ addActionHandler('requestAppWebView', async (global, actions, payload): Promise<
|
|||||||
|
|
||||||
const peer = selectCurrentChat(global, tabId);
|
const peer = selectCurrentChat(global, tabId);
|
||||||
|
|
||||||
const url = await callApi('requestAppWebView', {
|
const { url, isFullscreen } = await callApi('requestAppWebView', {
|
||||||
peer: peer || bot,
|
peer: peer || bot,
|
||||||
app: botApp,
|
app: botApp,
|
||||||
startParam: startApp,
|
startParam: startApp,
|
||||||
|
mode,
|
||||||
isWriteAllowed,
|
isWriteAllowed,
|
||||||
theme,
|
theme,
|
||||||
});
|
});
|
||||||
@ -811,14 +827,17 @@ addActionHandler('requestAppWebView', async (global, actions, payload): Promise<
|
|||||||
|
|
||||||
const newActiveApp: WebApp = {
|
const newActiveApp: WebApp = {
|
||||||
url,
|
url,
|
||||||
|
appName: appName && bot.firstName,
|
||||||
peerId,
|
peerId,
|
||||||
botId,
|
botId,
|
||||||
appName,
|
|
||||||
buttonText: '',
|
buttonText: '',
|
||||||
};
|
};
|
||||||
global = addWebAppToOpenList(global, newActiveApp, true, true, tabId);
|
global = addWebAppToOpenList(global, newActiveApp, true, true, tabId);
|
||||||
|
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
|
|
||||||
|
if (isFullscreen && getIsWebAppsFullscreenSupported()) {
|
||||||
|
actions.changeWebAppModalState({ state: 'fullScreen', tabId });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('prolongWebView', async (global, actions, payload): Promise<void> => {
|
addActionHandler('prolongWebView', async (global, actions, payload): Promise<void> => {
|
||||||
|
|||||||
@ -1438,6 +1438,7 @@ addActionHandler('openTelegramLink', async (global, actions, payload): Promise<v
|
|||||||
startAttach: params.startattach,
|
startAttach: params.startattach,
|
||||||
attach: params.attach,
|
attach: params.attach,
|
||||||
startApp: params.startapp,
|
startApp: params.startapp,
|
||||||
|
mode: params.mode,
|
||||||
originalParts: [part1, part2, part3],
|
originalParts: [part1, part2, part3],
|
||||||
tabId,
|
tabId,
|
||||||
});
|
});
|
||||||
@ -1491,7 +1492,7 @@ addActionHandler('acceptChatInvite', async (global, actions, payload): Promise<v
|
|||||||
|
|
||||||
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, originalParts, startApp,
|
username, messageId, commentId, startParam, startAttach, attach, threadId, originalParts, startApp, mode,
|
||||||
text, onChatChanged, choose,
|
text, onChatChanged, choose,
|
||||||
tabId = getCurrentTabId(),
|
tabId = getCurrentTabId(),
|
||||||
} = payload;
|
} = payload;
|
||||||
@ -1530,6 +1531,7 @@ addActionHandler('openChatByUsername', async (global, actions, payload): Promise
|
|||||||
peerId: chat.id,
|
peerId: chat.id,
|
||||||
theme,
|
theme,
|
||||||
tabId,
|
tabId,
|
||||||
|
mode,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -1579,6 +1581,7 @@ addActionHandler('openChatByUsername', async (global, actions, payload): Promise
|
|||||||
botId: chatByUsername.id,
|
botId: chatByUsername.id,
|
||||||
tabId,
|
tabId,
|
||||||
startApp,
|
startApp,
|
||||||
|
mode,
|
||||||
theme,
|
theme,
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
|
|||||||
@ -110,11 +110,9 @@ addActionHandler('closeWebAppModal', (global, actions, payload): ActionReturnTyp
|
|||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('changeWebAppModalState', (global, actions, payload): ActionReturnType => {
|
addActionHandler('changeWebAppModalState', (global, actions, payload): ActionReturnType => {
|
||||||
const { tabId = getCurrentTabId() } = payload || {};
|
const { state, tabId = getCurrentTabId() } = payload;
|
||||||
const tabState = selectTabState(global, tabId);
|
|
||||||
|
|
||||||
const newModalState = tabState.webApps.modalState === 'maximized' ? 'minimized' : 'maximized';
|
return replaceWebAppModalState(global, state, tabId);
|
||||||
return replaceWebAppModalState(global, newModalState, tabId);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('setWebAppPaymentSlug', (global, actions, payload): ActionReturnType => {
|
addActionHandler('setWebAppPaymentSlug', (global, actions, payload): ActionReturnType => {
|
||||||
|
|||||||
@ -149,7 +149,7 @@ export type MessageListType =
|
|||||||
|
|
||||||
export type ChatListType = 'active' | 'archived' | 'saved';
|
export type ChatListType = 'active' | 'archived' | 'saved';
|
||||||
|
|
||||||
export type WebAppModalStateType = 'maximized' | 'minimized';
|
export type WebAppModalStateType = 'fullScreen' | 'maximized' | 'minimized';
|
||||||
|
|
||||||
export interface MessageList {
|
export interface MessageList {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
@ -1844,6 +1844,7 @@ export interface ActionPayloads {
|
|||||||
startAttach?: string;
|
startAttach?: string;
|
||||||
attach?: string;
|
attach?: string;
|
||||||
startApp?: string;
|
startApp?: string;
|
||||||
|
mode?: string;
|
||||||
choose?: ApiChatType[];
|
choose?: ApiChatType[];
|
||||||
text?: string;
|
text?: string;
|
||||||
originalParts?: (string | undefined)[];
|
originalParts?: (string | undefined)[];
|
||||||
@ -3121,6 +3122,7 @@ export interface ActionPayloads {
|
|||||||
buttonText: string;
|
buttonText: string;
|
||||||
isFromBotMenu?: boolean;
|
isFromBotMenu?: boolean;
|
||||||
startParam?: string;
|
startParam?: string;
|
||||||
|
isFullscreen?: boolean;
|
||||||
} & WithTabId;
|
} & WithTabId;
|
||||||
updateWebApp: {
|
updateWebApp: {
|
||||||
key: string;
|
key: string;
|
||||||
@ -3131,6 +3133,7 @@ export interface ActionPayloads {
|
|||||||
peerId: string;
|
peerId: string;
|
||||||
theme?: ApiThemeParameters;
|
theme?: ApiThemeParameters;
|
||||||
startParam?: string;
|
startParam?: string;
|
||||||
|
mode?: string;
|
||||||
shouldMarkBotTrusted?: boolean;
|
shouldMarkBotTrusted?: boolean;
|
||||||
} & WithTabId;
|
} & WithTabId;
|
||||||
prolongWebView: {
|
prolongWebView: {
|
||||||
@ -3155,6 +3158,7 @@ export interface ActionPayloads {
|
|||||||
appName: string;
|
appName: string;
|
||||||
theme?: ApiThemeParameters;
|
theme?: ApiThemeParameters;
|
||||||
startApp?: string;
|
startApp?: string;
|
||||||
|
mode?: string;
|
||||||
isWriteAllowed?: boolean;
|
isWriteAllowed?: boolean;
|
||||||
isFromConfirm?: boolean;
|
isFromConfirm?: boolean;
|
||||||
shouldSkipBotTrustRequest?: boolean;
|
shouldSkipBotTrustRequest?: boolean;
|
||||||
@ -3279,7 +3283,9 @@ export interface ActionPayloads {
|
|||||||
closeWebAppModal: ({
|
closeWebAppModal: ({
|
||||||
shouldSkipConfirmation?: boolean;
|
shouldSkipConfirmation?: boolean;
|
||||||
} & WithTabId) | undefined;
|
} & WithTabId) | undefined;
|
||||||
changeWebAppModalState: WithTabId | undefined;
|
changeWebAppModalState: {
|
||||||
|
state: WebAppModalStateType;
|
||||||
|
} & WithTabId;
|
||||||
|
|
||||||
// Misc
|
// Misc
|
||||||
refreshLangPackFromCache: {
|
refreshLangPackFromCache: {
|
||||||
|
|||||||
@ -29,6 +29,10 @@ export function getIsTablet() {
|
|||||||
return isTablet;
|
return isTablet;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getIsWebAppsFullscreenSupported() {
|
||||||
|
return !getIsMobile();
|
||||||
|
}
|
||||||
|
|
||||||
function handleMediaQueryChange() {
|
function handleMediaQueryChange() {
|
||||||
isMobile = mediaQueryCache.get('mobile')?.matches || false;
|
isMobile = mediaQueryCache.get('mobile')?.matches || false;
|
||||||
isTablet = !isMobile && (mediaQueryCache.get('tablet')?.matches || false);
|
isTablet = !isMobile && (mediaQueryCache.get('tablet')?.matches || false);
|
||||||
|
|||||||
@ -1,5 +1,7 @@
|
|||||||
import type { RefObject } from 'react';
|
import type { RefObject } from 'react';
|
||||||
import { useEffect, useSignal, useState } from '../lib/teact/teact';
|
import {
|
||||||
|
useEffect, useSignal, useState,
|
||||||
|
} from '../lib/teact/teact';
|
||||||
|
|
||||||
import buildStyle from '../util/buildStyle';
|
import buildStyle from '../util/buildStyle';
|
||||||
import { captureEvents } from '../util/captureEvents';
|
import { captureEvents } from '../util/captureEvents';
|
||||||
@ -17,12 +19,14 @@ export interface Point {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let resizeTimeout: number | undefined;
|
let resizeTimeout: number | undefined;
|
||||||
|
const FULLSCREEN_POSITION = { x: 0, y: 0 };
|
||||||
|
|
||||||
export default function useDraggable(
|
export default function useDraggable(
|
||||||
ref: RefObject<HTMLElement>,
|
ref: RefObject<HTMLElement>,
|
||||||
dragHandleElementRef: RefObject<HTMLElement>,
|
dragHandleElementRef: RefObject<HTMLElement>,
|
||||||
isEnabled: boolean = true,
|
isDragEnabled: boolean = true,
|
||||||
originalSize: Size,
|
originalSize: Size,
|
||||||
|
isFullscreen: boolean = false,
|
||||||
) {
|
) {
|
||||||
const [elementCurrentPosition, setElementCurrentPosition] = useState<Point | undefined>(undefined);
|
const [elementCurrentPosition, setElementCurrentPosition] = useState<Point | undefined>(undefined);
|
||||||
const [elementCurrentSize, setElementCurrentSize] = useState<Size | undefined>(undefined);
|
const [elementCurrentSize, setElementCurrentSize] = useState<Size | undefined>(undefined);
|
||||||
@ -48,6 +52,14 @@ export default function useDraggable(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const updateCurrentPosition = useLastCallback((position: Point) => {
|
||||||
|
if (!isFullscreen) setElementCurrentPosition({ x: position.x, y: position.y });
|
||||||
|
});
|
||||||
|
|
||||||
|
const getActualPosition = useLastCallback(() => {
|
||||||
|
return isFullscreen ? FULLSCREEN_POSITION : elementCurrentPosition;
|
||||||
|
});
|
||||||
|
|
||||||
const getCenteredPosition = useLastCallback(() => {
|
const getCenteredPosition = useLastCallback(() => {
|
||||||
if (!elementCurrentSize) return undefined;
|
if (!elementCurrentSize) return undefined;
|
||||||
const { width, height } = elementCurrentSize;
|
const { width, height } = elementCurrentSize;
|
||||||
@ -71,7 +83,7 @@ export default function useDraggable(
|
|||||||
const centeredPosition = getCenteredPosition();
|
const centeredPosition = getCenteredPosition();
|
||||||
if (!centeredPosition) return;
|
if (!centeredPosition) return;
|
||||||
|
|
||||||
setElementCurrentPosition({ x: centeredPosition.x, y: centeredPosition.y });
|
updateCurrentPosition(centeredPosition);
|
||||||
setIsInitiated();
|
setIsInitiated();
|
||||||
}
|
}
|
||||||
}, [elementCurrentSize, isInitiated, element]);
|
}, [elementCurrentSize, isInitiated, element]);
|
||||||
@ -95,10 +107,10 @@ export default function useDraggable(
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isEnabled) {
|
if (!isDragEnabled) {
|
||||||
stopDragging();
|
stopDragging();
|
||||||
}
|
}
|
||||||
}, [isEnabled]);
|
}, [isDragEnabled]);
|
||||||
|
|
||||||
const ensurePositionInVisibleArea = (x: number, y: number) => {
|
const ensurePositionInVisibleArea = (x: number, y: number) => {
|
||||||
const visibleArea = getVisibleArea();
|
const visibleArea = getVisibleArea();
|
||||||
@ -121,10 +133,11 @@ export default function useDraggable(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const adjustPositionWithinBounds = useLastCallback(() => {
|
const adjustPositionWithinBounds = useLastCallback(() => {
|
||||||
|
if (isFullscreen) return;
|
||||||
const position = !wasElementShown ? getCenteredPosition() : elementCurrentPosition;
|
const position = !wasElementShown ? getCenteredPosition() : elementCurrentPosition;
|
||||||
if (!elementCurrentSize || !position) return;
|
if (!elementCurrentSize || !position) return;
|
||||||
const newPosition = ensurePositionInVisibleArea(position.x, position.y);
|
const newPosition = ensurePositionInVisibleArea(position.x, position.y);
|
||||||
setElementCurrentPosition(newPosition);
|
updateCurrentPosition(newPosition);
|
||||||
});
|
});
|
||||||
|
|
||||||
const ensureSizeInVisibleArea = useLastCallback((sizeForCheck: Size) => {
|
const ensureSizeInVisibleArea = useLastCallback((sizeForCheck: Size) => {
|
||||||
@ -192,7 +205,7 @@ export default function useDraggable(
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cleanup: NoneToVoidFunction | undefined;
|
let cleanup: NoneToVoidFunction | undefined;
|
||||||
if (dragHandleElement && isEnabled) {
|
if (dragHandleElement && isDragEnabled) {
|
||||||
cleanup = captureEvents(dragHandleElement, {
|
cleanup = captureEvents(dragHandleElement, {
|
||||||
onCapture: handleStartDrag,
|
onCapture: handleStartDrag,
|
||||||
onDrag: handleDrag,
|
onDrag: handleDrag,
|
||||||
@ -202,11 +215,13 @@ export default function useDraggable(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
return cleanup;
|
return cleanup;
|
||||||
}, [handleDrag, handleStartDrag, isEnabled, dragHandleElement]);
|
}, [handleDrag, handleStartDrag, isDragEnabled, dragHandleElement]);
|
||||||
|
|
||||||
const cursorStyle = isDragging ? 'cursor: grabbing !important; ' : '';
|
const cursorStyle = isDragging ? 'cursor: grabbing !important; ' : '';
|
||||||
|
|
||||||
if (!isInitiated || !elementCurrentSize || !elementCurrentPosition) {
|
const actualPosition = getActualPosition();
|
||||||
|
|
||||||
|
if (!isInitiated || !elementCurrentSize || !actualPosition) {
|
||||||
return {
|
return {
|
||||||
isDragging: false,
|
isDragging: false,
|
||||||
style: cursorStyle,
|
style: cursorStyle,
|
||||||
@ -214,10 +229,10 @@ export default function useDraggable(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const style = buildStyle(
|
const style = buildStyle(
|
||||||
`left: ${elementCurrentPosition.x}px;`,
|
`left: ${actualPosition.x}px;`,
|
||||||
`top: ${elementCurrentPosition.y}px;`,
|
`top: ${actualPosition.y}px;`,
|
||||||
`width: ${elementCurrentSize.width}px;`,
|
!isFullscreen && `max-width: ${elementCurrentSize.width}px;`,
|
||||||
`height: ${elementCurrentSize.height}px;`,
|
!isFullscreen && `max-height: ${elementCurrentSize.height}px;`,
|
||||||
'position: fixed;',
|
'position: fixed;',
|
||||||
(isDragging || isWindowsResizing) && 'transition: none !important;',
|
(isDragging || isWindowsResizing) && 'transition: none !important;',
|
||||||
cursorStyle,
|
cursorStyle,
|
||||||
|
|||||||
@ -4,8 +4,9 @@ import { ElectronEvent } from '../../types/electron';
|
|||||||
|
|
||||||
import { IS_IOS } from '../../util/windowEnvironment';
|
import { IS_IOS } from '../../util/windowEnvironment';
|
||||||
|
|
||||||
|
type ElementType = HTMLElement;
|
||||||
type RefType = {
|
type RefType = {
|
||||||
current: HTMLVideoElement | null;
|
current: ElementType | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ReturnType = [boolean, () => void, () => void] | [false];
|
type ReturnType = [boolean, () => void, () => void] | [false];
|
||||||
@ -13,11 +14,12 @@ type CallbackType = (isPlayed: boolean) => void;
|
|||||||
|
|
||||||
const prop = getBrowserFullscreenElementProp();
|
const prop = getBrowserFullscreenElementProp();
|
||||||
|
|
||||||
export default function useFullscreen(elRef: RefType, setIsPlayed: CallbackType): ReturnType {
|
export default function useFullscreen(elRef: RefType, exitCallback?: CallbackType,
|
||||||
|
enterCallback?: CallbackType): ReturnType {
|
||||||
const [isFullscreen, setIsFullscreen] = useState(Boolean(prop && document[prop]));
|
const [isFullscreen, setIsFullscreen] = useState(Boolean(prop && document[prop]));
|
||||||
|
|
||||||
const setFullscreen = () => {
|
const setFullscreen = () => {
|
||||||
if (!elRef.current || !(prop || IS_IOS)) {
|
if (!elRef.current || !(prop || IS_IOS) || isFullscreen) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
safeRequestFullscreen(elRef.current);
|
safeRequestFullscreen(elRef.current);
|
||||||
@ -33,35 +35,45 @@ export default function useFullscreen(elRef: RefType, setIsPlayed: CallbackType)
|
|||||||
};
|
};
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const video = elRef.current;
|
const element = elRef.current;
|
||||||
const listener = () => {
|
const listener = () => {
|
||||||
const isEnabled = Boolean(prop && document[prop]);
|
const isEnabled = Boolean(prop && document[prop]);
|
||||||
setIsFullscreen(isEnabled);
|
setIsFullscreen(isEnabled);
|
||||||
|
if (isEnabled) {
|
||||||
|
enterCallback?.(false);
|
||||||
|
} else {
|
||||||
|
exitCallback?.(false);
|
||||||
|
}
|
||||||
// In Firefox fullscreen video controls are not visible by default, so we force them manually
|
// In Firefox fullscreen video controls are not visible by default, so we force them manually
|
||||||
video!.controls = isEnabled;
|
if (element instanceof HTMLVideoElement) element.controls = isEnabled;
|
||||||
};
|
};
|
||||||
const listenerEnter = () => { setIsFullscreen(true); };
|
|
||||||
|
const listenerEnter = () => {
|
||||||
|
setIsFullscreen(true);
|
||||||
|
if (enterCallback) enterCallback(true);
|
||||||
|
};
|
||||||
|
|
||||||
const listenerExit = () => {
|
const listenerExit = () => {
|
||||||
setIsFullscreen(false);
|
setIsFullscreen(false);
|
||||||
setIsPlayed(false);
|
if (exitCallback) exitCallback(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('fullscreenchange', listener, false);
|
document.addEventListener('fullscreenchange', listener, false);
|
||||||
document.addEventListener('webkitfullscreenchange', listener, false);
|
document.addEventListener('webkitfullscreenchange', listener, false);
|
||||||
document.addEventListener('mozfullscreenchange', listener, false);
|
document.addEventListener('mozfullscreenchange', listener, false);
|
||||||
|
|
||||||
if (video) {
|
if (element) {
|
||||||
video.addEventListener('webkitbeginfullscreen', listenerEnter, false);
|
element.addEventListener('webkitbeginfullscreen', listenerEnter, false);
|
||||||
video.addEventListener('webkitendfullscreen', listenerExit, false);
|
element.addEventListener('webkitendfullscreen', listenerExit, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document.removeEventListener('fullscreenchange', listener, false);
|
document.removeEventListener('fullscreenchange', listener, false);
|
||||||
document.removeEventListener('webkitfullscreenchange', listener, false);
|
document.removeEventListener('webkitfullscreenchange', listener, false);
|
||||||
document.removeEventListener('mozfullscreenchange', listener, false);
|
document.removeEventListener('mozfullscreenchange', listener, false);
|
||||||
if (video) {
|
if (element) {
|
||||||
video.removeEventListener('webkitbeginfullscreen', listenerEnter, false);
|
element.removeEventListener('webkitbeginfullscreen', listenerEnter, false);
|
||||||
video.removeEventListener('webkitendfullscreen', listenerExit, false);
|
element.removeEventListener('webkitendfullscreen', listenerExit, false);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
@ -117,15 +129,15 @@ export function checkIfFullscreen() {
|
|||||||
return Boolean(fullscreenProp && document[fullscreenProp]);
|
return Boolean(fullscreenProp && document[fullscreenProp]);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function safeRequestFullscreen(video: HTMLVideoElement) {
|
export function safeRequestFullscreen(element: ElementType) {
|
||||||
if (video.requestFullscreen) {
|
if (element.requestFullscreen) {
|
||||||
video.requestFullscreen();
|
element.requestFullscreen();
|
||||||
} else if (video.webkitRequestFullscreen) {
|
} else if (element.webkitRequestFullscreen) {
|
||||||
video.webkitRequestFullscreen();
|
element.webkitRequestFullscreen();
|
||||||
} else if (video.webkitEnterFullscreen) {
|
} else if (element.webkitEnterFullscreen) {
|
||||||
video.webkitEnterFullscreen();
|
element.webkitEnterFullscreen();
|
||||||
} else if (video.mozRequestFullScreen) {
|
} else if (element.mozRequestFullScreen) {
|
||||||
video.mozRequestFullScreen();
|
element.mozRequestFullScreen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -26,6 +26,13 @@ export type WebAppButtonOptions = {
|
|||||||
position?: 'left' | 'right' | 'top' | 'bottom';
|
position?: 'left' | 'right' | 'top' | 'bottom';
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SafeArea = {
|
||||||
|
top: number;
|
||||||
|
bottom: number;
|
||||||
|
left: number;
|
||||||
|
right: number;
|
||||||
|
};
|
||||||
|
|
||||||
export type WebAppInboundEvent =
|
export type WebAppInboundEvent =
|
||||||
WebAppEvent<'iframe_ready', {
|
WebAppEvent<'iframe_ready', {
|
||||||
reload_supported?: boolean;
|
reload_supported?: boolean;
|
||||||
@ -102,7 +109,10 @@ export type WebAppInboundEvent =
|
|||||||
WebAppEvent<'web_app_request_viewport' | 'web_app_request_theme' | 'web_app_ready' | 'web_app_expand'
|
WebAppEvent<'web_app_request_viewport' | 'web_app_request_theme' | 'web_app_ready' | 'web_app_expand'
|
||||||
| 'web_app_request_phone' | 'web_app_close' | 'web_app_close_scan_qr_popup'
|
| 'web_app_request_phone' | 'web_app_close' | 'web_app_close_scan_qr_popup'
|
||||||
| 'web_app_request_write_access' | 'web_app_request_phone' | 'iframe_will_reload'
|
| 'web_app_request_write_access' | 'web_app_request_phone' | 'iframe_will_reload'
|
||||||
| 'web_app_biometry_get_info' | 'web_app_biometry_open_settings', null>;
|
| 'web_app_biometry_get_info' | 'web_app_biometry_open_settings'
|
||||||
|
| 'web_app_request_fullscreen' | 'web_app_exit_fullscreen'
|
||||||
|
| 'web_app_request_safe_area' | 'web_app_request_content_safe_area',
|
||||||
|
null>;
|
||||||
|
|
||||||
export type WebAppOutboundEvent =
|
export type WebAppOutboundEvent =
|
||||||
WebAppEvent<'viewport_changed', {
|
WebAppEvent<'viewport_changed', {
|
||||||
@ -111,6 +121,8 @@ export type WebAppOutboundEvent =
|
|||||||
is_expanded?: boolean;
|
is_expanded?: boolean;
|
||||||
is_state_stable?: boolean;
|
is_state_stable?: boolean;
|
||||||
}> |
|
}> |
|
||||||
|
WebAppEvent<'content_safe_area_changed', SafeArea> |
|
||||||
|
WebAppEvent<'safe_area_changed', SafeArea> |
|
||||||
WebAppEvent<'theme_changed', {
|
WebAppEvent<'theme_changed', {
|
||||||
theme_params: {
|
theme_params: {
|
||||||
bg_color: string;
|
bg_color: string;
|
||||||
@ -133,6 +145,12 @@ export type WebAppOutboundEvent =
|
|||||||
WebAppEvent<'popup_closed', {
|
WebAppEvent<'popup_closed', {
|
||||||
button_id?: string;
|
button_id?: string;
|
||||||
}> |
|
}> |
|
||||||
|
WebAppEvent<'fullscreen_changed', {
|
||||||
|
is_fullscreen: boolean;
|
||||||
|
}> |
|
||||||
|
WebAppEvent<'fullscreen_failed', {
|
||||||
|
error: 'UNSUPPORTED' | string;
|
||||||
|
}> |
|
||||||
WebAppEvent<'qr_text_received', {
|
WebAppEvent<'qr_text_received', {
|
||||||
data: string;
|
data: string;
|
||||||
}> |
|
}> |
|
||||||
|
|||||||
@ -61,6 +61,7 @@ interface PublicUsernameOrBotLink {
|
|||||||
username: string;
|
username: string;
|
||||||
start?: string;
|
start?: string;
|
||||||
startApp?: string;
|
startApp?: string;
|
||||||
|
mode?: string;
|
||||||
appName?: string;
|
appName?: string;
|
||||||
startAttach?: string;
|
startAttach?: string;
|
||||||
attach?: string;
|
attach?: string;
|
||||||
@ -201,6 +202,7 @@ function parseTgLink(url: URL) {
|
|||||||
text: queryParams.text,
|
text: queryParams.text,
|
||||||
appName: queryParams.appname,
|
appName: queryParams.appname,
|
||||||
startApp: queryParams.startapp,
|
startApp: queryParams.startapp,
|
||||||
|
mode: queryParams.mode,
|
||||||
startAttach: queryParams.startattach,
|
startAttach: queryParams.startattach,
|
||||||
attach: queryParams.attach,
|
attach: queryParams.attach,
|
||||||
choose: queryParams.choose,
|
choose: queryParams.choose,
|
||||||
@ -293,6 +295,7 @@ function parseHttpLink(url: URL) {
|
|||||||
start: queryParams.start,
|
start: queryParams.start,
|
||||||
text: queryParams.text,
|
text: queryParams.text,
|
||||||
startApp: queryParams.startapp,
|
startApp: queryParams.startapp,
|
||||||
|
mode: queryParams.mode,
|
||||||
appName: undefined,
|
appName: undefined,
|
||||||
startAttach: queryParams.startattach,
|
startAttach: queryParams.startattach,
|
||||||
attach: queryParams.attach,
|
attach: queryParams.attach,
|
||||||
@ -523,6 +526,7 @@ function buildPublicUsernameOrBotLink(
|
|||||||
start,
|
start,
|
||||||
text,
|
text,
|
||||||
startApp,
|
startApp,
|
||||||
|
mode,
|
||||||
startAttach,
|
startAttach,
|
||||||
attach,
|
attach,
|
||||||
appName,
|
appName,
|
||||||
@ -539,6 +543,7 @@ function buildPublicUsernameOrBotLink(
|
|||||||
username,
|
username,
|
||||||
start,
|
start,
|
||||||
startApp,
|
startApp,
|
||||||
|
mode,
|
||||||
appName,
|
appName,
|
||||||
startAttach,
|
startAttach,
|
||||||
attach,
|
attach,
|
||||||
|
|||||||
@ -25,6 +25,7 @@ export const processDeepLink = (url: string): boolean => {
|
|||||||
startParam: parsedLink.start,
|
startParam: parsedLink.start,
|
||||||
text: parsedLink.text,
|
text: parsedLink.text,
|
||||||
startApp: parsedLink.startApp,
|
startApp: parsedLink.startApp,
|
||||||
|
mode: parsedLink.mode,
|
||||||
startAttach: parsedLink.startAttach,
|
startAttach: parsedLink.startAttach,
|
||||||
attach: parsedLink.attach,
|
attach: parsedLink.attach,
|
||||||
choose,
|
choose,
|
||||||
@ -81,7 +82,7 @@ export const processDeepLink = (url: string): boolean => {
|
|||||||
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, story, text,
|
appname, startapp, mode, story, text,
|
||||||
} = params;
|
} = params;
|
||||||
|
|
||||||
const hasBoost = params.hasOwnProperty('boost');
|
const hasBoost = params.hasOwnProperty('boost');
|
||||||
@ -92,6 +93,7 @@ export const processDeepLink = (url: string): boolean => {
|
|||||||
openChatByUsername({
|
openChatByUsername({
|
||||||
username: domain,
|
username: domain,
|
||||||
startApp: startapp,
|
startApp: startapp,
|
||||||
|
mode,
|
||||||
originalParts: [domain, appname],
|
originalParts: [domain, appname],
|
||||||
text,
|
text,
|
||||||
});
|
});
|
||||||
@ -117,6 +119,7 @@ export const processDeepLink = (url: string): boolean => {
|
|||||||
messageId: post ? Number(post) : undefined,
|
messageId: post ? Number(post) : undefined,
|
||||||
commentId: comment ? Number(comment) : undefined,
|
commentId: comment ? Number(comment) : undefined,
|
||||||
startParam: start,
|
startParam: start,
|
||||||
|
mode,
|
||||||
startAttach: startattach,
|
startAttach: startattach,
|
||||||
attach,
|
attach,
|
||||||
threadId,
|
threadId,
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user