Mini Apps: Support loading placeholder (#5285)
This commit is contained in:
parent
9290ea1d1c
commit
c0b89697cd
@ -5,6 +5,7 @@ import type {
|
||||
ApiAttachBotIcon,
|
||||
ApiAttachMenuPeerType,
|
||||
ApiBotApp,
|
||||
ApiBotAppSettings,
|
||||
ApiBotCommand,
|
||||
ApiBotInfo,
|
||||
ApiBotInlineMediaResult,
|
||||
@ -16,11 +17,13 @@ import type {
|
||||
ApiMessagesBotApp,
|
||||
} from '../../types';
|
||||
|
||||
import { numberToHexColor } from '../../../util/colors';
|
||||
import { pick } from '../../../util/iteratees';
|
||||
import { addDocumentToLocalDb } from '../helpers';
|
||||
import { buildApiPhoto, buildApiThumbnailFromStripped } from './common';
|
||||
import { omitVirtualClassFields } from './helpers';
|
||||
import { buildApiDocument, buildApiWebDocument, buildVideoFromDocument } from './messageContent';
|
||||
import { buildSvgPath } from './pathBytesToSvg';
|
||||
import { buildApiPeerId } from './peers';
|
||||
import { buildStickerFromDocument } from './symbols';
|
||||
|
||||
@ -111,7 +114,7 @@ function buildApiAttachMenuIcon(icon: GramJs.AttachMenuBotIcon): ApiAttachBotIco
|
||||
export function buildApiBotInfo(botInfo: GramJs.BotInfo, chatId: string): ApiBotInfo {
|
||||
const {
|
||||
description, descriptionPhoto, descriptionDocument, userId, commands, menuButton, privacyPolicyUrl,
|
||||
hasPreviewMedias,
|
||||
hasPreviewMedias, appSettings,
|
||||
} = botInfo;
|
||||
|
||||
const botId = userId && buildApiPeerId(userId, 'user');
|
||||
@ -129,6 +132,18 @@ export function buildApiBotInfo(botInfo: GramJs.BotInfo, chatId: string): ApiBot
|
||||
privacyPolicyUrl,
|
||||
commands: commandsArray?.length ? commandsArray : undefined,
|
||||
hasPreviewMedia: hasPreviewMedias,
|
||||
appSettings: appSettings && buildBotAppSettings(appSettings),
|
||||
};
|
||||
}
|
||||
|
||||
export function buildBotAppSettings(settings: GramJs.BotAppSettings): ApiBotAppSettings {
|
||||
const placeholderPath = settings.placeholderPath && buildSvgPath(settings.placeholderPath);
|
||||
return {
|
||||
backgroundColor: settings.backgroundColor ? numberToHexColor(settings.backgroundColor) : undefined,
|
||||
backgroundDarkColor: settings.backgroundDarkColor ? numberToHexColor(settings.backgroundDarkColor) : undefined,
|
||||
headerColor: settings.headerColor ? numberToHexColor(settings.headerColor) : undefined,
|
||||
headerDarkColor: settings.headerDarkColor ? numberToHexColor(settings.headerDarkColor) : undefined,
|
||||
placeholderPath,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -10,6 +10,7 @@ import type {
|
||||
LangPackStringValue,
|
||||
} from '../../types';
|
||||
|
||||
import { numberToHexColor } from '../../../util/colors';
|
||||
import {
|
||||
buildCollectionByCallback, omit, omitUndefined, pick,
|
||||
} from '../../../util/iteratees';
|
||||
@ -297,7 +298,7 @@ export function buildApiLanguage(lang: GramJs.TypeLangPackLanguage): ApiLanguage
|
||||
|
||||
function buildApiPeerColorSet(colorSet: GramJs.help.TypePeerColorSet) {
|
||||
if (colorSet instanceof GramJs.help.PeerColorSet) {
|
||||
return colorSet.colors.map((color) => `#${color.toString(16).padStart(6, '0')}`);
|
||||
return colorSet.colors.map((color) => numberToHexColor(color));
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -6,12 +6,12 @@ const LOOKUP = 'AACAAAAHAAALMAAAQASTAVAAAZaacaaaahaaalmaaaqastava.az0123456789-,
|
||||
|
||||
export function pathBytesToSvg(bytes: Buffer, width: number, height: number) {
|
||||
return TEMPLATE
|
||||
.replace('{{path}}', buildPath(bytes))
|
||||
.replace('{{path}}', buildSvgPath(bytes))
|
||||
.replace('{{width}}', String(width))
|
||||
.replace('{{height}}', String(height));
|
||||
}
|
||||
|
||||
function buildPath(bytes: Buffer) {
|
||||
export function buildSvgPath(bytes: Buffer) {
|
||||
let path = 'M';
|
||||
|
||||
const len = bytes.length;
|
||||
|
||||
@ -67,6 +67,14 @@ type ApiBotMenuButtonWebApp = {
|
||||
|
||||
export type ApiBotMenuButton = ApiBotMenuButtonWebApp | ApiBotMenuButtonCommands;
|
||||
|
||||
export interface ApiBotAppSettings {
|
||||
placeholderPath?: string;
|
||||
backgroundColor?: string;
|
||||
backgroundDarkColor?: string;
|
||||
headerColor?: string;
|
||||
headerDarkColor?: string;
|
||||
}
|
||||
|
||||
export interface ApiBotInfo {
|
||||
botId: string;
|
||||
commands?: ApiBotCommand[];
|
||||
@ -76,6 +84,7 @@ export interface ApiBotInfo {
|
||||
menuButton: ApiBotMenuButton;
|
||||
privacyPolicyUrl?: string;
|
||||
hasPreviewMedia?: true;
|
||||
appSettings?: ApiBotAppSettings;
|
||||
}
|
||||
|
||||
export interface ApiBotPreviewMedia extends MediaContainer {
|
||||
|
||||
@ -13,7 +13,8 @@
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
.loadingPlaceholder {
|
||||
width: 4rem;
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
@ -22,6 +23,40 @@
|
||||
transition: opacity 0.25s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes fadeOpacityBreath {
|
||||
0% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
50% {
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0.4;
|
||||
}
|
||||
}
|
||||
|
||||
.placeholderPath {
|
||||
animation: 2s linear -0.8s infinite fadeOpacityBreath;
|
||||
}
|
||||
|
||||
.defaultPlaceholderGrid {
|
||||
width: 4.5rem;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 0.25rem;
|
||||
margin: 0 auto;
|
||||
animation: 2s linear -0.8s infinite fadeOpacityBreath;
|
||||
}
|
||||
|
||||
.placeholderSquare {
|
||||
width: 100%;
|
||||
aspect-ratio: 1;
|
||||
background-color: black;
|
||||
border-radius: 0.375rem;
|
||||
}
|
||||
|
||||
.hide {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useEffect,
|
||||
useMemo,
|
||||
useRef, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiAttachBot, ApiChat, ApiUser } from '../../../api/types';
|
||||
import type {
|
||||
ApiAttachBot, ApiBotAppSettings, ApiChat, ApiUser,
|
||||
} from '../../../api/types';
|
||||
import type { TabState, WebApp, WebAppModalStateType } from '../../../global/types';
|
||||
import type { ThemeKey } from '../../../types';
|
||||
import type { PopupOptions, WebAppInboundEvent, WebAppOutboundEvent } from '../../../types/webapp';
|
||||
@ -14,7 +17,7 @@ import { TME_LINK_PREFIX } from '../../../config';
|
||||
import { convertToApiChatType } from '../../../global/helpers';
|
||||
import { getWebAppKey } from '../../../global/helpers/bots';
|
||||
import {
|
||||
selectCurrentChat, selectTabState, selectTheme, selectUser,
|
||||
selectCurrentChat, selectTabState, selectTheme, selectUser, selectUserFullInfo,
|
||||
selectWebApp,
|
||||
} from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
@ -69,6 +72,7 @@ export type OwnProps = {
|
||||
type StateProps = {
|
||||
chat?: ApiChat;
|
||||
bot?: ApiUser;
|
||||
botAppSettings?: ApiBotAppSettings;
|
||||
attachBot?: ApiAttachBot;
|
||||
theme?: ThemeKey;
|
||||
isPaymentModalOpen?: boolean;
|
||||
@ -113,6 +117,7 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
||||
frameSize,
|
||||
isMultiTabSupported,
|
||||
onContextMenuButtonClick,
|
||||
botAppSettings,
|
||||
}) => {
|
||||
const {
|
||||
closeActiveWebApp,
|
||||
@ -169,8 +174,13 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
||||
const activeWebApp = modal?.activeWebAppKey ? modal.openedWebApps[modal.activeWebAppKey] : undefined;
|
||||
const activeWebAppName = activeWebApp?.appName;
|
||||
const {
|
||||
url, buttonText, headerColor, serverHeaderColorKey, serverHeaderColor, isBackButtonVisible,
|
||||
url, buttonText, isBackButtonVisible,
|
||||
} = webApp || {};
|
||||
|
||||
const {
|
||||
placeholderPath,
|
||||
} = botAppSettings || {};
|
||||
|
||||
const isCloseModalOpen = Boolean(webApp?.isCloseModalOpen);
|
||||
const isRemoveModalOpen = Boolean(webApp?.isRemoveModalOpen);
|
||||
|
||||
@ -184,11 +194,36 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
||||
updateWebApp({ key: webAppKey, update: updatedPartialWebApp });
|
||||
});
|
||||
|
||||
const themeParams = useMemo(() => {
|
||||
return extractCurrentThemeParams();
|
||||
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
|
||||
}, [theme]);
|
||||
|
||||
useEffect(() => {
|
||||
const themeParams = extractCurrentThemeParams();
|
||||
setBottomBarColor(themeParams.secondary_bg_color);
|
||||
updateCurrentWebApp({ headerColor: themeParams.bg_color, backgroundColor: themeParams.bg_color });
|
||||
}, []);
|
||||
}, [themeParams]);
|
||||
|
||||
const themeBackgroundColor = themeParams.bg_color;
|
||||
const [backgroundColorFromEvent, setBackgroundColorFromEvent] = useState<string | undefined>();
|
||||
const backgroundColorFromSettings = theme === 'light' ? botAppSettings?.backgroundColor
|
||||
: botAppSettings?.backgroundDarkColor;
|
||||
|
||||
useEffect(() => {
|
||||
const color = backgroundColorFromEvent || backgroundColorFromSettings || themeBackgroundColor;
|
||||
|
||||
updateCurrentWebApp({ backgroundColor: color });
|
||||
}, [themeBackgroundColor, backgroundColorFromEvent, backgroundColorFromSettings]);
|
||||
|
||||
const themeHeaderColor = themeParams.bg_color;
|
||||
const [headerColorFromEvent, setHeaderColorFromEvent] = useState<string | undefined>();
|
||||
const headerColorFromSettings = theme === 'light' ? botAppSettings?.headerColor
|
||||
: botAppSettings?.headerDarkColor;
|
||||
|
||||
useEffect(() => {
|
||||
const color = headerColorFromEvent || headerColorFromSettings || themeHeaderColor;
|
||||
|
||||
updateCurrentWebApp({ headerColor: color });
|
||||
}, [themeHeaderColor, headerColorFromEvent, headerColorFromSettings]);
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const frameRef = useRef<HTMLIFrameElement>(null);
|
||||
@ -210,8 +245,8 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
||||
if (isActive) registerReloadFrameCallback(reloadFrame);
|
||||
}, [reloadFrame, registerReloadFrameCallback, isActive]);
|
||||
|
||||
const isMainButtonVisible = mainButton?.isVisible && mainButton.text.trim().length > 0;
|
||||
const isSecondaryButtonVisible = secondaryButton?.isVisible && secondaryButton.text.trim().length > 0;
|
||||
const isMainButtonVisible = isLoaded && mainButton?.isVisible && mainButton.text.trim().length > 0;
|
||||
const isSecondaryButtonVisible = isLoaded && secondaryButton?.isVisible && secondaryButton.text.trim().length > 0;
|
||||
|
||||
const handleHideCloseModal = useLastCallback(() => {
|
||||
updateCurrentWebApp({ isCloseModalOpen: false });
|
||||
@ -254,32 +289,8 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
||||
handleAppPopupClose();
|
||||
});
|
||||
|
||||
const calculateHeaderColor = useLastCallback(
|
||||
(serverColorKey? : 'bg_color' | 'secondary_bg_color', serverColor? : string) => {
|
||||
if (serverColorKey) {
|
||||
const themeParams = extractCurrentThemeParams();
|
||||
const key = serverColorKey;
|
||||
const newColor = themeParams[key];
|
||||
const color = validateHexColor(newColor) ? newColor : headerColor;
|
||||
updateCurrentWebApp({ headerColor: color, serverHeaderColorKey: key });
|
||||
}
|
||||
|
||||
if (serverColor) {
|
||||
const color = validateHexColor(serverColor) ? serverColor : headerColor;
|
||||
updateCurrentWebApp({ headerColor: color, serverHeaderColor: serverColor });
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
const updateHeaderColor = useLastCallback(
|
||||
() => {
|
||||
calculateHeaderColor(serverHeaderColorKey, serverHeaderColor);
|
||||
},
|
||||
);
|
||||
|
||||
const sendThemeCallback = useLastCallback(() => {
|
||||
sendTheme();
|
||||
updateHeaderColor();
|
||||
});
|
||||
|
||||
// Notify view that theme changed
|
||||
@ -526,13 +537,12 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
|
||||
if (eventType === 'web_app_set_background_color') {
|
||||
const themeParams = extractCurrentThemeParams();
|
||||
const color = validateHexColor(eventData.color) ? eventData.color : themeParams.bg_color;
|
||||
updateCurrentWebApp({ backgroundColor: color });
|
||||
setBackgroundColorFromEvent(validateHexColor(eventData.color) ? eventData.color : undefined);
|
||||
}
|
||||
|
||||
if (eventType === 'web_app_set_header_color') {
|
||||
calculateHeaderColor(eventData.color_key, eventData.color);
|
||||
const key = eventData.color_key;
|
||||
setHeaderColorFromEvent(eventData.color || (key ? themeParams[key] : undefined));
|
||||
}
|
||||
|
||||
if (eventType === 'web_app_set_bottom_bar_color') {
|
||||
@ -882,6 +892,32 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
function renderDefaultPlaceholder() {
|
||||
const className = buildClassName(styles.loadingPlaceholder, styles.defaultPlaceholderGrid, isLoaded && styles.hide);
|
||||
return (
|
||||
<div className={className}>
|
||||
<div className={styles.placeholderSquare} />
|
||||
<div className={styles.placeholderSquare} />
|
||||
<div className={styles.placeholderSquare} />
|
||||
<div className={styles.placeholderSquare} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function renderPlaceholder() {
|
||||
if (!placeholderPath) {
|
||||
return renderDefaultPlaceholder();
|
||||
}
|
||||
return (
|
||||
<svg
|
||||
className={buildClassName(styles.loadingPlaceholder, isLoaded && styles.hide)}
|
||||
viewBox="0 0 512 512"
|
||||
>
|
||||
<path className={styles.placeholderPath} d={placeholderPath} />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
@ -892,7 +928,7 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
||||
)}
|
||||
>
|
||||
{isFullscreen && getIsWebAppsFullscreenSupported() && renderFullscreenHeaderPanel()}
|
||||
{!isMinimizedState && <Spinner className={buildClassName(styles.loadingSpinner, isLoaded && styles.hide)} />}
|
||||
{!isMinimizedState && renderPlaceholder()}
|
||||
<iframe
|
||||
className={buildClassName(
|
||||
styles.frame,
|
||||
@ -1045,6 +1081,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
|
||||
const attachBot = activeBotId ? global.attachMenu.bots[activeBotId] : undefined;
|
||||
const bot = activeBotId ? selectUser(global, activeBotId) : undefined;
|
||||
const userFullInfo = activeBotId ? selectUserFullInfo(global, activeBotId) : undefined;
|
||||
const botAppSettings = userFullInfo?.botInfo?.appSettings;
|
||||
const chat = selectCurrentChat(global);
|
||||
const theme = selectTheme(global);
|
||||
const { isPaymentModalOpen, status: regularPaymentStatus } = selectTabState(global).payment;
|
||||
@ -1060,6 +1098,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
isPaymentModalOpen: isPaymentModalOpen || Boolean(starsInputInvoice),
|
||||
paymentStatus,
|
||||
modalState,
|
||||
botAppSettings,
|
||||
};
|
||||
},
|
||||
)(WebAppModalTabContent));
|
||||
|
||||
@ -1337,8 +1337,6 @@ export type WebApp = {
|
||||
isCloseModalOpen?: boolean;
|
||||
shouldConfirmClosing?: boolean;
|
||||
headerColor?: string;
|
||||
serverHeaderColor?: string;
|
||||
serverHeaderColorKey?: 'bg_color' | 'secondary_bg_color';
|
||||
backgroundColor?: string;
|
||||
isBackButtonVisible?: boolean;
|
||||
isSettingsButtonVisible?: boolean;
|
||||
|
||||
@ -216,6 +216,10 @@ export const convertToRGBA = (color: number): string => {
|
||||
return `rgba(${red}, ${green}, ${blue}, ${alphaFloat})`;
|
||||
};
|
||||
|
||||
export const numberToHexColor = (color: number): string => {
|
||||
return `#${color.toString(16).padStart(6, '0')}`;
|
||||
};
|
||||
|
||||
export const getTextColor = (color: number): string => {
|
||||
const r = (color >> 16) & 0xff;
|
||||
const g = (color >> 8) & 0xff;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user