Mini Apps: Support icons in Mini App buttons (#6880)

This commit is contained in:
Alexander Zinchuk 2026-04-27 14:29:02 +02:00
parent e42616655e
commit dcb01114ba
3 changed files with 79 additions and 6 deletions

View File

@ -238,6 +238,25 @@
.secondary-button-spinner, .secondary-button-spinner,
.main-button-spinner { .main-button-spinner {
position: absolute; position: absolute;
z-index: 1;
:global(.Spinner__inner) {
animation-play-state: running !important;
}
}
.button-emoji {
--custom-emoji-size: 1.25rem;
flex: 0 0 auto;
}
.button-emoji-with-label {
margin-inline-end: 0.375rem;
}
.button-label {
min-width: 0;
} }
.web-app-popup { .web-app-popup {

View File

@ -30,6 +30,7 @@ import buildStyle from '../../../util/buildStyle.ts';
import download from '../../../util/download'; import download from '../../../util/download';
import { extractCurrentThemeParams, validateHexColor } from '../../../util/themeStyle'; import { extractCurrentThemeParams, validateHexColor } from '../../../util/themeStyle';
import { callApi } from '../../../api/gramjs'; import { callApi } from '../../../api/gramjs';
import { REM } from '../../common/helpers/mediaDimensions';
import renderText from '../../common/helpers/renderText'; import renderText from '../../common/helpers/renderText';
import { getIsWebAppsFullscreenSupported } from '../../../hooks/useAppLayout'; import { getIsWebAppsFullscreenSupported } from '../../../hooks/useAppLayout';
@ -44,6 +45,7 @@ import useFullscreen, { checkIfFullscreen } from '../../../hooks/window/useFulls
import usePopupLimit from './hooks/usePopupLimit'; import usePopupLimit from './hooks/usePopupLimit';
import useWebAppFrame from './hooks/useWebAppFrame'; import useWebAppFrame from './hooks/useWebAppFrame';
import CustomEmoji from '../../common/CustomEmoji';
import Icon from '../../common/icons/Icon'; 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';
@ -60,6 +62,8 @@ type WebAppButton = {
color: string; color: string;
textColor: string; textColor: string;
isProgressVisible: boolean; isProgressVisible: boolean;
iconCustomEmojiId?: string;
hasShineEffect?: boolean;
position?: 'left' | 'right' | 'top' | 'bottom'; position?: 'left' | 'right' | 'top' | 'bottom';
}; };
@ -262,8 +266,18 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
if (isActive) registerReloadFrameCallback(reloadFrame); if (isActive) registerReloadFrameCallback(reloadFrame);
}, [reloadFrame, registerReloadFrameCallback, isActive]); }, [reloadFrame, registerReloadFrameCallback, isActive]);
const isMainButtonVisible = isLoaded && mainButton?.isVisible && mainButton.text.trim().length > 0; function hasBottomButtonContent(text: string | undefined, iconCustomEmojiId?: string) {
const isSecondaryButtonVisible = isLoaded && secondaryButton?.isVisible && secondaryButton.text.trim().length > 0; return Boolean(text?.trim().length || iconCustomEmojiId);
}
const isMainButtonVisible = isLoaded && mainButton?.isVisible && hasBottomButtonContent(
mainButton.text,
mainButton.iconCustomEmojiId,
);
const isSecondaryButtonVisible = isLoaded && secondaryButton?.isVisible && hasBottomButtonContent(
secondaryButton.text,
secondaryButton.iconCustomEmojiId,
);
const handleHideCloseModal = useLastCallback(() => { const handleHideCloseModal = useLastCallback(() => {
updateCurrentWebApp({ isCloseModalOpen: false }); updateCurrentWebApp({ isCloseModalOpen: false });
@ -651,12 +665,14 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
const color = eventData.color; const color = eventData.color;
const textColor = eventData.text_color; const textColor = eventData.text_color;
setMainButton({ setMainButton({
isVisible: eventData.is_visible && Boolean(eventData.text?.trim().length), isVisible: eventData.is_visible && hasBottomButtonContent(eventData.text, eventData.icon_custom_emoji_id),
isActive: eventData.is_active, isActive: eventData.is_active,
text: eventData.text, text: eventData.text,
color, color,
textColor, textColor,
isProgressVisible: eventData.is_progress_visible, isProgressVisible: eventData.is_progress_visible,
iconCustomEmojiId: eventData.icon_custom_emoji_id,
hasShineEffect: eventData.has_shine_effect,
}); });
} }
@ -664,12 +680,14 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
const color = eventData.color; const color = eventData.color;
const textColor = eventData.text_color; const textColor = eventData.text_color;
setSecondaryButton({ setSecondaryButton({
isVisible: eventData.is_visible && Boolean(eventData.text?.trim().length), isVisible: eventData.is_visible && hasBottomButtonContent(eventData.text, eventData.icon_custom_emoji_id),
isActive: eventData.is_active, isActive: eventData.is_active,
text: eventData.text, text: eventData.text,
color, color,
textColor, textColor,
isProgressVisible: eventData.is_progress_visible, isProgressVisible: eventData.is_progress_visible,
iconCustomEmojiId: eventData.icon_custom_emoji_id,
hasShineEffect: eventData.has_shine_effect,
position: eventData.position, position: eventData.position,
}); });
} }
@ -1084,6 +1102,32 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
); );
} }
function renderBottomButtonContent(text: string | undefined, iconCustomEmojiId?: string) {
const hasText = Boolean(text?.trim().length);
if (!hasText && !iconCustomEmojiId) return undefined;
const textContent = hasText ? renderText(text, ['emoji']) : undefined;
if (!iconCustomEmojiId) {
return textContent;
}
return (
<>
<CustomEmoji
className={buildClassName(styles.buttonEmoji, hasText && styles.buttonEmojiWithLabel)}
documentId={iconCustomEmojiId}
size={1.25 * REM}
forceAlways
/>
{hasText && (
<span className={styles.buttonLabel}>
{textContent}
</span>
)}
</>
);
}
return ( return (
<div <div
ref={containerRef} ref={containerRef}
@ -1133,9 +1177,13 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
style={`background-color: ${secondaryButtonCurrentColor}; color: ${secondaryButtonCurrentTextColor}`} style={`background-color: ${secondaryButtonCurrentColor}; color: ${secondaryButtonCurrentTextColor}`}
disabled={!secondaryButtonCurrentIsActive && !secondaryButton?.isProgressVisible} disabled={!secondaryButtonCurrentIsActive && !secondaryButton?.isProgressVisible}
nonInteractive={secondaryButton?.isProgressVisible} nonInteractive={secondaryButton?.isProgressVisible}
isShiny={secondaryButton?.hasShineEffect && !secondaryButton?.isProgressVisible}
onClick={handleSecondaryButtonClick} onClick={handleSecondaryButtonClick}
> >
{!secondaryButton?.isProgressVisible && secondaryButtonCurrentText} {!secondaryButton?.isProgressVisible && renderBottomButtonContent(
secondaryButtonCurrentText,
secondaryButton?.iconCustomEmojiId,
)}
{secondaryButton?.isProgressVisible {secondaryButton?.isProgressVisible
&& <Spinner className={styles.mainButtonSpinner} color="blue" />} && <Spinner className={styles.mainButtonSpinner} color="blue" />}
</Button> </Button>
@ -1149,9 +1197,13 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
style={`background-color: ${mainButtonCurrentColor}; color: ${mainButtonCurrentTextColor}`} style={`background-color: ${mainButtonCurrentColor}; color: ${mainButtonCurrentTextColor}`}
disabled={!mainButtonCurrentIsActive && !mainButton?.isProgressVisible} disabled={!mainButtonCurrentIsActive && !mainButton?.isProgressVisible}
nonInteractive={mainButton?.isProgressVisible} nonInteractive={mainButton?.isProgressVisible}
isShiny={mainButton?.hasShineEffect && !mainButton?.isProgressVisible}
onClick={handleMainButtonClick} onClick={handleMainButtonClick}
> >
{!mainButton?.isProgressVisible && mainButtonCurrentText} {!mainButton?.isProgressVisible && renderBottomButtonContent(
mainButtonCurrentText,
mainButton?.iconCustomEmojiId,
)}
{mainButton?.isProgressVisible && <Spinner className={styles.mainButtonSpinner} color="white" />} {mainButton?.isProgressVisible && <Spinner className={styles.mainButtonSpinner} color="white" />}
</Button> </Button>
</div> </div>

View File

@ -50,6 +50,8 @@ export type WebAppButtonOptions = {
color: string; color: string;
text_color: string; text_color: string;
is_progress_visible: boolean; is_progress_visible: boolean;
icon_custom_emoji_id?: string;
has_shine_effect?: boolean;
position?: 'left' | 'right' | 'top' | 'bottom'; position?: 'left' | 'right' | 'top' | 'bottom';
}; };