Mini Apps: Multi window in desktop (#4921)
This commit is contained in:
parent
9ddad6924f
commit
faee4152f6
1
src/assets/font-icons/collapse-modal.svg
Normal file
1
src/assets/font-icons/collapse-modal.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none"><path fill="#000" fill-rule="evenodd" stroke="null" d="M14.5 15.5a1 1 0 0 0 1-1v-7a1 1 0 1 0-2 0v6h-6a1 1 0 0 0 0 2zm3 1a1 1 0 0 0-1 1v7a1 1 0 1 0 2 0v-6h6a1 1 0 1 0 0-2z" clip-rule="evenodd"/><path fill="#000" fill-opacity=".3" fill-rule="evenodd" d="M14.5 15.5a1 1 0 0 0 1-1v-7a1 1 0 1 0-2 0v6h-6a1 1 0 0 0 0 2zm3 1a1 1 0 0 0-1 1v7a1 1 0 1 0 2 0v-6h6a1 1 0 1 0 0-2z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 471 B |
1
src/assets/font-icons/expand-modal.svg
Normal file
1
src/assets/font-icons/expand-modal.svg
Normal file
@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" fill="none"><path fill="#000" fill-rule="evenodd" d="M10 9a1 1 0 0 0-1 1v7a1 1 0 0 0 2 0v-6h6a1 1 0 0 0 0-2zm12 14a1 1 0 0 0 1-1v-7a1 1 0 1 0-2 0v6h-6a1 1 0 1 0 0 2z" clip-rule="evenodd"/><path fill="#000" fill-opacity=".3" fill-rule="evenodd" d="M58 0a1 1 0 0 0-1 1v7a1 1 0 0 0 2 0V2h6a1 1 0 0 0 0-2zm12 14a1 1 0 0 0 1-1V6a1 1 0 1 0-2 0v6h-6a1 1 0 1 0 0 2z" clip-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 449 B |
@ -1283,4 +1283,6 @@
|
||||
"CreditsBoxHistoryEntryGiftOutAbout" = "With Stars, {user} will be able to unlock content and services on Telegram. {link}"
|
||||
"CreditsBoxOutAbout" = "Review the {link} for Stars."
|
||||
"GiftStarsOutgoing" = "With Stars, {user} will be able to unlock content and services on Telegram."
|
||||
"MiniAppsMoreTabs_one" = "{botName} & {count} Other";
|
||||
"MiniAppsMoreTabs_other" = "{botName} & {count} Others";
|
||||
"PrizeCredits" = "Your prize is {count} Stars."
|
||||
|
||||
@ -12,6 +12,7 @@ export { default as UrlAuthModal } from '../components/modals/urlAuth/UrlAuthMod
|
||||
export { default as HistoryCalendar } from '../components/main/HistoryCalendar';
|
||||
export { default as NewContactModal } from '../components/main/NewContactModal';
|
||||
export { default as WebAppModal } from '../components/modals/webApp/WebAppModal';
|
||||
export { default as MinimizedWebAppModal } from '../components/modals/webApp/MinimizedWebAppModal';
|
||||
export { default as BotTrustModal } from '../components/main/BotTrustModal';
|
||||
export { default as AttachBotInstallModal } from '../components/modals/attachBotInstall/AttachBotInstallModal';
|
||||
export { default as DeleteFolderDialog } from '../components/main/DeleteFolderDialog';
|
||||
|
||||
@ -35,7 +35,7 @@ type ModalKey = keyof Pick<TabState,
|
||||
'reportAdModal' |
|
||||
'starsBalanceModal' |
|
||||
'isStarPaymentModalOpen' |
|
||||
'webApp' |
|
||||
'webApps' |
|
||||
'starsTransactionModal'
|
||||
>;
|
||||
|
||||
@ -52,7 +52,6 @@ type Entries<T> = {
|
||||
}[keyof T][];
|
||||
|
||||
const MODALS: ModalRegistry = {
|
||||
webApp: WebAppModal,
|
||||
giftCodeModal: GiftCodeModal,
|
||||
boostModal: BoostModal,
|
||||
chatlistModal: ChatlistModal,
|
||||
@ -61,6 +60,7 @@ const MODALS: ModalRegistry = {
|
||||
inviteViaLinkModal: InviteViaLinkModal,
|
||||
requestedAttachBotInstall: AttachBotInstallModal,
|
||||
reportAdModal: ReportAdModal,
|
||||
webApps: WebAppModal,
|
||||
collectibleInfoModal: CollectibleInfoModal,
|
||||
mapModal: MapModal,
|
||||
isStarPaymentModalOpen: StarsPaymentModal,
|
||||
|
||||
@ -0,0 +1,45 @@
|
||||
.root {
|
||||
height: 2.5rem;
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
|
||||
border-radius: var(--border-radius-default);
|
||||
background-color: transparent;
|
||||
padding: 0;
|
||||
padding-inline: 0.5rem;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.window-state-button,
|
||||
.button {
|
||||
width: 1.75rem !important;
|
||||
height: 1.75rem !important;
|
||||
}
|
||||
|
||||
.window-state-button {
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.avatars {
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.state-icon,
|
||||
.icon {
|
||||
color: var(--color-text-secondary);
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.state-icon {
|
||||
font-size: 2rem !important;
|
||||
}
|
||||
|
||||
.title {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
margin-left: 0.5rem;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
137
src/components/modals/webApp/MinimizedWebAppModal.tsx
Normal file
137
src/components/modals/webApp/MinimizedWebAppModal.tsx
Normal file
@ -0,0 +1,137 @@
|
||||
import React, {
|
||||
memo, useMemo,
|
||||
useRef,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, getGlobal, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiUser } from '../../../api/types';
|
||||
import type { WebApp } from '../../../global/types';
|
||||
|
||||
import { selectTabState, selectUser } from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { unique } from '../../../util/iteratees';
|
||||
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
|
||||
import AvatarList from '../../common/AvatarList';
|
||||
import Icon from '../../common/icons/Icon';
|
||||
import Button from '../../ui/Button';
|
||||
|
||||
import styles from './MinimizedWebAppModal.module.scss';
|
||||
|
||||
type StateProps = {
|
||||
activeTabBot?: ApiUser;
|
||||
isMinimizedState?: boolean;
|
||||
openedWebApps?: Record<string, WebApp>;
|
||||
};
|
||||
|
||||
const MinimizedWebAppModal = ({
|
||||
activeTabBot, isMinimizedState, openedWebApps,
|
||||
}: StateProps) => {
|
||||
const {
|
||||
changeWebAppModalState,
|
||||
closeWebAppModal,
|
||||
} = getActions();
|
||||
|
||||
const oldLang = useOldLang();
|
||||
const lang = useLang();
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const openedWebAppsValues = useMemo(() => {
|
||||
return openedWebApps && Object.values(openedWebApps);
|
||||
}, [openedWebApps]);
|
||||
|
||||
const openedTabsCount = openedWebAppsValues?.length;
|
||||
|
||||
const peers = useMemo(() => {
|
||||
if (!openedTabsCount) return [];
|
||||
|
||||
const global = getGlobal();
|
||||
const activeTabBotId = activeTabBot?.id;
|
||||
const openedApps = unique([activeTabBotId, ...openedWebAppsValues.map((app) => app.botId)]);
|
||||
const bots = openedApps.map((id) => id && selectUser(global, id)).filter(Boolean).slice(0, 3);
|
||||
return bots;
|
||||
}, [openedTabsCount, activeTabBot, openedWebAppsValues]);
|
||||
|
||||
const handleCloseClick = useLastCallback(() => {
|
||||
closeWebAppModal();
|
||||
});
|
||||
|
||||
const handleExpandClick = useLastCallback(() => {
|
||||
changeWebAppModalState();
|
||||
});
|
||||
|
||||
if (!isMinimizedState) return undefined;
|
||||
|
||||
function renderTitle() {
|
||||
const activeTabName = activeTabBot?.firstName;
|
||||
const title = openedTabsCount && activeTabName && openedTabsCount > 1
|
||||
? `${lang('MiniAppsMoreTabs',
|
||||
{
|
||||
botName: activeTabName,
|
||||
count: openedTabsCount - 1,
|
||||
})}`
|
||||
: activeTabName;
|
||||
|
||||
return (
|
||||
<div className={styles.title}>
|
||||
{title}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={buildClassName(
|
||||
styles.root,
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
className={styles.button}
|
||||
round
|
||||
color="translucent"
|
||||
size="tiny"
|
||||
ariaLabel={oldLang('Close')}
|
||||
onClick={handleCloseClick}
|
||||
>
|
||||
<Icon className={styles.icon} name="close" />
|
||||
</Button>
|
||||
<AvatarList className={styles.avatars} size="mini" peers={peers} />
|
||||
{renderTitle()}
|
||||
<Button
|
||||
className={buildClassName(
|
||||
styles.windowStateButton,
|
||||
'no-drag',
|
||||
)}
|
||||
round
|
||||
color="translucent"
|
||||
size="tiny"
|
||||
onClick={handleExpandClick}
|
||||
>
|
||||
<Icon className={styles.stateIcon} name="expand-modal" />
|
||||
</Button>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal(
|
||||
(global): StateProps => {
|
||||
const tabState = selectTabState(global);
|
||||
const webApps = tabState.webApps;
|
||||
|
||||
const { botId } = webApps?.activeWebApp || {};
|
||||
const { modalState, openedWebApps } = webApps || {};
|
||||
const isMinimizedState = modalState === 'minimized';
|
||||
const activeTabBot = botId ? selectUser(global, botId) : undefined;
|
||||
|
||||
return {
|
||||
activeTabBot,
|
||||
isMinimizedState,
|
||||
openedWebApps,
|
||||
};
|
||||
},
|
||||
)(MinimizedWebAppModal));
|
||||
@ -1,15 +1,21 @@
|
||||
@use '../../../styles/mixins';
|
||||
|
||||
.root {
|
||||
--color-transition: 0.25s ease-in-out;
|
||||
--more-button-opacity: 0;
|
||||
--modal-shadow: 0 0 1rem rgba(0, 0, 0, 0.15);
|
||||
--active-tab-background: var(--color-background);
|
||||
--state-transition: 0.25s cubic-bezier(0.29, 0.81, 0.27, 0.99);
|
||||
:global {
|
||||
.modal-header {
|
||||
color: var(--color-header-text);
|
||||
border-bottom: 1px solid var(--color-dividers);
|
||||
padding: 0.5rem;
|
||||
|
||||
border-bottom: 1px solid var(--color-dividers);
|
||||
transition: var(--color-transition) background-color, var(--color-transition) color;
|
||||
}
|
||||
|
||||
.modal-dialog {
|
||||
pointer-events: auto;
|
||||
height: 75%;
|
||||
justify-content: center;
|
||||
border: none;
|
||||
@ -18,6 +24,10 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.modal-container {
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -45,12 +55,243 @@
|
||||
}
|
||||
}
|
||||
|
||||
.multi-tab {
|
||||
:global {
|
||||
.modal-dialog {
|
||||
height: min(42.5rem, 85vh);
|
||||
max-width: 26.25rem;
|
||||
background-color: var(--color-web-app-browser);
|
||||
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;
|
||||
box-shadow: var(--modal-shadow);
|
||||
}
|
||||
|
||||
.modal-header {
|
||||
padding: 0;
|
||||
padding-inline: 0.5rem;
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
background-color: var(--color-background);
|
||||
border-top-right-radius: var(--border-radius-default);
|
||||
border-top-left-radius: var(--border-radius-default);
|
||||
box-shadow: var(--modal-shadow);
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.modal-dialog {
|
||||
background-color: var(--color-web-app-browser);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
opacity: 0.75;
|
||||
position: absolute;
|
||||
transform: rotate(-45deg);
|
||||
|
||||
&,
|
||||
&::before,
|
||||
&::after {
|
||||
background-color: var(--color-text-secondary);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.minimized {
|
||||
:global {
|
||||
.modal-dialog {
|
||||
cursor: grab !important;
|
||||
width: 300px;
|
||||
height: 2.5rem;
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tabs {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
flex-grow: 1;
|
||||
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
white-space: nowrap;
|
||||
|
||||
scrollbar-width: none;
|
||||
scrollbar-color: rgba(0, 0, 0, 0);
|
||||
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
|
||||
@include mixins.gradient-border-horizontal(0.5rem, calc(100% - 0.5rem));
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
&::-webkit-scrollbar-thumb {
|
||||
background-color: rgba(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
.tab-button-wrapper {
|
||||
display: flex;
|
||||
margin-left: -0.5rem;
|
||||
margin-right: -0.5rem;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
|
||||
transition: var(--color-transition) background-color, var(--color-transition) color;
|
||||
background-color: var(--active-tab-background);
|
||||
color: var(--color-header-text);
|
||||
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 100%;
|
||||
padding: 0.5rem;
|
||||
padding-left: 1rem;
|
||||
padding-right: 1rem;
|
||||
|
||||
font-size: 0.875rem;
|
||||
font-weight: 500;
|
||||
text-overflow: ellipsis;
|
||||
|
||||
border-top-right-radius: var(--border-radius-default);
|
||||
border-top-left-radius: var(--border-radius-default);
|
||||
|
||||
box-shadow: var(--modal-shadow);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
--more-button-opacity: 1;
|
||||
|
||||
.tab-right-mask {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.tab-close-button {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.tab-right-mask {
|
||||
@include mixins.gradient-border-left(2rem);
|
||||
|
||||
opacity: 0;
|
||||
transition: opacity 0.25s ease-in-out;
|
||||
|
||||
position: absolute;
|
||||
width: 4rem;
|
||||
height: 100%;
|
||||
right: 0;
|
||||
|
||||
background-color: var(--active-tab-background);
|
||||
border-top-right-radius: var(--border-radius-default);
|
||||
}
|
||||
|
||||
.tab-button-curve-path {
|
||||
fill: var(--active-tab-background);
|
||||
transition: var(--color-transition) fill;
|
||||
}
|
||||
|
||||
.tab-button-left-curve,
|
||||
.tab-button-right-curve {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
|
||||
transition: var(--color-transition) background-color, var(--color-transition) color;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.tab-button-right-curve {
|
||||
transform: scaleX(-1);
|
||||
}
|
||||
|
||||
.avatar-container {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-right: 0.5rem;
|
||||
}
|
||||
|
||||
.web-app-tab-more-menu {
|
||||
z-index: 1;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
.more-button {
|
||||
opacity: 0.75;
|
||||
color: var(--color-header-text) !important;
|
||||
transition: var(--color-transition) color;
|
||||
}
|
||||
|
||||
.tab-more-button {
|
||||
z-index: 2;
|
||||
padding: 0 !important;
|
||||
width: 1.5rem !important;
|
||||
height: 1.5rem !important;
|
||||
font-size: 0.75rem;
|
||||
opacity: var(--more-button-opacity);
|
||||
color: white !important;
|
||||
transition: opacity 0.25s ease-in-out;
|
||||
background-color: rgba(0, 0, 0, 0.45) !important;
|
||||
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
}
|
||||
|
||||
.window-state-button,
|
||||
.header-button {
|
||||
width: 1.75rem !important;
|
||||
height: 1.75rem !important;
|
||||
}
|
||||
|
||||
.tab-close-button {
|
||||
transition: opacity 0.25s ease-in-out;
|
||||
opacity: 0;
|
||||
position: absolute;
|
||||
right: 0.5rem;
|
||||
color: var(--color-header-text) !important;
|
||||
|
||||
width: 1.5rem !important;
|
||||
height: 1.5rem !important;
|
||||
}
|
||||
|
||||
.tab-close-icon {
|
||||
opacity: 0.75;
|
||||
font-size: 1rem !important;
|
||||
}
|
||||
|
||||
.state-icon,
|
||||
.icon {
|
||||
opacity: 0.75;
|
||||
}
|
||||
|
||||
.state-icon {
|
||||
font-size: 2rem !important;
|
||||
}
|
||||
|
||||
.tab-avatar {
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
margin-right: 0.5rem;
|
||||
margin-left: 0.5rem;
|
||||
}
|
||||
|
||||
.close-icon {
|
||||
opacity: 0.75;
|
||||
position: absolute;
|
||||
@ -93,6 +334,7 @@
|
||||
|
||||
.loading-spinner {
|
||||
position: absolute;
|
||||
pointer-events: none;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,84 @@
|
||||
.root {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.multi-tab {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.loading-spinner {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, -50%);
|
||||
|
||||
transition: opacity 0.25s ease-in-out;
|
||||
}
|
||||
|
||||
.hide {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.frame {
|
||||
transition: opacity 0.25s ease-in-out;
|
||||
opacity: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border: 0;
|
||||
z-index: 1;
|
||||
|
||||
&.with-button {
|
||||
height: calc(100% - 3.5rem);
|
||||
}
|
||||
|
||||
&.hide {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.hidden {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.main-button {
|
||||
position: absolute;
|
||||
bottom: 0;
|
||||
border-radius: 0;
|
||||
|
||||
z-index: 1;
|
||||
transform: translateY(100%);
|
||||
transition-property: background-color, color, transform;
|
||||
transition-duration: 0.25s;
|
||||
transition-timing-function: ease-in-out;
|
||||
|
||||
&.visible {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
&.hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
}
|
||||
|
||||
.main-button-spinner {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
}
|
||||
|
||||
.web-app-popup {
|
||||
:global(.modal-dialog) {
|
||||
max-width: min(30rem, 100%);
|
||||
}
|
||||
|
||||
&.without-title :global(.modal-content) {
|
||||
padding-top: 0;
|
||||
}
|
||||
|
||||
:global(.modal-content) {
|
||||
padding-left: 2rem;
|
||||
}
|
||||
}
|
||||
648
src/components/modals/webApp/WebAppModalTabContent.tsx
Normal file
648
src/components/modals/webApp/WebAppModalTabContent.tsx
Normal file
@ -0,0 +1,648 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useEffect,
|
||||
useRef, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiAttachBot, ApiChat, ApiUser } from '../../../api/types';
|
||||
import type { TabState, WebApp } from '../../../global/types';
|
||||
import type { ThemeKey } from '../../../types';
|
||||
import type { PopupOptions, WebAppInboundEvent, WebAppOutboundEvent } from '../../../types/webapp';
|
||||
|
||||
import { TME_LINK_PREFIX } from '../../../config';
|
||||
import { convertToApiChatType } from '../../../global/helpers';
|
||||
import { getWebAppKey } from '../../../global/helpers/bots';
|
||||
import {
|
||||
selectCurrentChat, selectTabState, selectTheme, selectUser,
|
||||
} from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import buildStyle from '../../../util/buildStyle';
|
||||
import { extractCurrentThemeParams, validateHexColor } from '../../../util/themeStyle';
|
||||
import { callApi } from '../../../api/gramjs';
|
||||
import { REM } from '../../common/helpers/mediaDimensions';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
import usePreviousDeprecated from '../../../hooks/usePreviousDeprecated';
|
||||
import useSyncEffect from '../../../hooks/useSyncEffect';
|
||||
import usePopupLimit from './hooks/usePopupLimit';
|
||||
import useWebAppFrame from './hooks/useWebAppFrame';
|
||||
|
||||
import Button from '../../ui/Button';
|
||||
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||
import Modal from '../../ui/Modal';
|
||||
import Spinner from '../../ui/Spinner';
|
||||
|
||||
import styles from './WebAppModalTabContent.module.scss';
|
||||
|
||||
type WebAppButton = {
|
||||
isVisible: boolean;
|
||||
isActive: boolean;
|
||||
text: string;
|
||||
color: string;
|
||||
textColor: string;
|
||||
isProgressVisible: boolean;
|
||||
};
|
||||
|
||||
export type OwnProps = {
|
||||
modal?: TabState['webApps'];
|
||||
webApp?: WebApp;
|
||||
registerSendEventCallback: (callback: (event: WebAppOutboundEvent) => void) => void;
|
||||
registerReloadFrameCallback: (callback: (url: string) => void) => void;
|
||||
isDragging?: boolean;
|
||||
frameSize?: { width: number; height: number };
|
||||
isMultiTabSupported? : boolean;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
chat?: ApiChat;
|
||||
bot?: ApiUser;
|
||||
attachBot?: ApiAttachBot;
|
||||
theme?: ThemeKey;
|
||||
isPaymentModalOpen?: boolean;
|
||||
paymentStatus?: TabState['payment']['status'];
|
||||
isMaximizedState: boolean;
|
||||
};
|
||||
|
||||
const NBSP = '\u00A0';
|
||||
|
||||
const MAIN_BUTTON_ANIMATION_TIME = 250;
|
||||
const ANIMATION_WAIT = 400;
|
||||
const POPUP_SEQUENTIAL_LIMIT = 3;
|
||||
const POPUP_RESET_DELAY = 2000; // 2s
|
||||
const SANDBOX_ATTRIBUTES = [
|
||||
'allow-scripts',
|
||||
'allow-same-origin',
|
||||
'allow-popups',
|
||||
'allow-forms',
|
||||
'allow-modals',
|
||||
'allow-storage-access-by-user-activation',
|
||||
].join(' ');
|
||||
|
||||
const DEFAULT_BUTTON_TEXT: Record<string, string> = {
|
||||
ok: 'OK',
|
||||
cancel: 'Cancel',
|
||||
close: 'Close',
|
||||
};
|
||||
|
||||
const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
|
||||
modal,
|
||||
webApp,
|
||||
bot,
|
||||
theme,
|
||||
isPaymentModalOpen,
|
||||
paymentStatus,
|
||||
registerSendEventCallback,
|
||||
registerReloadFrameCallback,
|
||||
isDragging,
|
||||
isMaximizedState,
|
||||
frameSize,
|
||||
isMultiTabSupported,
|
||||
}) => {
|
||||
const {
|
||||
closeActiveWebApp,
|
||||
sendWebViewData,
|
||||
toggleAttachBot,
|
||||
openTelegramLink,
|
||||
setWebAppPaymentSlug,
|
||||
switchBotInline,
|
||||
sharePhoneWithBot,
|
||||
updateWebApp,
|
||||
} = getActions();
|
||||
const [mainButton, setMainButton] = useState<WebAppButton | undefined>();
|
||||
|
||||
const [isLoaded, markLoaded, markUnloaded] = useFlag(false);
|
||||
|
||||
const [popupParameters, setPopupParameters] = useState<PopupOptions | undefined>();
|
||||
const [isRequestingPhone, setIsRequestingPhone] = useState(false);
|
||||
const [isRequestingWriteAccess, setIsRequestingWriteAccess] = useState(false);
|
||||
const {
|
||||
unlockPopupsAt, handlePopupOpened, handlePopupClosed,
|
||||
} = usePopupLimit(POPUP_SEQUENTIAL_LIMIT, POPUP_RESET_DELAY);
|
||||
|
||||
const activeWebApp = modal?.activeWebApp;
|
||||
const {
|
||||
url, buttonText, headerColor, serverHeaderColorKey, serverHeaderColor,
|
||||
} = webApp || {};
|
||||
const isCloseModalOpen = Boolean(webApp?.isCloseModalOpen);
|
||||
const isRemoveModalOpen = Boolean(webApp?.isRemoveModalOpen);
|
||||
|
||||
const webAppKey = webApp && getWebAppKey(webApp);
|
||||
const activeWebAppKey = activeWebApp && getWebAppKey(activeWebApp);
|
||||
|
||||
const isActive = (activeWebApp && webApp) && activeWebAppKey === webAppKey;
|
||||
|
||||
const updateCurrentWebApp = useLastCallback((updatedPartialWebApp: Partial<WebApp>) => {
|
||||
if (!webApp) return;
|
||||
const updatedWebApp = {
|
||||
...webApp,
|
||||
...updatedPartialWebApp,
|
||||
};
|
||||
webApp = updatedWebApp;
|
||||
updateWebApp({ webApp: updatedWebApp });
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const themeParams = extractCurrentThemeParams();
|
||||
updateCurrentWebApp({ headerColor: themeParams.bg_color, backgroundColor: themeParams.bg_color });
|
||||
}, []);
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const frameRef = useRef<HTMLIFrameElement>(null);
|
||||
|
||||
const lang = useOldLang();
|
||||
const isOpen = modal?.isModalOpen || false;
|
||||
const isSimple = Boolean(buttonText);
|
||||
|
||||
const {
|
||||
reloadFrame, sendEvent, sendViewport, sendTheme,
|
||||
} = useWebAppFrame(frameRef, isOpen, isSimple, handleEvent, webApp, markLoaded);
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive) registerSendEventCallback(sendEvent);
|
||||
}, [sendEvent, registerSendEventCallback, isActive]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isActive) registerReloadFrameCallback(reloadFrame);
|
||||
}, [reloadFrame, registerReloadFrameCallback, isActive]);
|
||||
|
||||
const shouldShowMainButton = mainButton?.isVisible && mainButton.text.trim().length > 0;
|
||||
|
||||
const handleHideCloseModal = useLastCallback(() => {
|
||||
updateCurrentWebApp({ isCloseModalOpen: false });
|
||||
});
|
||||
const handleConfirmCloseModal = useLastCallback(() => {
|
||||
updateCurrentWebApp({ shouldConfirmClosing: false, isCloseModalOpen: false });
|
||||
setTimeout(() => {
|
||||
closeActiveWebApp();
|
||||
}, ANIMATION_WAIT);
|
||||
});
|
||||
|
||||
const handleHideRemoveModal = useLastCallback(() => {
|
||||
updateCurrentWebApp({ isRemoveModalOpen: false });
|
||||
});
|
||||
|
||||
const handleMainButtonClick = useLastCallback(() => {
|
||||
sendEvent({
|
||||
eventType: 'main_button_pressed',
|
||||
});
|
||||
});
|
||||
|
||||
const handleAppPopupClose = useLastCallback((buttonId?: string) => {
|
||||
setPopupParameters(undefined);
|
||||
handlePopupClosed();
|
||||
sendEvent({
|
||||
eventType: 'popup_closed',
|
||||
eventData: {
|
||||
button_id: buttonId,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const handleAppPopupModalClose = useLastCallback(() => {
|
||||
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
|
||||
useSyncEffect(() => {
|
||||
setTimeout(() => {
|
||||
sendThemeCallback();
|
||||
}, ANIMATION_WAIT);
|
||||
}, [theme]);
|
||||
|
||||
// Notify view that height changed
|
||||
useSyncEffect(() => {
|
||||
setTimeout(() => {
|
||||
sendViewport();
|
||||
}, ANIMATION_WAIT);
|
||||
}, [mainButton?.isVisible, sendViewport]);
|
||||
|
||||
useSyncEffect(([prevIsPaymentModalOpen]) => {
|
||||
if (isPaymentModalOpen === prevIsPaymentModalOpen) return;
|
||||
if (webApp?.slug && !isPaymentModalOpen && paymentStatus) {
|
||||
sendEvent({
|
||||
eventType: 'invoice_closed',
|
||||
eventData: {
|
||||
slug: webApp.slug,
|
||||
status: paymentStatus,
|
||||
},
|
||||
});
|
||||
setWebAppPaymentSlug({
|
||||
slug: undefined,
|
||||
});
|
||||
}
|
||||
}, [isPaymentModalOpen, paymentStatus, sendEvent, webApp?.slug]);
|
||||
|
||||
const handleRemoveAttachBot = useLastCallback(() => {
|
||||
toggleAttachBot({
|
||||
botId: bot!.id,
|
||||
isEnabled: false,
|
||||
});
|
||||
closeActiveWebApp();
|
||||
});
|
||||
|
||||
const handleRejectPhone = useLastCallback(() => {
|
||||
setIsRequestingPhone(false);
|
||||
handlePopupClosed();
|
||||
sendEvent({
|
||||
eventType: 'phone_requested',
|
||||
eventData: {
|
||||
status: 'cancelled',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const handleAcceptPhone = useLastCallback(() => {
|
||||
sharePhoneWithBot({ botId: bot!.id });
|
||||
setIsRequestingPhone(false);
|
||||
handlePopupClosed();
|
||||
sendEvent({
|
||||
eventType: 'phone_requested',
|
||||
eventData: {
|
||||
status: 'sent',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
const handleRejectWriteAccess = useLastCallback(() => {
|
||||
sendEvent({
|
||||
eventType: 'write_access_requested',
|
||||
eventData: {
|
||||
status: 'cancelled',
|
||||
},
|
||||
});
|
||||
setIsRequestingWriteAccess(false);
|
||||
handlePopupClosed();
|
||||
});
|
||||
|
||||
const handleAcceptWriteAccess = useLastCallback(async () => {
|
||||
const result = await callApi('allowBotSendMessages', { bot: bot! });
|
||||
if (!result) {
|
||||
handleRejectWriteAccess();
|
||||
return;
|
||||
}
|
||||
|
||||
sendEvent({
|
||||
eventType: 'write_access_requested',
|
||||
eventData: {
|
||||
status: 'allowed',
|
||||
},
|
||||
});
|
||||
setIsRequestingWriteAccess(false);
|
||||
handlePopupClosed();
|
||||
});
|
||||
|
||||
async function handleRequestWriteAccess() {
|
||||
const canWrite = await callApi('fetchBotCanSendMessage', {
|
||||
bot: bot!,
|
||||
});
|
||||
|
||||
if (canWrite) {
|
||||
sendEvent({
|
||||
eventType: 'write_access_requested',
|
||||
eventData: {
|
||||
status: 'allowed',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setIsRequestingWriteAccess(!canWrite);
|
||||
}
|
||||
|
||||
async function handleInvokeCustomMethod(requestId: string, method: string, parameters: string) {
|
||||
const result = await callApi('invokeWebViewCustomMethod', {
|
||||
bot: bot!,
|
||||
customMethod: method,
|
||||
parameters,
|
||||
});
|
||||
|
||||
sendEvent({
|
||||
eventType: 'custom_method_invoked',
|
||||
eventData: {
|
||||
req_id: requestId,
|
||||
...result,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
setPopupParameters(undefined);
|
||||
setIsRequestingPhone(false);
|
||||
setIsRequestingWriteAccess(false);
|
||||
setMainButton(undefined);
|
||||
updateCurrentWebApp({
|
||||
isSettingsButtonVisible: false,
|
||||
shouldConfirmClosing: false,
|
||||
isBackButtonVisible: false,
|
||||
isCloseModalOpen: false,
|
||||
isRemoveModalOpen: false,
|
||||
});
|
||||
markUnloaded();
|
||||
}
|
||||
}, [isOpen]);
|
||||
|
||||
function handleEvent(event: WebAppInboundEvent) {
|
||||
const { eventType, eventData } = event;
|
||||
if (eventType === 'web_app_open_tg_link' && !isPaymentModalOpen) {
|
||||
const linkUrl = TME_LINK_PREFIX + eventData.path_full;
|
||||
openTelegramLink({ url: linkUrl });
|
||||
closeActiveWebApp();
|
||||
}
|
||||
|
||||
if (eventType === 'web_app_setup_back_button') {
|
||||
updateCurrentWebApp({ isBackButtonVisible: eventData.is_visible });
|
||||
}
|
||||
|
||||
if (eventType === 'web_app_setup_settings_button') {
|
||||
updateCurrentWebApp({ isSettingsButtonVisible: eventData.is_visible });
|
||||
}
|
||||
|
||||
if (eventType === 'web_app_set_background_color') {
|
||||
const themeParams = extractCurrentThemeParams();
|
||||
const color = validateHexColor(eventData.color) ? eventData.color : themeParams.bg_color;
|
||||
updateCurrentWebApp({ backgroundColor: color });
|
||||
}
|
||||
|
||||
if (eventType === 'web_app_set_header_color') {
|
||||
calculateHeaderColor(eventData.color_key, eventData.color);
|
||||
}
|
||||
|
||||
if (eventType === 'web_app_data_send') {
|
||||
closeActiveWebApp();
|
||||
sendWebViewData({
|
||||
bot: bot!,
|
||||
buttonText: buttonText!,
|
||||
data: eventData.data,
|
||||
});
|
||||
}
|
||||
|
||||
if (eventType === 'web_app_setup_main_button') {
|
||||
const themeParams = extractCurrentThemeParams();
|
||||
const color = validateHexColor(eventData.color) ? eventData.color : themeParams.button_color;
|
||||
const textColor = validateHexColor(eventData.text_color) ? eventData.text_color : themeParams.text_color;
|
||||
setMainButton({
|
||||
isVisible: eventData.is_visible && Boolean(eventData.text?.trim().length),
|
||||
isActive: eventData.is_active,
|
||||
text: eventData.text || '',
|
||||
color,
|
||||
textColor,
|
||||
isProgressVisible: eventData.is_progress_visible,
|
||||
});
|
||||
}
|
||||
|
||||
if (eventType === 'web_app_setup_closing_behavior') {
|
||||
updateCurrentWebApp({ shouldConfirmClosing: true });
|
||||
}
|
||||
|
||||
if (eventType === 'web_app_open_popup') {
|
||||
if (popupParameters || !eventData.message.trim().length || !eventData.buttons?.length
|
||||
|| eventData.buttons.length > 3 || isRequestingPhone || isRequestingWriteAccess
|
||||
|| unlockPopupsAt > Date.now()) {
|
||||
handleAppPopupClose(undefined);
|
||||
return;
|
||||
}
|
||||
|
||||
setPopupParameters(eventData);
|
||||
handlePopupOpened();
|
||||
}
|
||||
|
||||
if (eventType === 'web_app_switch_inline_query') {
|
||||
const filter = eventData.chat_types?.map(convertToApiChatType).filter(Boolean);
|
||||
const isSamePeer = !filter?.length;
|
||||
|
||||
switchBotInline({
|
||||
botId: bot!.id,
|
||||
query: eventData.query,
|
||||
filter,
|
||||
isSamePeer,
|
||||
});
|
||||
|
||||
closeActiveWebApp();
|
||||
}
|
||||
|
||||
if (eventType === 'web_app_request_phone') {
|
||||
if (popupParameters || isRequestingWriteAccess || unlockPopupsAt > Date.now()) {
|
||||
handleRejectPhone();
|
||||
return;
|
||||
}
|
||||
|
||||
setIsRequestingPhone(true);
|
||||
handlePopupOpened();
|
||||
}
|
||||
|
||||
if (eventType === 'web_app_request_write_access') {
|
||||
if (popupParameters || isRequestingPhone || unlockPopupsAt > Date.now()) {
|
||||
handleRejectWriteAccess();
|
||||
return;
|
||||
}
|
||||
|
||||
handleRequestWriteAccess();
|
||||
handlePopupOpened();
|
||||
}
|
||||
|
||||
if (eventType === 'web_app_invoke_custom_method') {
|
||||
const { method, params, req_id: requestId } = eventData;
|
||||
handleInvokeCustomMethod(requestId, method, JSON.stringify(params));
|
||||
}
|
||||
}
|
||||
|
||||
const prevMainButtonColor = usePreviousDeprecated(mainButton?.color, true);
|
||||
const prevMainButtonTextColor = usePreviousDeprecated(mainButton?.textColor, true);
|
||||
const prevMainButtonIsActive = usePreviousDeprecated(mainButton && Boolean(mainButton.isActive), true);
|
||||
const prevMainButtonText = usePreviousDeprecated(mainButton?.text, true);
|
||||
|
||||
const mainButtonCurrentColor = mainButton?.color || prevMainButtonColor;
|
||||
const mainButtonCurrentTextColor = mainButton?.textColor || prevMainButtonTextColor;
|
||||
const mainButtonCurrentIsActive = mainButton?.isActive !== undefined ? mainButton.isActive : prevMainButtonIsActive;
|
||||
const mainButtonCurrentText = mainButton?.text || prevMainButtonText;
|
||||
|
||||
const [shouldDecreaseWebFrameSize, setShouldDecreaseWebFrameSize] = useState(false);
|
||||
const [shouldHideButton, setShouldHideButton] = useState(true);
|
||||
|
||||
const buttonChangeTimeout = useRef<ReturnType<typeof setTimeout>>();
|
||||
|
||||
useEffect(() => {
|
||||
if (buttonChangeTimeout.current) clearTimeout(buttonChangeTimeout.current);
|
||||
if (!shouldShowMainButton) {
|
||||
setShouldDecreaseWebFrameSize(false);
|
||||
buttonChangeTimeout.current = setTimeout(() => {
|
||||
setShouldHideButton(true);
|
||||
}, MAIN_BUTTON_ANIMATION_TIME);
|
||||
} else {
|
||||
setShouldHideButton(false);
|
||||
buttonChangeTimeout.current = setTimeout(() => {
|
||||
setShouldDecreaseWebFrameSize(true);
|
||||
}, MAIN_BUTTON_ANIMATION_TIME);
|
||||
}
|
||||
}, [setShouldDecreaseWebFrameSize, shouldShowMainButton]);
|
||||
|
||||
const frameWidth = frameSize?.width || 0;
|
||||
let frameHeight = frameSize?.height || 0;
|
||||
if (shouldDecreaseWebFrameSize) { frameHeight -= 3.5 * REM; }
|
||||
const frameStyle = buildStyle(
|
||||
`left: ${0}px;`,
|
||||
`top: ${0}px;`,
|
||||
`width: ${frameWidth}px;`,
|
||||
`height: ${frameHeight}px;`,
|
||||
isDragging ? 'pointer-events: none;' : '',
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={buildClassName(
|
||||
styles.root,
|
||||
!isActive && styles.hidden,
|
||||
isMultiTabSupported && styles.multiTab,
|
||||
)}
|
||||
>
|
||||
{isMaximizedState && <Spinner className={buildClassName(styles.loadingSpinner, isLoaded && styles.hide)} />}
|
||||
<iframe
|
||||
className={buildClassName(
|
||||
styles.frame,
|
||||
shouldDecreaseWebFrameSize && styles.withButton,
|
||||
!isLoaded && styles.hide,
|
||||
)}
|
||||
style={frameSize ? frameStyle : undefined}
|
||||
src={url}
|
||||
title={`${bot?.firstName} Web App`}
|
||||
sandbox={SANDBOX_ATTRIBUTES}
|
||||
allow="camera; microphone; geolocation;"
|
||||
allowFullScreen
|
||||
ref={frameRef}
|
||||
/>
|
||||
{isMaximizedState && (
|
||||
<Button
|
||||
className={buildClassName(
|
||||
styles.mainButton,
|
||||
shouldShowMainButton && styles.visible,
|
||||
shouldHideButton && styles.hidden,
|
||||
)}
|
||||
style={`background-color: ${mainButtonCurrentColor}; color: ${mainButtonCurrentTextColor}`}
|
||||
disabled={!mainButtonCurrentIsActive}
|
||||
onClick={handleMainButtonClick}
|
||||
>
|
||||
{mainButtonCurrentText}
|
||||
{mainButton?.isProgressVisible && <Spinner className={styles.mainButtonSpinner} color="white" />}
|
||||
</Button>
|
||||
) }
|
||||
<ConfirmDialog
|
||||
isOpen={isRequestingPhone}
|
||||
onClose={handleRejectPhone}
|
||||
title={lang('ShareYouPhoneNumberTitle')}
|
||||
text={lang('AreYouSureShareMyContactInfoBot')}
|
||||
confirmHandler={handleAcceptPhone}
|
||||
confirmLabel={lang('ContactShare')}
|
||||
/>
|
||||
<ConfirmDialog
|
||||
isOpen={isRequestingWriteAccess}
|
||||
onClose={handleRejectWriteAccess}
|
||||
title={lang('lng_bot_allow_write_title')}
|
||||
text={lang('lng_bot_allow_write')}
|
||||
confirmHandler={handleAcceptWriteAccess}
|
||||
confirmLabel={lang('lng_bot_allow_write_confirm')}
|
||||
/>
|
||||
{popupParameters && (
|
||||
<Modal
|
||||
isOpen={Boolean(popupParameters)}
|
||||
title={popupParameters.title || NBSP}
|
||||
onClose={handleAppPopupModalClose}
|
||||
hasCloseButton
|
||||
className={
|
||||
buildClassName(styles.webAppPopup, !popupParameters.title?.trim().length && styles.withoutTitle)
|
||||
}
|
||||
>
|
||||
{popupParameters.message}
|
||||
<div className="dialog-buttons mt-2">
|
||||
{popupParameters.buttons.map((button) => (
|
||||
<Button
|
||||
key={button.id || button.type}
|
||||
className="confirm-dialog-button"
|
||||
color={button.type === 'destructive' ? 'danger' : 'primary'}
|
||||
isText
|
||||
size="smaller"
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={() => handleAppPopupClose(button.id)}
|
||||
>
|
||||
{button.text || lang(DEFAULT_BUTTON_TEXT[button.type])}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
)}
|
||||
|
||||
<ConfirmDialog
|
||||
isOpen={isCloseModalOpen}
|
||||
onClose={handleHideCloseModal}
|
||||
title={lang('lng_bot_close_warning_title')}
|
||||
text={lang('lng_bot_close_warning')}
|
||||
confirmHandler={handleConfirmCloseModal}
|
||||
confirmIsDestructive
|
||||
confirmLabel={lang('lng_bot_close_warning_sure')}
|
||||
/>
|
||||
<ConfirmDialog
|
||||
isOpen={isRemoveModalOpen}
|
||||
onClose={handleHideRemoveModal}
|
||||
title={lang('BotRemoveFromMenuTitle')}
|
||||
textParts={renderText(lang('BotRemoveFromMenu', bot?.firstName), ['simple_markdown'])}
|
||||
confirmHandler={handleRemoveAttachBot}
|
||||
confirmIsDestructive
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { modal }): StateProps => {
|
||||
const { botId: activeBotId } = modal?.activeWebApp || {};
|
||||
const isMaximizedState = modal?.modalState === 'maximized';
|
||||
|
||||
const attachBot = activeBotId ? global.attachMenu.bots[activeBotId] : undefined;
|
||||
const bot = activeBotId ? selectUser(global, activeBotId) : undefined;
|
||||
const chat = selectCurrentChat(global);
|
||||
const theme = selectTheme(global);
|
||||
const { isPaymentModalOpen, status } = selectTabState(global).payment;
|
||||
const { isStarPaymentModalOpen } = selectTabState(global);
|
||||
|
||||
return {
|
||||
attachBot,
|
||||
bot,
|
||||
chat,
|
||||
theme,
|
||||
isPaymentModalOpen: isPaymentModalOpen || isStarPaymentModalOpen,
|
||||
paymentStatus: status,
|
||||
isMaximizedState,
|
||||
};
|
||||
},
|
||||
)(WebAppModalTabContent));
|
||||
@ -1,6 +1,7 @@
|
||||
import { useCallback, useEffect, useRef } from '../../../../lib/teact/teact';
|
||||
import { getActions } from '../../../../global';
|
||||
|
||||
import type { WebApp } from '../../../../global/types';
|
||||
import type { WebAppInboundEvent, WebAppOutboundEvent } from '../../../../types/webapp';
|
||||
|
||||
import { extractCurrentThemeParams } from '../../../../util/themeStyle';
|
||||
@ -35,6 +36,7 @@ const useWebAppFrame = (
|
||||
isOpen: boolean,
|
||||
isSimpleView: boolean,
|
||||
onEvent: (event: WebAppInboundEvent) => void,
|
||||
webApp?: WebApp,
|
||||
onLoad?: () => void,
|
||||
) => {
|
||||
const {
|
||||
@ -128,6 +130,12 @@ const useWebAppFrame = (
|
||||
if (ignoreEventsRef.current) {
|
||||
return;
|
||||
}
|
||||
const contentWindow = ref.current?.contentWindow;
|
||||
const sourceWindow = event.source as Window;
|
||||
|
||||
if (contentWindow !== sourceWindow) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const data = JSON.parse(event.data) as WebAppInboundEvent;
|
||||
@ -138,7 +146,7 @@ const useWebAppFrame = (
|
||||
}
|
||||
|
||||
if (eventType === 'web_app_close') {
|
||||
closeWebApp();
|
||||
if (webApp) closeWebApp({ webApp, skipClosingConfirmation: true });
|
||||
}
|
||||
|
||||
if (eventType === 'web_app_request_viewport') {
|
||||
@ -175,9 +183,9 @@ const useWebAppFrame = (
|
||||
},
|
||||
});
|
||||
|
||||
showNotification({
|
||||
message: 'Clipboard access is not supported in this client yet',
|
||||
});
|
||||
// showNotification({
|
||||
// message: 'Clipboard access is not supported in this client yet',
|
||||
// });
|
||||
}
|
||||
|
||||
if (eventType === 'web_app_open_scan_qr_popup') {
|
||||
@ -214,7 +222,10 @@ const useWebAppFrame = (
|
||||
} catch (err) {
|
||||
// Ignore other messages
|
||||
}
|
||||
}, [isSimpleView, sendEvent, onEvent, sendCustomStyle, sendTheme, sendViewport, onLoad, windowSize.isResizing]);
|
||||
}, [
|
||||
isSimpleView, sendEvent, onEvent, sendCustomStyle, webApp,
|
||||
sendTheme, sendViewport, onLoad, windowSize.isResizing, ref,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const { width, height, isResizing } = windowSize;
|
||||
@ -227,7 +238,7 @@ const useWebAppFrame = (
|
||||
useEffect(() => {
|
||||
window.addEventListener('message', handleMessage);
|
||||
return () => window.removeEventListener('message', handleMessage);
|
||||
}, [handleMessage]);
|
||||
}, [handleMessage, ref]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && ref.current?.contentWindow) {
|
||||
|
||||
@ -37,6 +37,7 @@ export type OwnProps = {
|
||||
noBackdropClose?: boolean;
|
||||
children: React.ReactNode;
|
||||
style?: string;
|
||||
dialogStyle?: string;
|
||||
dialogRef?: React.RefObject<HTMLDivElement>;
|
||||
isLowStackPriority?: boolean;
|
||||
onClose: () => void;
|
||||
@ -59,6 +60,7 @@ const Modal: FC<OwnProps> = ({
|
||||
noBackdropClose,
|
||||
children,
|
||||
style,
|
||||
dialogStyle,
|
||||
isLowStackPriority,
|
||||
onClose,
|
||||
onCloseAnimationEnd,
|
||||
@ -167,7 +169,7 @@ const Modal: FC<OwnProps> = ({
|
||||
>
|
||||
<div className="modal-container">
|
||||
<div className="modal-backdrop" onClick={!noBackdropClose ? onClose : undefined} />
|
||||
<div className="modal-dialog" ref={dialogRef}>
|
||||
<div className="modal-dialog" ref={dialogRef} style={dialogStyle}>
|
||||
{renderHeader()}
|
||||
<div className={buildClassName('modal-content custom-scroll', contentClassName)} style={style}>
|
||||
{children}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
import type { InlineBotSettings } from '../../../types';
|
||||
import type { RequiredGlobalActions } from '../../index';
|
||||
import type { ActionReturnType, GlobalState, TabArgs } from '../../types';
|
||||
import type {
|
||||
ActionReturnType, GlobalState, TabArgs, WebApp,
|
||||
} from '../../types';
|
||||
import {
|
||||
type ApiChat, type ApiChatType, type ApiContact, type ApiInputMessageReplyInfo, type ApiPeer, type ApiUrlAuthResult,
|
||||
MAIN_THREAD_ID,
|
||||
@ -16,13 +18,22 @@ import { debounce } from '../../../util/schedulers';
|
||||
import { getServerTime } from '../../../util/serverTime';
|
||||
import { extractCurrentThemeParams } from '../../../util/themeStyle';
|
||||
import { callApi } from '../../../api/gramjs';
|
||||
import {
|
||||
getWebAppKey,
|
||||
} from '../../helpers/bots';
|
||||
import {
|
||||
addActionHandler, getGlobal, setGlobal,
|
||||
} from '../../index';
|
||||
import {
|
||||
removeBlockedUser, updateManagementProgress, updateUser, updateUserFullInfo,
|
||||
} from '../../reducers';
|
||||
import { replaceInlineBotSettings, replaceInlineBotsIsLoading } from '../../reducers/bots';
|
||||
import {
|
||||
activateWebAppIfOpen,
|
||||
addWebAppToOpenList, clearOpenedWebApps, hasOpenedWebApps,
|
||||
removeActiveWebAppFromOpenList, removeWebAppFromOpenList,
|
||||
replaceInlineBotSettings, replaceInlineBotsIsLoading,
|
||||
replaceIsWebAppModalOpen, replaceWebAppModalState, updateWebApp,
|
||||
} from '../../reducers/bots';
|
||||
import { updateTabState } from '../../reducers/tabs';
|
||||
import {
|
||||
selectBot,
|
||||
@ -482,6 +493,8 @@ addActionHandler('requestSimpleWebView', async (global, actions, payload): Promi
|
||||
tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
|
||||
if (checkIfOpenOrActivate(global, botId, tabId, url)) return;
|
||||
|
||||
const bot = selectUser(global, botId);
|
||||
if (!bot) return;
|
||||
|
||||
@ -513,13 +526,13 @@ addActionHandler('requestSimpleWebView', async (global, actions, payload): Promi
|
||||
}
|
||||
|
||||
global = getGlobal();
|
||||
global = updateTabState(global, {
|
||||
webApp: {
|
||||
url: webViewUrl,
|
||||
botId,
|
||||
buttonText,
|
||||
},
|
||||
}, tabId);
|
||||
const newActiveApp: WebApp = {
|
||||
requestUrl: url,
|
||||
url: webViewUrl,
|
||||
botId,
|
||||
buttonText,
|
||||
};
|
||||
global = addWebAppToOpenList(global, newActiveApp, true, true, tabId);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
@ -529,6 +542,8 @@ addActionHandler('requestWebView', async (global, actions, payload): Promise<voi
|
||||
tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
|
||||
if (checkIfOpenOrActivate(global, botId, tabId, url)) return;
|
||||
|
||||
const bot = selectUser(global, botId);
|
||||
if (!bot) return;
|
||||
const peer = selectPeer(global, peerId);
|
||||
@ -574,15 +589,16 @@ addActionHandler('requestWebView', async (global, actions, payload): Promise<voi
|
||||
const { url: webViewUrl, queryId } = result;
|
||||
|
||||
global = getGlobal();
|
||||
global = updateTabState(global, {
|
||||
webApp: {
|
||||
url: webViewUrl,
|
||||
botId,
|
||||
queryId,
|
||||
replyInfo,
|
||||
buttonText,
|
||||
},
|
||||
}, tabId);
|
||||
const newActiveApp: WebApp = {
|
||||
requestUrl: url,
|
||||
url: webViewUrl,
|
||||
botId,
|
||||
peerId,
|
||||
queryId,
|
||||
replyInfo,
|
||||
buttonText,
|
||||
};
|
||||
global = addWebAppToOpenList(global, newActiveApp, true, true, tabId);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
@ -592,6 +608,8 @@ addActionHandler('requestMainWebView', async (global, actions, payload): Promise
|
||||
tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
|
||||
if (checkIfOpenOrActivate(global, botId, tabId)) return;
|
||||
|
||||
const bot = selectUser(global, botId);
|
||||
if (!bot) return;
|
||||
const peer = selectPeer(global, peerId);
|
||||
@ -629,14 +647,14 @@ addActionHandler('requestMainWebView', async (global, actions, payload): Promise
|
||||
const { url: webViewUrl, queryId } = result;
|
||||
|
||||
global = getGlobal();
|
||||
global = updateTabState(global, {
|
||||
webApp: {
|
||||
url: webViewUrl,
|
||||
botId,
|
||||
queryId,
|
||||
buttonText: '',
|
||||
},
|
||||
}, tabId);
|
||||
const newActiveApp: WebApp = {
|
||||
url: webViewUrl,
|
||||
botId,
|
||||
peerId,
|
||||
queryId,
|
||||
buttonText: '',
|
||||
};
|
||||
global = addWebAppToOpenList(global, newActiveApp, true, true, tabId);
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
@ -668,12 +686,26 @@ addActionHandler('loadPreviewMedias', async (global, actions, payload): Promise<
|
||||
}
|
||||
});
|
||||
|
||||
addActionHandler('openWebAppTab', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
webApp, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
|
||||
if (webApp) {
|
||||
global = getGlobal();
|
||||
global = addWebAppToOpenList(global, webApp, true, true, tabId);
|
||||
setGlobal(global);
|
||||
}
|
||||
});
|
||||
|
||||
addActionHandler('requestAppWebView', async (global, actions, payload): Promise<void> => {
|
||||
const {
|
||||
botId, appName, startApp, theme, isWriteAllowed, isFromConfirm, shouldSkipBotTrustRequest,
|
||||
tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
|
||||
if (checkIfOpenOrActivate(global, botId, tabId, appName)) return;
|
||||
|
||||
const bot = selectUser(global, botId);
|
||||
if (!bot) return;
|
||||
|
||||
@ -751,13 +783,19 @@ addActionHandler('requestAppWebView', async (global, actions, payload): Promise<
|
||||
|
||||
if (!url) return;
|
||||
|
||||
global = updateTabState(global, {
|
||||
webApp: {
|
||||
url,
|
||||
botId,
|
||||
buttonText: '',
|
||||
},
|
||||
}, tabId);
|
||||
global = getGlobal();
|
||||
|
||||
const peerId = (peer ? peer.id : bot!.id);
|
||||
|
||||
const newActiveApp: WebApp = {
|
||||
url,
|
||||
peerId,
|
||||
botId,
|
||||
appName,
|
||||
buttonText: '',
|
||||
};
|
||||
global = addWebAppToOpenList(global, newActiveApp, true, true, tabId);
|
||||
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
@ -783,7 +821,7 @@ addActionHandler('prolongWebView', async (global, actions, payload): Promise<voi
|
||||
});
|
||||
|
||||
if (!result) {
|
||||
actions.closeWebApp({ tabId });
|
||||
actions.closeActiveWebApp({ tabId });
|
||||
}
|
||||
});
|
||||
|
||||
@ -799,25 +837,60 @@ addActionHandler('sendWebViewData', (global, actions, payload): ActionReturnType
|
||||
});
|
||||
});
|
||||
|
||||
addActionHandler('closeWebApp', (global, actions, payload): ActionReturnType => {
|
||||
addActionHandler('updateWebApp', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
webApp, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
return updateWebApp(global, webApp, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('closeActiveWebApp', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
|
||||
return updateTabState(global, {
|
||||
webApp: undefined,
|
||||
}, tabId);
|
||||
global = removeActiveWebAppFromOpenList(global, tabId);
|
||||
if (!hasOpenedWebApps(global, tabId)) return replaceIsWebAppModalOpen(global, false, tabId);
|
||||
|
||||
return global;
|
||||
});
|
||||
|
||||
addActionHandler('closeWebApp', (global, actions, payload): ActionReturnType => {
|
||||
const { webApp, skipClosingConfirmation, tabId = getCurrentTabId() } = payload || {};
|
||||
|
||||
global = removeWebAppFromOpenList(global, webApp, skipClosingConfirmation, tabId);
|
||||
if (!hasOpenedWebApps(global, tabId)) return replaceIsWebAppModalOpen(global, false, tabId);
|
||||
|
||||
return global;
|
||||
});
|
||||
|
||||
addActionHandler('closeWebAppModal', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
|
||||
global = clearOpenedWebApps(global, tabId);
|
||||
if (!hasOpenedWebApps(global, tabId)) return replaceIsWebAppModalOpen(global, false, tabId);
|
||||
|
||||
return global;
|
||||
});
|
||||
|
||||
addActionHandler('changeWebAppModalState', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload || {};
|
||||
const tabState = selectTabState(global, tabId);
|
||||
|
||||
const newModalState = tabState.webApps.modalState === 'maximized' ? 'minimized' : 'maximized';
|
||||
return replaceWebAppModalState(global, newModalState, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('setWebAppPaymentSlug', (global, actions, payload): ActionReturnType => {
|
||||
const { tabId = getCurrentTabId() } = payload;
|
||||
const tabState = selectTabState(global, tabId);
|
||||
if (!tabState.webApp?.url) return undefined;
|
||||
const activeWebApp = tabState.webApps.activeWebApp;
|
||||
if (!activeWebApp?.url) return undefined;
|
||||
|
||||
return updateTabState(global, {
|
||||
webApp: {
|
||||
...tabState.webApp,
|
||||
slug: payload.slug,
|
||||
},
|
||||
}, tabId);
|
||||
const updatedApp = {
|
||||
...activeWebApp,
|
||||
slug: payload.slug,
|
||||
};
|
||||
|
||||
return updateWebApp(global, updatedApp, tabId);
|
||||
});
|
||||
|
||||
addActionHandler('cancelBotTrustRequest', (global, actions, payload): ActionReturnType => {
|
||||
@ -875,6 +948,31 @@ addActionHandler('toggleAttachBot', async (global, actions, payload): Promise<vo
|
||||
await callApi('toggleAttachBot', { bot, isWriteAllowed, isEnabled });
|
||||
});
|
||||
|
||||
export function isWepAppOpened<T extends GlobalState>(
|
||||
global: T, webApp: Partial<WebApp>, tabId: number,
|
||||
) {
|
||||
const currentTabState = selectTabState(global, tabId);
|
||||
const openedWebApps = currentTabState.webApps.openedWebApps;
|
||||
const key = getWebAppKey(webApp);
|
||||
if (!key) return false;
|
||||
return openedWebApps[key];
|
||||
}
|
||||
|
||||
export function checkIfOpenOrActivate<T extends GlobalState>(
|
||||
global: T, botId: string, tabId: number, requestUrl?: string, webAppName?: string,
|
||||
) {
|
||||
const webAppForCheck = { botId, requestUrl, webAppName };
|
||||
if (isWepAppOpened(global, webAppForCheck, tabId)) {
|
||||
const key = getWebAppKey(webAppForCheck);
|
||||
if (key) {
|
||||
global = activateWebAppIfOpen(global, key, tabId);
|
||||
setGlobal(global);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
async function loadAttachBots<T extends GlobalState>(global: T, hash?: string) {
|
||||
const result = await callApi('loadAttachBots', { hash });
|
||||
if (!result) {
|
||||
|
||||
@ -127,9 +127,9 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
|
||||
case 'updateWebViewResultSent':
|
||||
Object.values(global.byTabId).forEach((tabState) => {
|
||||
if (tabState.webApp?.queryId === update.queryId) {
|
||||
if (tabState.webApps.activeWebApp?.queryId === update.queryId) {
|
||||
actions.resetDraftReplyInfo({ tabId: tabState.id });
|
||||
actions.closeWebApp({ tabId: tabState.id });
|
||||
actions.closeActiveWebApp({ tabId: tabState.id });
|
||||
}
|
||||
});
|
||||
break;
|
||||
|
||||
17
src/global/actions/ui/bots.ts
Normal file
17
src/global/actions/ui/bots.ts
Normal file
@ -0,0 +1,17 @@
|
||||
import type { ActionReturnType } from '../../types';
|
||||
|
||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
||||
import { addWebAppToOpenList } from '../../reducers/bots';
|
||||
|
||||
addActionHandler('openWebAppTab', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
webApp, tabId = getCurrentTabId(),
|
||||
} = payload;
|
||||
|
||||
if (!webApp) return;
|
||||
|
||||
global = getGlobal();
|
||||
global = addWebAppToOpenList(global, webApp, true, true, tabId);
|
||||
setGlobal(global);
|
||||
});
|
||||
@ -1,4 +1,7 @@
|
||||
import type { ApiChatType, ApiPhoto } from '../../api/types';
|
||||
import type {
|
||||
WebApp,
|
||||
} from '../types';
|
||||
|
||||
export function getBotCoverMediaHash(photo: ApiPhoto) {
|
||||
return `photo${photo.id}?size=x`;
|
||||
@ -11,3 +14,9 @@ export function convertToApiChatType(type: string): ApiChatType | undefined {
|
||||
if (type === 'bots') return 'bots';
|
||||
return undefined;
|
||||
}
|
||||
|
||||
export function getWebAppKey(webApp: Partial<WebApp>) {
|
||||
if (webApp.requestUrl) return webApp.requestUrl;
|
||||
if (webApp.appName) return `${webApp.botId}?appName=${webApp.appName}`;
|
||||
return webApp.botId;
|
||||
}
|
||||
|
||||
@ -325,6 +325,14 @@ export const INITIAL_TAB_STATE: TabState = {
|
||||
byUsername: {},
|
||||
},
|
||||
|
||||
webApps: {
|
||||
openedWebApps: {},
|
||||
openedOrderedKeys: [],
|
||||
sessionKeys: [],
|
||||
modalState: 'maximized',
|
||||
isModalOpen: false,
|
||||
},
|
||||
|
||||
globalSearch: {},
|
||||
|
||||
userSearch: {},
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import type { InlineBotSettings } from '../../types';
|
||||
import type { GlobalState, TabArgs } from '../types';
|
||||
import type {
|
||||
GlobalState, TabArgs, WebApp, WebAppModalStateType,
|
||||
} from '../types';
|
||||
|
||||
import { getCurrentTabId } from '../../util/establishMultitabRole';
|
||||
import { getWebAppKey } from '../helpers/bots';
|
||||
import { selectTabState } from '../selectors';
|
||||
import { updateTabState } from './tabs';
|
||||
|
||||
@ -32,3 +35,239 @@ export function replaceInlineBotsIsLoading<T extends GlobalState>(
|
||||
},
|
||||
}, tabId);
|
||||
}
|
||||
|
||||
export function updateWebApp <T extends GlobalState>(
|
||||
global: T, webApp: Partial<WebApp>,
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
): T {
|
||||
const currentTabState = selectTabState(global, tabId);
|
||||
const openedWebApps = currentTabState.webApps.openedWebApps;
|
||||
|
||||
const key = webApp && getWebAppKey(webApp);
|
||||
const originalWebApp = key ? openedWebApps[key] : undefined;
|
||||
|
||||
if (!originalWebApp) return global;
|
||||
|
||||
const updatedValue = {
|
||||
...originalWebApp,
|
||||
...webApp,
|
||||
};
|
||||
|
||||
const updatedWebAppKey = getWebAppKey(updatedValue);
|
||||
if (!updatedWebAppKey) return global;
|
||||
|
||||
const activeWebApp = currentTabState.webApps.activeWebApp;
|
||||
const activeWebAppKey = activeWebApp && getWebAppKey(activeWebApp);
|
||||
global = updateTabState(global, {
|
||||
webApps: {
|
||||
...currentTabState.webApps,
|
||||
...updatedWebAppKey === activeWebAppKey && {
|
||||
activeWebApp: updatedValue,
|
||||
},
|
||||
openedWebApps: {
|
||||
...openedWebApps,
|
||||
[updatedWebAppKey]: updatedValue,
|
||||
},
|
||||
},
|
||||
}, tabId);
|
||||
|
||||
return global;
|
||||
}
|
||||
|
||||
export function activateWebAppIfOpen<T extends GlobalState>(
|
||||
global: T, webAppKey: string,
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
): T {
|
||||
const currentTabState = selectTabState(global, tabId);
|
||||
const openedWebApps = currentTabState.webApps.openedWebApps;
|
||||
|
||||
if (!openedWebApps[webAppKey]) {
|
||||
return global;
|
||||
}
|
||||
|
||||
const newActiveWebApp = openedWebApps[webAppKey];
|
||||
|
||||
global = updateTabState(global, {
|
||||
webApps: {
|
||||
...currentTabState.webApps,
|
||||
activeWebApp: newActiveWebApp,
|
||||
modalState: 'maximized',
|
||||
},
|
||||
}, tabId);
|
||||
|
||||
return global;
|
||||
}
|
||||
|
||||
export function addWebAppToOpenList<T extends GlobalState>(
|
||||
global: T, webApp: WebApp,
|
||||
makeActive: boolean = true, openModalIfNotOpen: boolean = true,
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
): T {
|
||||
const currentTabState = selectTabState(global, tabId);
|
||||
|
||||
const key = getWebAppKey(webApp);
|
||||
|
||||
if (!key) return global;
|
||||
const newOpenedKeys = [...currentTabState.webApps.openedOrderedKeys];
|
||||
if (!newOpenedKeys.includes(key)) newOpenedKeys.push(key);
|
||||
|
||||
const newSessionKeys = [...currentTabState.webApps.sessionKeys];
|
||||
if (!newSessionKeys.includes(key)) newSessionKeys.push(key);
|
||||
|
||||
const openedWebApps = currentTabState.webApps.openedWebApps;
|
||||
|
||||
global = updateTabState(global, {
|
||||
webApps: {
|
||||
...currentTabState.webApps,
|
||||
...makeActive && { activeWebApp: webApp },
|
||||
isModalOpen: openModalIfNotOpen,
|
||||
modalState: 'maximized',
|
||||
openedWebApps: {
|
||||
...openedWebApps,
|
||||
[key]: webApp,
|
||||
},
|
||||
openedOrderedKeys: newOpenedKeys,
|
||||
sessionKeys: newSessionKeys,
|
||||
},
|
||||
}, tabId);
|
||||
|
||||
return global;
|
||||
}
|
||||
|
||||
export function removeActiveWebAppFromOpenList<T extends GlobalState>(
|
||||
global: T, ...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
): T {
|
||||
const currentTabState = selectTabState(global, tabId);
|
||||
|
||||
if (!currentTabState.webApps.activeWebApp) return global;
|
||||
|
||||
return removeWebAppFromOpenList(global, currentTabState.webApps.activeWebApp, false, tabId);
|
||||
}
|
||||
|
||||
export function removeWebAppFromOpenList<T extends GlobalState>(
|
||||
global: T, webApp: WebApp, skipClosingConfirmation?: boolean,
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
): T {
|
||||
const currentTabState = selectTabState(global, tabId);
|
||||
const openedWebApps = currentTabState.webApps.openedWebApps;
|
||||
|
||||
if (!skipClosingConfirmation && webApp.shouldConfirmClosing) {
|
||||
return updateWebApp(global, { ...webApp, isCloseModalOpen: true }, tabId);
|
||||
}
|
||||
|
||||
const updatedOpenedWebApps = { ...openedWebApps };
|
||||
const removingWebAppKey = getWebAppKey(webApp);
|
||||
|
||||
let newOpenedKeys = currentTabState.webApps.openedOrderedKeys;
|
||||
|
||||
if (removingWebAppKey) {
|
||||
delete updatedOpenedWebApps[removingWebAppKey];
|
||||
newOpenedKeys = currentTabState.webApps.openedOrderedKeys.filter((key) => key !== removingWebAppKey);
|
||||
}
|
||||
|
||||
const activeWebApp = currentTabState.webApps.activeWebApp;
|
||||
|
||||
const isRemovedAppActive = activeWebApp && (getWebAppKey(activeWebApp) === getWebAppKey(webApp));
|
||||
|
||||
const openedWebAppsValues = Object.values(updatedOpenedWebApps);
|
||||
const openedWebAppsCount = openedWebAppsValues.length;
|
||||
|
||||
global = updateTabState(global, {
|
||||
webApps: {
|
||||
...currentTabState.webApps,
|
||||
...isRemovedAppActive && {
|
||||
activeWebApp: openedWebAppsCount
|
||||
? openedWebAppsValues[openedWebAppsCount - 1] : undefined,
|
||||
},
|
||||
openedWebApps: updatedOpenedWebApps,
|
||||
openedOrderedKeys: newOpenedKeys,
|
||||
...!openedWebAppsCount && {
|
||||
sessionKeys: [],
|
||||
},
|
||||
},
|
||||
}, tabId);
|
||||
|
||||
return global;
|
||||
}
|
||||
|
||||
export function clearOpenedWebApps<T extends GlobalState>(
|
||||
global: T,
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
): T {
|
||||
const currentTabState = selectTabState(global, tabId);
|
||||
|
||||
const webAppsNotAllowedToClose = Object.fromEntries(
|
||||
Object.entries(currentTabState.webApps.openedWebApps).filter(
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
([url, webApp]) => webApp.shouldConfirmClosing,
|
||||
),
|
||||
);
|
||||
|
||||
const webAppsNotAllowedToCloseValues = Object.values(webAppsNotAllowedToClose);
|
||||
const hasNotAllowedToCloseApps = webAppsNotAllowedToCloseValues.length > 0;
|
||||
|
||||
if (!hasNotAllowedToCloseApps) {
|
||||
return updateTabState(global, {
|
||||
webApps: {
|
||||
...currentTabState.webApps,
|
||||
activeWebApp: undefined,
|
||||
openedWebApps: {},
|
||||
openedOrderedKeys: [],
|
||||
sessionKeys: [],
|
||||
},
|
||||
}, tabId);
|
||||
}
|
||||
|
||||
const currentActiveWebApp = currentTabState.webApps.activeWebApp;
|
||||
|
||||
const newActiveWebApp = currentActiveWebApp?.shouldConfirmClosing
|
||||
? currentActiveWebApp : webAppsNotAllowedToCloseValues[0];
|
||||
|
||||
newActiveWebApp.isCloseModalOpen = true;
|
||||
|
||||
const key = getWebAppKey(newActiveWebApp);
|
||||
|
||||
if (key) webAppsNotAllowedToClose[key] = newActiveWebApp;
|
||||
const newOpenedKeys = currentTabState.webApps.openedOrderedKeys.filter((k) => k in webAppsNotAllowedToClose);
|
||||
|
||||
return updateTabState(global, {
|
||||
webApps: {
|
||||
...currentTabState.webApps,
|
||||
activeWebApp: newActiveWebApp,
|
||||
openedWebApps: webAppsNotAllowedToClose,
|
||||
openedOrderedKeys: newOpenedKeys,
|
||||
},
|
||||
}, tabId);
|
||||
}
|
||||
|
||||
export function hasOpenedWebApps<T extends GlobalState>(
|
||||
global: T, ...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
): boolean {
|
||||
return Object.keys(selectTabState(global, tabId).webApps.openedWebApps).length > 0;
|
||||
}
|
||||
|
||||
export function replaceWebAppModalState<T extends GlobalState>(
|
||||
global: T, modalState: WebAppModalStateType,
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
): T {
|
||||
const currentTabState = selectTabState(global, tabId);
|
||||
return updateTabState(global, {
|
||||
webApps: {
|
||||
...currentTabState.webApps,
|
||||
modalState,
|
||||
},
|
||||
}, tabId);
|
||||
}
|
||||
|
||||
export function replaceIsWebAppModalOpen<T extends GlobalState>(
|
||||
global: T, value: boolean,
|
||||
...[tabId = getCurrentTabId()]: TabArgs<T>
|
||||
): T {
|
||||
const currentTabState = selectTabState(global, tabId);
|
||||
return updateTabState(global, {
|
||||
webApps: {
|
||||
...currentTabState.webApps,
|
||||
isModalOpen: value,
|
||||
},
|
||||
}, tabId);
|
||||
}
|
||||
|
||||
@ -133,6 +133,7 @@ import type {
|
||||
ThemeKey,
|
||||
ThreadId,
|
||||
} from '../types';
|
||||
import type { WebAppOutboundEvent } from '../types/webapp';
|
||||
import type { SearchResultKey } from '../util/keys/searchResultKey';
|
||||
import type { DownloadableMedia } from './helpers';
|
||||
|
||||
@ -143,6 +144,8 @@ export type MessageListType =
|
||||
|
||||
export type ChatListType = 'active' | 'archived' | 'saved';
|
||||
|
||||
export type WebAppModalStateType = 'maximized' | 'minimized';
|
||||
|
||||
export interface MessageList {
|
||||
chatId: string;
|
||||
threadId: ThreadId;
|
||||
@ -649,14 +652,13 @@ export type TabState = {
|
||||
isQuiz?: boolean;
|
||||
};
|
||||
|
||||
webApp?: {
|
||||
url: string;
|
||||
botId: string;
|
||||
buttonText: string;
|
||||
queryId?: string;
|
||||
slug?: string;
|
||||
replyInfo?: ApiInputMessageReplyInfo;
|
||||
canSendMessages?: boolean;
|
||||
webApps: {
|
||||
activeWebApp?: WebApp;
|
||||
openedOrderedKeys: string[];
|
||||
sessionKeys: string[];
|
||||
openedWebApps: Record<string, WebApp>;
|
||||
modalState : WebAppModalStateType;
|
||||
isModalOpen: boolean;
|
||||
};
|
||||
|
||||
botTrustRequest?: {
|
||||
@ -1229,6 +1231,30 @@ export type ApiDraft = {
|
||||
isLocal?: boolean;
|
||||
};
|
||||
|
||||
export type WebApp = {
|
||||
url: string;
|
||||
requestUrl?: string;
|
||||
botId: string;
|
||||
appName?: string;
|
||||
buttonText: string;
|
||||
peerId?: string;
|
||||
queryId?: string;
|
||||
slug?: string;
|
||||
replyInfo?: ApiInputMessageReplyInfo;
|
||||
canSendMessages?: boolean;
|
||||
isRemoveModalOpen?: boolean;
|
||||
isCloseModalOpen?: boolean;
|
||||
shouldConfirmClosing?: boolean;
|
||||
headerColor?: string;
|
||||
serverHeaderColor?: string;
|
||||
serverHeaderColorKey?: 'bg_color' | 'secondary_bg_color';
|
||||
backgroundColor?: string;
|
||||
isBackButtonVisible?: boolean;
|
||||
isSettingsButtonVisible?: boolean;
|
||||
sendEvent?: (event: WebAppOutboundEvent) => void;
|
||||
reloadFrame?: (url: string) => void;
|
||||
};
|
||||
|
||||
type WithTabId = { tabId?: number };
|
||||
|
||||
export interface ActionPayloads {
|
||||
@ -2032,7 +2058,6 @@ export interface ActionPayloads {
|
||||
focusLastMessage: WithTabId | undefined;
|
||||
updateDraftReplyInfo: Partial<ApiInputMessageReplyInfo> & WithTabId;
|
||||
resetDraftReplyInfo: WithTabId | undefined;
|
||||
closeWebApp: WithTabId | undefined;
|
||||
|
||||
// Multitab
|
||||
destroyConnection: undefined;
|
||||
@ -2930,6 +2955,9 @@ export interface ActionPayloads {
|
||||
isFromBotMenu?: boolean;
|
||||
startParam?: string;
|
||||
} & WithTabId;
|
||||
updateWebApp: {
|
||||
webApp: Partial<WebApp>;
|
||||
} & WithTabId;
|
||||
requestMainWebView: {
|
||||
botId: string;
|
||||
peerId: string;
|
||||
@ -2963,6 +2991,9 @@ export interface ActionPayloads {
|
||||
isFromConfirm?: boolean;
|
||||
shouldSkipBotTrustRequest?: boolean;
|
||||
} & WithTabId;
|
||||
openWebAppTab: {
|
||||
webApp?: WebApp;
|
||||
} & WithTabId;
|
||||
loadPreviewMedias: {
|
||||
botId: string;
|
||||
};
|
||||
@ -3066,6 +3097,13 @@ export interface ActionPayloads {
|
||||
chatId: string;
|
||||
usernames: string[];
|
||||
};
|
||||
closeActiveWebApp: WithTabId | undefined;
|
||||
closeWebApp: {
|
||||
webApp: WebApp;
|
||||
skipClosingConfirmation?: boolean;
|
||||
} & WithTabId;
|
||||
closeWebAppModal: WithTabId | undefined;
|
||||
changeWebAppModalState: WithTabId | undefined;
|
||||
|
||||
// Misc
|
||||
refreshLangPackFromCache: {
|
||||
|
||||
232
src/hooks/useDraggable.ts
Normal file
232
src/hooks/useDraggable.ts
Normal file
@ -0,0 +1,232 @@
|
||||
import type { RefObject } from 'react';
|
||||
import { useEffect, useSignal, useState } from '../lib/teact/teact';
|
||||
|
||||
import buildStyle from '../util/buildStyle';
|
||||
import { captureEvents } from '../util/captureEvents';
|
||||
import useFlag from './useFlag';
|
||||
import useLastCallback from './useLastCallback';
|
||||
|
||||
export interface Size {
|
||||
width: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface Point {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
let resizeTimeout: number | undefined;
|
||||
|
||||
export default function useDraggable(
|
||||
ref: RefObject<HTMLElement>,
|
||||
dragHandleElementRef: RefObject<HTMLElement>,
|
||||
isEnabled: boolean = true,
|
||||
originalSize: Size,
|
||||
) {
|
||||
const [elementCurrentPosition, setElementCurrentPosition] = useState<Point | undefined>(undefined);
|
||||
const [elementCurrentSize, setElementCurrentSize] = useState<Size | undefined>(undefined);
|
||||
|
||||
const [getElementPositionOnStartDrag, setElementPositionOnStartDrag] = useSignal({ x: 0, y: 0 });
|
||||
const [getDragStartPoint, setDragStartPoint] = useSignal({ x: 0, y: 0 });
|
||||
|
||||
const elementPositionOnStartDrag = getElementPositionOnStartDrag();
|
||||
const dragStartPoint = getDragStartPoint();
|
||||
|
||||
const element = ref.current;
|
||||
const dragHandleElement = dragHandleElementRef.current;
|
||||
|
||||
const [isInitiated, setIsInitiated] = useFlag(false);
|
||||
const [wasElementShown, setWasElementShown] = useFlag(false);
|
||||
const [isDragging, startDragging, stopDragging] = useFlag(false);
|
||||
const [isWindowsResizing, startWindowResizing, stopWindowResizing] = useFlag(false);
|
||||
|
||||
function getVisibleArea() {
|
||||
return {
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
};
|
||||
}
|
||||
|
||||
const getCenteredPosition = useLastCallback(() => {
|
||||
if (!elementCurrentSize) return undefined;
|
||||
const { width, height } = elementCurrentSize;
|
||||
|
||||
const visibleArea = getVisibleArea();
|
||||
const viewportWidth = visibleArea.width;
|
||||
const viewportHeight = visibleArea.height;
|
||||
|
||||
const centeredX = (viewportWidth - width) / 2;
|
||||
const centeredY = (viewportHeight - height) / 2;
|
||||
|
||||
return { x: centeredX, y: centeredY };
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (element) setWasElementShown();
|
||||
}, [element]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isInitiated && elementCurrentSize) {
|
||||
const centeredPosition = getCenteredPosition();
|
||||
if (!centeredPosition) return;
|
||||
|
||||
setElementCurrentPosition({ x: centeredPosition.x, y: centeredPosition.y });
|
||||
setIsInitiated();
|
||||
}
|
||||
}, [elementCurrentSize, isInitiated, element]);
|
||||
|
||||
const handleStartDrag = useLastCallback((event: MouseEvent | TouchEvent) => {
|
||||
const targetElement = event.target as HTMLElement;
|
||||
if (targetElement.closest('.no-drag') || !element) {
|
||||
return;
|
||||
}
|
||||
const { pageX, pageY } = ('touches' in event) ? event.touches[0] : event;
|
||||
|
||||
const { left, top } = element.getBoundingClientRect();
|
||||
setElementPositionOnStartDrag({ x: left, y: top });
|
||||
setDragStartPoint({ x: pageX, y: pageY });
|
||||
|
||||
startDragging();
|
||||
});
|
||||
|
||||
const handleRelease = useLastCallback(() => {
|
||||
stopDragging();
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!isEnabled) {
|
||||
stopDragging();
|
||||
}
|
||||
}, [isEnabled]);
|
||||
|
||||
const ensurePositionInVisibleArea = (x: number, y: number) => {
|
||||
const visibleArea = getVisibleArea();
|
||||
|
||||
const visibleAreaWidth = visibleArea.width;
|
||||
const visibleAreaHeight = visibleArea.height;
|
||||
|
||||
const componentWidth = elementCurrentSize!.width;
|
||||
const componentHeight = elementCurrentSize!.height;
|
||||
|
||||
let newX = x;
|
||||
let newY = y;
|
||||
|
||||
if (newX < 0) newX = 0;
|
||||
if (newY < 0) newY = 0;
|
||||
if (newX + componentWidth > visibleAreaWidth) newX = visibleAreaWidth - componentWidth;
|
||||
if (newY + componentHeight > visibleAreaHeight) newY = visibleAreaHeight - componentHeight;
|
||||
|
||||
return { x: newX, y: newY };
|
||||
};
|
||||
|
||||
const adjustPositionWithinBounds = useLastCallback(() => {
|
||||
const position = !wasElementShown ? getCenteredPosition() : elementCurrentPosition;
|
||||
if (!elementCurrentSize || !position) return;
|
||||
const newPosition = ensurePositionInVisibleArea(position.x, position.y);
|
||||
setElementCurrentPosition(newPosition);
|
||||
});
|
||||
|
||||
const ensureSizeInVisibleArea = useLastCallback((sizeForCheck: Size) => {
|
||||
const newSize = sizeForCheck;
|
||||
|
||||
const visibleArea = getVisibleArea();
|
||||
|
||||
newSize.width = Math.min(visibleArea.width, Math.max(originalSize.width, newSize.width));
|
||||
newSize.height = Math.min(visibleArea.height, Math.max(originalSize.height, newSize.height));
|
||||
|
||||
return newSize;
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const newSize = ensureSizeInVisibleArea({ width: originalSize.width, height: originalSize.height });
|
||||
if (newSize) setElementCurrentSize(newSize);
|
||||
}, [originalSize]);
|
||||
|
||||
const adjustSizeWithinBounds = useLastCallback(() => {
|
||||
if (!elementCurrentSize) return;
|
||||
const newSize = ensureSizeInVisibleArea(elementCurrentSize);
|
||||
if (newSize) setElementCurrentSize(newSize);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
adjustPositionWithinBounds();
|
||||
}, [elementCurrentSize]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleResize = () => {
|
||||
startWindowResizing();
|
||||
adjustSizeWithinBounds();
|
||||
adjustPositionWithinBounds();
|
||||
if (resizeTimeout) {
|
||||
clearTimeout(resizeTimeout);
|
||||
resizeTimeout = undefined;
|
||||
}
|
||||
resizeTimeout = window.setTimeout(() => {
|
||||
resizeTimeout = undefined;
|
||||
stopWindowResizing();
|
||||
}, 250);
|
||||
};
|
||||
|
||||
window.addEventListener('resize', handleResize);
|
||||
|
||||
return () => {
|
||||
clearTimeout(resizeTimeout);
|
||||
resizeTimeout = undefined;
|
||||
window.removeEventListener('resize', handleResize);
|
||||
};
|
||||
}, [adjustPositionWithinBounds]);
|
||||
|
||||
const handleDrag = useLastCallback((event: MouseEvent | TouchEvent) => {
|
||||
if (!isDragging || !element) return;
|
||||
const { pageX, pageY } = ('touches' in event) ? event.touches[0] : event;
|
||||
|
||||
const offsetX = pageX - dragStartPoint.x;
|
||||
const offsetY = pageY - dragStartPoint.y;
|
||||
|
||||
const newX = elementPositionOnStartDrag.x + offsetX;
|
||||
const newY = elementPositionOnStartDrag.y + offsetY;
|
||||
|
||||
if (elementCurrentSize) setElementCurrentPosition(ensurePositionInVisibleArea(newX, newY));
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
let cleanup: NoneToVoidFunction | undefined;
|
||||
if (dragHandleElement && isEnabled) {
|
||||
cleanup = captureEvents(dragHandleElement, {
|
||||
onCapture: handleStartDrag,
|
||||
onDrag: handleDrag,
|
||||
onRelease: handleRelease,
|
||||
onClick: handleRelease,
|
||||
onDoubleClick: handleRelease,
|
||||
});
|
||||
}
|
||||
return cleanup;
|
||||
}, [handleDrag, handleStartDrag, isEnabled, dragHandleElement]);
|
||||
|
||||
const cursorStyle = isDragging ? 'cursor: grabbing !important; ' : '';
|
||||
|
||||
if (!isInitiated || !elementCurrentSize || !elementCurrentPosition) {
|
||||
return {
|
||||
isDragging: false,
|
||||
style: cursorStyle,
|
||||
};
|
||||
}
|
||||
|
||||
const style = buildStyle(
|
||||
`left: ${elementCurrentPosition.x}px;`,
|
||||
`top: ${elementCurrentPosition.y}px;`,
|
||||
`width: ${elementCurrentSize.width}px;`,
|
||||
`height: ${elementCurrentSize.height}px;`,
|
||||
'position: fixed;',
|
||||
(isDragging || isWindowsResizing) && 'transition: none !important;',
|
||||
cursorStyle,
|
||||
);
|
||||
|
||||
return {
|
||||
position: elementCurrentPosition,
|
||||
size: elementCurrentSize,
|
||||
isDragging,
|
||||
style,
|
||||
};
|
||||
}
|
||||
@ -29,6 +29,14 @@
|
||||
mask-image: linear-gradient(to top, transparent $cutout, black $height);
|
||||
}
|
||||
|
||||
@mixin gradient-border-horizontal($borderStart, $borderEnd) {
|
||||
mask-image: linear-gradient(to right, transparent, black $borderStart, black $borderEnd, transparent);
|
||||
}
|
||||
|
||||
@mixin gradient-border-left($indent) {
|
||||
mask-image: linear-gradient(to right, transparent, black $indent);
|
||||
}
|
||||
|
||||
@mixin gradient-border-top-bottom($top, $bottom) {
|
||||
mask-image: linear-gradient(transparent 0%, black $top, black calc(100% - $bottom), transparent 100%);
|
||||
}
|
||||
|
||||
@ -79,198 +79,200 @@ $icons-map: (
|
||||
"close-topic": "\f130",
|
||||
"close": "\f131",
|
||||
"cloud-download": "\f132",
|
||||
"collapse": "\f133",
|
||||
"colorize": "\f134",
|
||||
"comments-sticker": "\f135",
|
||||
"comments": "\f136",
|
||||
"copy-media": "\f137",
|
||||
"copy": "\f138",
|
||||
"darkmode": "\f139",
|
||||
"data": "\f13a",
|
||||
"delete-filled": "\f13b",
|
||||
"delete-left": "\f13c",
|
||||
"delete-user": "\f13d",
|
||||
"delete": "\f13e",
|
||||
"document": "\f13f",
|
||||
"double-badge": "\f140",
|
||||
"down": "\f141",
|
||||
"download": "\f142",
|
||||
"eats": "\f143",
|
||||
"edit": "\f144",
|
||||
"email": "\f145",
|
||||
"enter": "\f146",
|
||||
"expand": "\f147",
|
||||
"eye-closed-outline": "\f148",
|
||||
"eye-closed": "\f149",
|
||||
"eye-outline": "\f14a",
|
||||
"eye": "\f14b",
|
||||
"favorite-filled": "\f14c",
|
||||
"favorite": "\f14d",
|
||||
"file-badge": "\f14e",
|
||||
"flag": "\f14f",
|
||||
"folder-badge": "\f150",
|
||||
"folder": "\f151",
|
||||
"fontsize": "\f152",
|
||||
"forums": "\f153",
|
||||
"forward": "\f154",
|
||||
"fullscreen": "\f155",
|
||||
"gifs": "\f156",
|
||||
"gift": "\f157",
|
||||
"group-filled": "\f158",
|
||||
"group": "\f159",
|
||||
"grouped-disable": "\f15a",
|
||||
"grouped": "\f15b",
|
||||
"hand-stop": "\f15c",
|
||||
"hashtag": "\f15d",
|
||||
"heart-outline": "\f15e",
|
||||
"heart": "\f15f",
|
||||
"help": "\f160",
|
||||
"info-filled": "\f161",
|
||||
"info": "\f162",
|
||||
"install": "\f163",
|
||||
"italic": "\f164",
|
||||
"key": "\f165",
|
||||
"keyboard": "\f166",
|
||||
"lamp": "\f167",
|
||||
"language": "\f168",
|
||||
"large-pause": "\f169",
|
||||
"large-play": "\f16a",
|
||||
"link-badge": "\f16b",
|
||||
"link-broken": "\f16c",
|
||||
"link": "\f16d",
|
||||
"location": "\f16e",
|
||||
"lock-badge": "\f16f",
|
||||
"lock": "\f170",
|
||||
"logout": "\f171",
|
||||
"loop": "\f172",
|
||||
"mention": "\f173",
|
||||
"message-failed": "\f174",
|
||||
"message-pending": "\f175",
|
||||
"message-read": "\f176",
|
||||
"message-succeeded": "\f177",
|
||||
"message": "\f178",
|
||||
"microphone-alt": "\f179",
|
||||
"microphone": "\f17a",
|
||||
"monospace": "\f17b",
|
||||
"more-circle": "\f17c",
|
||||
"more": "\f17d",
|
||||
"move-caption-down": "\f17e",
|
||||
"move-caption-up": "\f17f",
|
||||
"mute": "\f180",
|
||||
"muted": "\f181",
|
||||
"my-notes": "\f182",
|
||||
"new-chat-filled": "\f183",
|
||||
"next": "\f184",
|
||||
"nochannel": "\f185",
|
||||
"noise-suppression": "\f186",
|
||||
"non-contacts": "\f187",
|
||||
"one-filled": "\f188",
|
||||
"open-in-new-tab": "\f189",
|
||||
"password-off": "\f18a",
|
||||
"pause": "\f18b",
|
||||
"permissions": "\f18c",
|
||||
"phone-discard-outline": "\f18d",
|
||||
"phone-discard": "\f18e",
|
||||
"phone": "\f18f",
|
||||
"photo": "\f190",
|
||||
"pin-badge": "\f191",
|
||||
"pin-list": "\f192",
|
||||
"pin": "\f193",
|
||||
"pinned-chat": "\f194",
|
||||
"pinned-message": "\f195",
|
||||
"pip": "\f196",
|
||||
"play-story": "\f197",
|
||||
"play": "\f198",
|
||||
"poll": "\f199",
|
||||
"previous": "\f19a",
|
||||
"privacy-policy": "\f19b",
|
||||
"quote-text": "\f19c",
|
||||
"quote": "\f19d",
|
||||
"readchats": "\f19e",
|
||||
"recent": "\f19f",
|
||||
"reload": "\f1a0",
|
||||
"remove-quote": "\f1a1",
|
||||
"remove": "\f1a2",
|
||||
"reopen-topic": "\f1a3",
|
||||
"replace": "\f1a4",
|
||||
"replies": "\f1a5",
|
||||
"reply-filled": "\f1a6",
|
||||
"reply": "\f1a7",
|
||||
"revenue-split": "\f1a8",
|
||||
"revote": "\f1a9",
|
||||
"save-story": "\f1aa",
|
||||
"saved-messages": "\f1ab",
|
||||
"schedule": "\f1ac",
|
||||
"search": "\f1ad",
|
||||
"select": "\f1ae",
|
||||
"send-outline": "\f1af",
|
||||
"send": "\f1b0",
|
||||
"settings-filled": "\f1b1",
|
||||
"settings": "\f1b2",
|
||||
"share-filled": "\f1b3",
|
||||
"share-screen-outlined": "\f1b4",
|
||||
"share-screen-stop": "\f1b5",
|
||||
"share-screen": "\f1b6",
|
||||
"show-message": "\f1b7",
|
||||
"sidebar": "\f1b8",
|
||||
"skip-next": "\f1b9",
|
||||
"skip-previous": "\f1ba",
|
||||
"smallscreen": "\f1bb",
|
||||
"smile": "\f1bc",
|
||||
"sort": "\f1bd",
|
||||
"speaker-muted-story": "\f1be",
|
||||
"speaker-outline": "\f1bf",
|
||||
"speaker-story": "\f1c0",
|
||||
"speaker": "\f1c1",
|
||||
"spoiler-disable": "\f1c2",
|
||||
"spoiler": "\f1c3",
|
||||
"sport": "\f1c4",
|
||||
"star": "\f1c5",
|
||||
"stars-lock": "\f1c6",
|
||||
"stats": "\f1c7",
|
||||
"stealth-future": "\f1c8",
|
||||
"stealth-past": "\f1c9",
|
||||
"stickers": "\f1ca",
|
||||
"stop-raising-hand": "\f1cb",
|
||||
"stop": "\f1cc",
|
||||
"story-caption": "\f1cd",
|
||||
"story-expired": "\f1ce",
|
||||
"story-priority": "\f1cf",
|
||||
"story-reply": "\f1d0",
|
||||
"strikethrough": "\f1d1",
|
||||
"tag-add": "\f1d2",
|
||||
"tag-crossed": "\f1d3",
|
||||
"tag-filter": "\f1d4",
|
||||
"tag-name": "\f1d5",
|
||||
"tag": "\f1d6",
|
||||
"timer": "\f1d7",
|
||||
"toncoin": "\f1d8",
|
||||
"transcribe": "\f1d9",
|
||||
"truck": "\f1da",
|
||||
"unarchive": "\f1db",
|
||||
"underlined": "\f1dc",
|
||||
"unlock-badge": "\f1dd",
|
||||
"unlock": "\f1de",
|
||||
"unmute": "\f1df",
|
||||
"unpin": "\f1e0",
|
||||
"unread": "\f1e1",
|
||||
"up": "\f1e2",
|
||||
"user-filled": "\f1e3",
|
||||
"user-online": "\f1e4",
|
||||
"user": "\f1e5",
|
||||
"video-outlined": "\f1e6",
|
||||
"video-stop": "\f1e7",
|
||||
"video": "\f1e8",
|
||||
"view-once": "\f1e9",
|
||||
"voice-chat": "\f1ea",
|
||||
"volume-1": "\f1eb",
|
||||
"volume-2": "\f1ec",
|
||||
"volume-3": "\f1ed",
|
||||
"web": "\f1ee",
|
||||
"webapp": "\f1ef",
|
||||
"word-wrap": "\f1f0",
|
||||
"zoom-in": "\f1f1",
|
||||
"zoom-out": "\f1f2",
|
||||
"collapse-modal": "\f133",
|
||||
"collapse": "\f134",
|
||||
"colorize": "\f135",
|
||||
"comments-sticker": "\f136",
|
||||
"comments": "\f137",
|
||||
"copy-media": "\f138",
|
||||
"copy": "\f139",
|
||||
"darkmode": "\f13a",
|
||||
"data": "\f13b",
|
||||
"delete-filled": "\f13c",
|
||||
"delete-left": "\f13d",
|
||||
"delete-user": "\f13e",
|
||||
"delete": "\f13f",
|
||||
"document": "\f140",
|
||||
"double-badge": "\f141",
|
||||
"down": "\f142",
|
||||
"download": "\f143",
|
||||
"eats": "\f144",
|
||||
"edit": "\f145",
|
||||
"email": "\f146",
|
||||
"enter": "\f147",
|
||||
"expand-modal": "\f148",
|
||||
"expand": "\f149",
|
||||
"eye-closed-outline": "\f14a",
|
||||
"eye-closed": "\f14b",
|
||||
"eye-outline": "\f14c",
|
||||
"eye": "\f14d",
|
||||
"favorite-filled": "\f14e",
|
||||
"favorite": "\f14f",
|
||||
"file-badge": "\f150",
|
||||
"flag": "\f151",
|
||||
"folder-badge": "\f152",
|
||||
"folder": "\f153",
|
||||
"fontsize": "\f154",
|
||||
"forums": "\f155",
|
||||
"forward": "\f156",
|
||||
"fullscreen": "\f157",
|
||||
"gifs": "\f158",
|
||||
"gift": "\f159",
|
||||
"group-filled": "\f15a",
|
||||
"group": "\f15b",
|
||||
"grouped-disable": "\f15c",
|
||||
"grouped": "\f15d",
|
||||
"hand-stop": "\f15e",
|
||||
"hashtag": "\f15f",
|
||||
"heart-outline": "\f160",
|
||||
"heart": "\f161",
|
||||
"help": "\f162",
|
||||
"info-filled": "\f163",
|
||||
"info": "\f164",
|
||||
"install": "\f165",
|
||||
"italic": "\f166",
|
||||
"key": "\f167",
|
||||
"keyboard": "\f168",
|
||||
"lamp": "\f169",
|
||||
"language": "\f16a",
|
||||
"large-pause": "\f16b",
|
||||
"large-play": "\f16c",
|
||||
"link-badge": "\f16d",
|
||||
"link-broken": "\f16e",
|
||||
"link": "\f16f",
|
||||
"location": "\f170",
|
||||
"lock-badge": "\f171",
|
||||
"lock": "\f172",
|
||||
"logout": "\f173",
|
||||
"loop": "\f174",
|
||||
"mention": "\f175",
|
||||
"message-failed": "\f176",
|
||||
"message-pending": "\f177",
|
||||
"message-read": "\f178",
|
||||
"message-succeeded": "\f179",
|
||||
"message": "\f17a",
|
||||
"microphone-alt": "\f17b",
|
||||
"microphone": "\f17c",
|
||||
"monospace": "\f17d",
|
||||
"more-circle": "\f17e",
|
||||
"more": "\f17f",
|
||||
"move-caption-down": "\f180",
|
||||
"move-caption-up": "\f181",
|
||||
"mute": "\f182",
|
||||
"muted": "\f183",
|
||||
"my-notes": "\f184",
|
||||
"new-chat-filled": "\f185",
|
||||
"next": "\f186",
|
||||
"nochannel": "\f187",
|
||||
"noise-suppression": "\f188",
|
||||
"non-contacts": "\f189",
|
||||
"one-filled": "\f18a",
|
||||
"open-in-new-tab": "\f18b",
|
||||
"password-off": "\f18c",
|
||||
"pause": "\f18d",
|
||||
"permissions": "\f18e",
|
||||
"phone-discard-outline": "\f18f",
|
||||
"phone-discard": "\f190",
|
||||
"phone": "\f191",
|
||||
"photo": "\f192",
|
||||
"pin-badge": "\f193",
|
||||
"pin-list": "\f194",
|
||||
"pin": "\f195",
|
||||
"pinned-chat": "\f196",
|
||||
"pinned-message": "\f197",
|
||||
"pip": "\f198",
|
||||
"play-story": "\f199",
|
||||
"play": "\f19a",
|
||||
"poll": "\f19b",
|
||||
"previous": "\f19c",
|
||||
"privacy-policy": "\f19d",
|
||||
"quote-text": "\f19e",
|
||||
"quote": "\f19f",
|
||||
"readchats": "\f1a0",
|
||||
"recent": "\f1a1",
|
||||
"reload": "\f1a2",
|
||||
"remove-quote": "\f1a3",
|
||||
"remove": "\f1a4",
|
||||
"reopen-topic": "\f1a5",
|
||||
"replace": "\f1a6",
|
||||
"replies": "\f1a7",
|
||||
"reply-filled": "\f1a8",
|
||||
"reply": "\f1a9",
|
||||
"revenue-split": "\f1aa",
|
||||
"revote": "\f1ab",
|
||||
"save-story": "\f1ac",
|
||||
"saved-messages": "\f1ad",
|
||||
"schedule": "\f1ae",
|
||||
"search": "\f1af",
|
||||
"select": "\f1b0",
|
||||
"send-outline": "\f1b1",
|
||||
"send": "\f1b2",
|
||||
"settings-filled": "\f1b3",
|
||||
"settings": "\f1b4",
|
||||
"share-filled": "\f1b5",
|
||||
"share-screen-outlined": "\f1b6",
|
||||
"share-screen-stop": "\f1b7",
|
||||
"share-screen": "\f1b8",
|
||||
"show-message": "\f1b9",
|
||||
"sidebar": "\f1ba",
|
||||
"skip-next": "\f1bb",
|
||||
"skip-previous": "\f1bc",
|
||||
"smallscreen": "\f1bd",
|
||||
"smile": "\f1be",
|
||||
"sort": "\f1bf",
|
||||
"speaker-muted-story": "\f1c0",
|
||||
"speaker-outline": "\f1c1",
|
||||
"speaker-story": "\f1c2",
|
||||
"speaker": "\f1c3",
|
||||
"spoiler-disable": "\f1c4",
|
||||
"spoiler": "\f1c5",
|
||||
"sport": "\f1c6",
|
||||
"star": "\f1c7",
|
||||
"stars-lock": "\f1c8",
|
||||
"stats": "\f1c9",
|
||||
"stealth-future": "\f1ca",
|
||||
"stealth-past": "\f1cb",
|
||||
"stickers": "\f1cc",
|
||||
"stop-raising-hand": "\f1cd",
|
||||
"stop": "\f1ce",
|
||||
"story-caption": "\f1cf",
|
||||
"story-expired": "\f1d0",
|
||||
"story-priority": "\f1d1",
|
||||
"story-reply": "\f1d2",
|
||||
"strikethrough": "\f1d3",
|
||||
"tag-add": "\f1d4",
|
||||
"tag-crossed": "\f1d5",
|
||||
"tag-filter": "\f1d6",
|
||||
"tag-name": "\f1d7",
|
||||
"tag": "\f1d8",
|
||||
"timer": "\f1d9",
|
||||
"toncoin": "\f1da",
|
||||
"transcribe": "\f1db",
|
||||
"truck": "\f1dc",
|
||||
"unarchive": "\f1dd",
|
||||
"underlined": "\f1de",
|
||||
"unlock-badge": "\f1df",
|
||||
"unlock": "\f1e0",
|
||||
"unmute": "\f1e1",
|
||||
"unpin": "\f1e2",
|
||||
"unread": "\f1e3",
|
||||
"up": "\f1e4",
|
||||
"user-filled": "\f1e5",
|
||||
"user-online": "\f1e6",
|
||||
"user": "\f1e7",
|
||||
"video-outlined": "\f1e8",
|
||||
"video-stop": "\f1e9",
|
||||
"video": "\f1ea",
|
||||
"view-once": "\f1eb",
|
||||
"voice-chat": "\f1ec",
|
||||
"volume-1": "\f1ed",
|
||||
"volume-2": "\f1ee",
|
||||
"volume-3": "\f1ef",
|
||||
"web": "\f1f0",
|
||||
"webapp": "\f1f1",
|
||||
"word-wrap": "\f1f2",
|
||||
"zoom-in": "\f1f3",
|
||||
"zoom-out": "\f1f4",
|
||||
);
|
||||
|
||||
.icon-active-sessions::before {
|
||||
@ -423,6 +425,9 @@ $icons-map: (
|
||||
.icon-cloud-download::before {
|
||||
content: map.get($icons-map, "cloud-download");
|
||||
}
|
||||
.icon-collapse-modal::before {
|
||||
content: map.get($icons-map, "collapse-modal");
|
||||
}
|
||||
.icon-collapse::before {
|
||||
content: map.get($icons-map, "collapse");
|
||||
}
|
||||
@ -483,6 +488,9 @@ $icons-map: (
|
||||
.icon-enter::before {
|
||||
content: map.get($icons-map, "enter");
|
||||
}
|
||||
.icon-expand-modal::before {
|
||||
content: map.get($icons-map, "expand-modal");
|
||||
}
|
||||
.icon-expand::before {
|
||||
content: map.get($icons-map, "expand");
|
||||
}
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@ -6,6 +6,7 @@
|
||||
"--color-primary-shade": ["#4a95d6", "#7b71c6"],
|
||||
"--color-background": ["#FFFFFF", "#212121"],
|
||||
"--color-background-compact-menu": ["#FFFFFFBB", "#212121DD"],
|
||||
"--color-web-app-browser": ["#FFFFFFBB", "#0303038F"],
|
||||
"--color-background-compact-menu-reactions": ["#FFFFFFEB", "#212121DD"],
|
||||
"--color-background-compact-menu-hover": ["#00000011", "#00000066"],
|
||||
"--color-background-secondary": ["#f4f4f5", "#0F0F0F"],
|
||||
|
||||
@ -49,6 +49,7 @@ export type FontIconName =
|
||||
| 'close-topic'
|
||||
| 'close'
|
||||
| 'cloud-download'
|
||||
| 'collapse-modal'
|
||||
| 'collapse'
|
||||
| 'colorize'
|
||||
| 'comments-sticker'
|
||||
@ -69,6 +70,7 @@ export type FontIconName =
|
||||
| 'edit'
|
||||
| 'email'
|
||||
| 'enter'
|
||||
| 'expand-modal'
|
||||
| 'expand'
|
||||
| 'eye-closed-outline'
|
||||
| 'eye-closed'
|
||||
|
||||
3
src/types/language.d.ts
vendored
3
src/types/language.d.ts
vendored
@ -1539,6 +1539,9 @@ export interface LangPair {
|
||||
'GiftStarsOutgoing': {
|
||||
'user': string | number;
|
||||
};
|
||||
MiniAppsMoreTabs: {
|
||||
'botName': string | number;
|
||||
};
|
||||
'PrizeCredits': {
|
||||
'count': string | number;
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user