Mini Apps: Various fixes (#5471)

This commit is contained in:
Alexander Zinchuk 2025-01-21 18:21:24 +01:00
parent d0d41d0875
commit 93cb31c980
11 changed files with 153 additions and 55 deletions

View File

@ -32,7 +32,7 @@
justify-content: center; justify-content: center;
align-items: center; align-items: center;
gap: 0.125rem; gap: 0.125rem;
grid-template-columns: repeat(5, 1fr); grid-template-columns: repeat(auto-fit, minmax(4rem, 1fr));
padding: 0.125rem; padding: 0.125rem;
padding-bottom: 1.25em; padding-bottom: 1.25em;
} }

View File

@ -210,7 +210,6 @@
display: flex; display: flex;
align-items: center; align-items: center;
height: 100%; height: 100%;
flex-grow: 1;
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
@ -233,6 +232,12 @@
} }
} }
.tool-bar {
flex-grow: 1;
display: flex;
justify-content: flex-end;
}
.tab-button-wrapper { .tab-button-wrapper {
display: flex; display: flex;
margin-left: -0.5rem; margin-left: -0.5rem;
@ -362,8 +367,12 @@
height: 1.75rem !important; height: 1.75rem !important;
} }
.more-apps-button {
margin-right: auto;
}
.fullscreenButton { .fullscreenButton {
margin-right: 0.5rem; margin-inline: 0.5rem;
} }
.tab-close-button { .tab-close-button {

View File

@ -9,7 +9,7 @@ import { getActions, getGlobal, withGlobal } from '../../../global';
import type { ApiAttachBot, ApiChat, ApiUser } from '../../../api/types'; import type { ApiAttachBot, ApiChat, ApiUser } from '../../../api/types';
import type { TabState } from '../../../global/types'; import type { TabState } from '../../../global/types';
import type { ThemeKey } from '../../../types'; import type { Point, Size, ThemeKey } from '../../../types';
import type { WebApp, WebAppOutboundEvent } from '../../../types/webapp'; import type { WebApp, WebAppOutboundEvent } from '../../../types/webapp';
import { RESIZE_HANDLE_CLASS_NAME } from '../../../config'; import { RESIZE_HANDLE_CLASS_NAME } from '../../../config';
@ -61,6 +61,8 @@ type StateProps = {
bot?: ApiUser; bot?: ApiUser;
attachBot?: ApiAttachBot; attachBot?: ApiAttachBot;
theme?: ThemeKey; theme?: ThemeKey;
cachedSize?: Size;
cachedPosition?: Point;
}; };
const PROLONG_INTERVAL = 45000; // 45s const PROLONG_INTERVAL = 45000; // 45s
@ -76,6 +78,8 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
bot, bot,
attachBot, attachBot,
theme, theme,
cachedSize,
cachedPosition,
}) => { }) => {
const { const {
closeActiveWebApp, closeActiveWebApp,
@ -88,11 +92,11 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
updateWebApp, updateWebApp,
openMoreAppsTab, openMoreAppsTab,
closeMoreAppsTab, closeMoreAppsTab,
updateMiniAppCachedPosition,
updateMiniAppCachedSize,
} = getActions(); } = getActions();
const [getMaximizedStateSize, setMaximizedStateSize] = useSignal( const [getMaximizedStateSize, setMaximizedStateSize] = useSignal(cachedSize || DEFAULT_MAXIMIZED_STATE_SIZE);
{ width: DEFAULT_MAXIMIZED_STATE_SIZE.width, height: DEFAULT_MAXIMIZED_STATE_SIZE.height },
);
function getSize() { function getSize() {
if (modal?.modalState === 'fullScreen') return windowSize.get(); if (modal?.modalState === 'fullScreen') return windowSize.get();
@ -173,6 +177,7 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
isResizing, isResizing,
style: draggableStyle, style: draggableStyle,
size, size,
position,
} = useDraggable( } = useDraggable(
ref, ref,
headerRef, headerRef,
@ -180,8 +185,21 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
getSize(), getSize(),
isFullScreen, isFullScreen,
getMinimumSize(), getMinimumSize(),
cachedPosition,
); );
const x = position?.x;
const y = position?.y;
useEffect(() => {
if (!isDragging && x !== undefined && y !== undefined) {
updateMiniAppCachedPosition({ position: { x, y } });
}
}, [isDragging, x, y]);
useEffect(() => {
if (!isDragging && size && isMaximizedState) { updateMiniAppCachedSize({ size }); }
}, [isDragging, isMaximizedState, size]);
const currentSize = size || getSize(); const currentSize = size || getSize();
const currentWidth = currentSize.width; const currentWidth = currentSize.width;
@ -544,7 +562,6 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
) )
))} ))}
{isMoreAppsTabActive && renderMoreAppsTab()} {isMoreAppsTabActive && renderMoreAppsTab()}
{!isMoreAppsTabActive && renderMoreAppsButton()}
</div> </div>
); );
} }
@ -585,32 +602,38 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
{renderTabs()} {renderTabs()}
{renderMoreMenu()} {renderMoreMenu()}
<Button <div className={styles.toolBar}>
className={buildClassName( {!isMoreAppsTabActive && renderMoreAppsButton()}
styles.windowStateButton,
styles.fullscreenButton,
'no-drag',
)}
round
color="translucent"
size="tiny"
onClick={handleFullscreenClick}
>
<Icon className={styles.stateIcon} name="expand-modal" />
</Button>
<Button {!isMoreAppsTabActive && (
className={buildClassName( <Button
styles.windowStateButton, className={buildClassName(
'no-drag', styles.windowStateButton,
styles.fullscreenButton,
'no-drag',
)}
round
color="translucent"
size="tiny"
onClick={handleFullscreenClick}
>
<Icon className={styles.stateIcon} name="expand-modal" />
</Button>
)} )}
round
color="translucent" <Button
size="tiny" className={buildClassName(
onClick={handleCollapseClick} styles.windowStateButton,
> 'no-drag',
<Icon className={styles.stateIcon} name="collapse-modal" /> )}
</Button> round
color="translucent"
size="tiny"
onClick={handleCollapseClick}
>
<Icon className={styles.stateIcon} name="collapse-modal" />
</Button>
</div>
</div> </div>
); );
} }
@ -705,12 +728,16 @@ export default memo(withGlobal<OwnProps>(
const bot = activeBotId ? selectUser(global, activeBotId) : undefined; const bot = activeBotId ? selectUser(global, activeBotId) : undefined;
const chat = selectCurrentChat(global); const chat = selectCurrentChat(global);
const theme = selectTheme(global); const theme = selectTheme(global);
const cachedPosition = global.settings.miniAppsCachedPosition;
const cachedSize = global.settings.miniAppsCachedSize;
return { return {
attachBot, attachBot,
bot, bot,
chat, chat,
theme, theme,
cachedPosition,
cachedSize,
}; };
}, },
)(WebAppModal)); )(WebAppModal));

View File

@ -3,6 +3,11 @@
.root { .root {
height: 100%; height: 100%;
width: 100%; width: 100%;
position: relative;
display: flex;
flex-direction: column;
padding: 0;
z-index: 0;
} }
.multi-tab { .multi-tab {
@ -84,13 +89,21 @@
.secondary-button, .secondary-button,
.main-button { .main-button {
--color-transition: 0.15s;
--transform-transition: 0.25s ease-in-out;
flex-grow: 1; flex-grow: 1;
margin: 0.5rem; margin: 0.5rem;
transform: translateY(100%); transform: translateY(100%);
opacity: 0; opacity: 0;
transition-property: background-color, color, transform, margin-inline, flex-grow, opacity, filter; transition:
transition-duration: 0.25s; background-color var(--color-transition),
transition-timing-function: ease-in-out; color var(--color-transition),
opacity var(--color-transition),
filter var(--color-transition),
transform var(--transform-transition),
margin-inline var(--transform-transition),
flex-grow var(--transform-transition);
&.visible { &.visible {
transform: translateY(0); transform: translateY(0);
@ -127,10 +140,18 @@
&.one-row { &.one-row {
align-items: center; align-items: center;
height: 4rem; height: 4rem;
@media (max-width: 600px) {
height: 3.5rem;
}
} }
&.two-rows { &.two-rows {
height: 7.75rem; height: 7.75rem;
@media (max-width: 600px) {
height: 6.9375rem;
}
} }
&.left-to-right { &.left-to-right {

View File

@ -32,7 +32,7 @@ const SCROLLBAR_STYLE = `* {
}`; }`;
const RELOAD_TIMEOUT = 500; const RELOAD_TIMEOUT = 500;
const SAFE_AREA_HEIGHT = 3.675 * REM; const FULLSCREEN_BUTTONS_AREA_HEIGHT = 3.675 * REM;
const useWebAppFrame = ( const useWebAppFrame = (
ref: React.RefObject<HTMLIFrameElement>, ref: React.RefObject<HTMLIFrameElement>,
@ -129,15 +129,13 @@ const useWebAppFrame = (
if (!ref.current) { if (!ref.current) {
return; return;
} }
const { height } = ref.current.getBoundingClientRect();
const safeAreaHeight = isFullscreen ? SAFE_AREA_HEIGHT : 0;
sendEvent({ sendEvent({
eventType: 'safe_area_changed', eventType: 'safe_area_changed',
eventData: { eventData: {
left: 0, left: 0,
right: 0, right: 0,
top: 0, top: 0,
bottom: height - safeAreaHeight, bottom: 0,
}, },
}); });
@ -146,7 +144,7 @@ const useWebAppFrame = (
eventData: { eventData: {
left: 0, left: 0,
right: 0, right: 0,
top: safeAreaHeight, top: isFullscreen ? FULLSCREEN_BUTTONS_AREA_HEIGHT : 0,
bottom: 0, bottom: 0,
}, },
}); });
@ -328,7 +326,6 @@ 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);
sendSafeArea();
}, [sendViewport, sendSafeArea, windowSize]); }, [sendViewport, sendSafeArea, windowSize]);
useEffect(() => { useEffect(() => {

View File

@ -117,6 +117,34 @@ addActionHandler('changeWebAppModalState', (global, actions, payload): ActionRet
return replaceWebAppModalState(global, state, tabId); return replaceWebAppModalState(global, state, tabId);
}); });
addActionHandler('updateMiniAppCachedPosition', (global, actions, payload): ActionReturnType => {
const { position } = payload;
global = {
...global,
settings: {
...global.settings,
miniAppsCachedPosition: position,
},
};
return global;
});
addActionHandler('updateMiniAppCachedSize', (global, actions, payload): ActionReturnType => {
const { size } = payload;
global = {
...global,
settings: {
...global.settings,
miniAppsCachedSize: size,
},
};
return global;
});
addActionHandler('setWebAppPaymentSlug', (global, actions, payload): ActionReturnType => { addActionHandler('setWebAppPaymentSlug', (global, actions, payload): ActionReturnType => {
const { tabId = getCurrentTabId() } = payload; const { tabId = getCurrentTabId() } = payload;
const activeWebApp = selectActiveWebApp(global, tabId); const activeWebApp = selectActiveWebApp(global, tabId);

View File

@ -632,7 +632,7 @@ function omitLocalMedia(message: ApiMessage): ApiMessage {
function reduceSettings<T extends GlobalState>(global: T): GlobalState['settings'] { function reduceSettings<T extends GlobalState>(global: T): GlobalState['settings'] {
const { const {
byKey, themes, performance, botVerificationShownPeerIds, byKey, themes, performance, botVerificationShownPeerIds, miniAppsCachedPosition, miniAppsCachedSize,
} = global.settings; } = global.settings;
return { return {
@ -642,6 +642,8 @@ function reduceSettings<T extends GlobalState>(global: T): GlobalState['settings
privacy: {}, privacy: {},
notifyExceptions: {}, notifyExceptions: {},
botVerificationShownPeerIds, botVerificationShownPeerIds,
miniAppsCachedPosition,
miniAppsCachedSize,
}; };
} }

View File

@ -73,10 +73,12 @@ import type {
NewChatMembersProgress, NewChatMembersProgress,
PaymentStep, PaymentStep,
PerformanceType, PerformanceType,
Point,
ProfileTabType, ProfileTabType,
ScrollTargetPosition, ScrollTargetPosition,
SettingsScreens, SettingsScreens,
SharedMediaType, SharedMediaType,
Size,
StarGiftInfo, StarGiftInfo,
StarsTransactionType, StarsTransactionType,
StoryViewerOrigin, StoryViewerOrigin,
@ -2040,7 +2042,12 @@ export interface ActionPayloads {
changeWebAppModalState: { changeWebAppModalState: {
state: WebAppModalStateType; state: WebAppModalStateType;
} & WithTabId; } & WithTabId;
updateMiniAppCachedPosition: {
position: Point;
};
updateMiniAppCachedSize: {
size: Size;
};
// Misc // Misc
refreshLangPackFromCache: { refreshLangPackFromCache: {
langCode: string; langCode: string;

View File

@ -55,7 +55,9 @@ import type {
IThemeSettings, IThemeSettings,
NotifyException, NotifyException,
PerformanceType, PerformanceType,
Point,
ServiceNotification, ServiceNotification,
Size,
StarGiftCategory, StarGiftCategory,
StarsSubscriptions, StarsSubscriptions,
StarsTransactionHistory, StarsTransactionHistory,
@ -404,6 +406,8 @@ export type GlobalState = {
paidReactionPrivacy?: boolean; paidReactionPrivacy?: boolean;
languages?: ApiLanguage[]; languages?: ApiLanguage[];
botVerificationShownPeerIds: string[]; botVerificationShownPeerIds: string[];
miniAppsCachedPosition?: Point;
miniAppsCachedSize?: Size;
}; };
push?: { push?: {

View File

@ -3,17 +3,14 @@ import {
useEffect, useSignal, useState, useEffect, useSignal, useState,
} from '../lib/teact/teact'; } from '../lib/teact/teact';
import type { Point, Size } from '../types';
import { RESIZE_HANDLE_SELECTOR } from '../config'; import { RESIZE_HANDLE_SELECTOR } from '../config';
import buildStyle from '../util/buildStyle'; import buildStyle from '../util/buildStyle';
import { captureEvents } from '../util/captureEvents'; import { captureEvents } from '../util/captureEvents';
import useFlag from './useFlag'; import useFlag from './useFlag';
import useLastCallback from './useLastCallback'; import useLastCallback from './useLastCallback';
export interface Size {
width: number;
height: number;
}
export enum ResizeHandleType { export enum ResizeHandleType {
Top, Top,
Bottom, Bottom,
@ -41,11 +38,6 @@ const resizeHandleSelectorsMap: Record<ResizeHandleSelectorType, ResizeHandleTyp
const resizeHandleSelectors = Object.keys(resizeHandleSelectorsMap) as ResizeHandleSelectorType[]; const resizeHandleSelectors = Object.keys(resizeHandleSelectorsMap) as ResizeHandleSelectorType[];
export interface Point {
x: number;
y: number;
}
let resizeTimeout: number | undefined; let resizeTimeout: number | undefined;
const FULLSCREEN_POSITION = { x: 0, y: 0 }; const FULLSCREEN_POSITION = { x: 0, y: 0 };
@ -56,8 +48,9 @@ export default function useDraggable(
originalSize: Size, originalSize: Size,
isFullscreen: boolean = false, isFullscreen: boolean = false,
minimumSize: Size = { width: 0, height: 0 }, minimumSize: Size = { width: 0, height: 0 },
cachedPosition?: Point,
) { ) {
const [elementCurrentPosition, setElementCurrentPosition] = useState<Point | undefined>(undefined); const [elementCurrentPosition, setElementCurrentPosition] = useState<Point | undefined>(cachedPosition);
const [elementCurrentSize, setElementCurrentSize] = useState<Size | undefined>(undefined); const [elementCurrentSize, setElementCurrentSize] = useState<Size | undefined>(undefined);
const [getElementPositionOnStartTransform, setElementPositionOnStartTransform] = useSignal({ x: 0, y: 0 }); const [getElementPositionOnStartTransform, setElementPositionOnStartTransform] = useSignal({ x: 0, y: 0 });
@ -211,7 +204,7 @@ export default function useDraggable(
const adjustPositionWithinBounds = useLastCallback(() => { const adjustPositionWithinBounds = useLastCallback(() => {
if (isFullscreen) return; if (isFullscreen) return;
const position = !wasElementShown ? getCenteredPosition() : elementCurrentPosition; const position = !wasElementShown && !cachedPosition ? getCenteredPosition() : elementCurrentPosition;
if (!elementCurrentSize || !position) return; if (!elementCurrentSize || !position) return;
const newPosition = ensurePositionInVisibleArea(position.x, position.y); const newPosition = ensurePositionInVisibleArea(position.x, position.y);
updateCurrentPosition(newPosition); updateCurrentPosition(newPosition);

View File

@ -629,6 +629,16 @@ export type ConfettiParams = OptionalCombine<{
height?: number; height?: number;
}>; }>;
export interface Size {
width: number;
height: number;
}
export interface Point {
x: number;
y: number;
}
export type WebPageMediaSize = 'large' | 'small'; export type WebPageMediaSize = 'large' | 'small';
export type StarGiftCategory = number | 'all' | 'limited' | 'stock'; export type StarGiftCategory = number | 'all' | 'limited' | 'stock';