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}"
|
"CreditsBoxHistoryEntryGiftOutAbout" = "With Stars, {user} will be able to unlock content and services on Telegram. {link}"
|
||||||
"CreditsBoxOutAbout" = "Review the {link} for Stars."
|
"CreditsBoxOutAbout" = "Review the {link} for Stars."
|
||||||
"GiftStarsOutgoing" = "With Stars, {user} will be able to unlock content and services on Telegram."
|
"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."
|
"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 HistoryCalendar } from '../components/main/HistoryCalendar';
|
||||||
export { default as NewContactModal } from '../components/main/NewContactModal';
|
export { default as NewContactModal } from '../components/main/NewContactModal';
|
||||||
export { default as WebAppModal } from '../components/modals/webApp/WebAppModal';
|
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 BotTrustModal } from '../components/main/BotTrustModal';
|
||||||
export { default as AttachBotInstallModal } from '../components/modals/attachBotInstall/AttachBotInstallModal';
|
export { default as AttachBotInstallModal } from '../components/modals/attachBotInstall/AttachBotInstallModal';
|
||||||
export { default as DeleteFolderDialog } from '../components/main/DeleteFolderDialog';
|
export { default as DeleteFolderDialog } from '../components/main/DeleteFolderDialog';
|
||||||
|
|||||||
@ -35,7 +35,7 @@ type ModalKey = keyof Pick<TabState,
|
|||||||
'reportAdModal' |
|
'reportAdModal' |
|
||||||
'starsBalanceModal' |
|
'starsBalanceModal' |
|
||||||
'isStarPaymentModalOpen' |
|
'isStarPaymentModalOpen' |
|
||||||
'webApp' |
|
'webApps' |
|
||||||
'starsTransactionModal'
|
'starsTransactionModal'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
@ -52,7 +52,6 @@ type Entries<T> = {
|
|||||||
}[keyof T][];
|
}[keyof T][];
|
||||||
|
|
||||||
const MODALS: ModalRegistry = {
|
const MODALS: ModalRegistry = {
|
||||||
webApp: WebAppModal,
|
|
||||||
giftCodeModal: GiftCodeModal,
|
giftCodeModal: GiftCodeModal,
|
||||||
boostModal: BoostModal,
|
boostModal: BoostModal,
|
||||||
chatlistModal: ChatlistModal,
|
chatlistModal: ChatlistModal,
|
||||||
@ -61,6 +60,7 @@ const MODALS: ModalRegistry = {
|
|||||||
inviteViaLinkModal: InviteViaLinkModal,
|
inviteViaLinkModal: InviteViaLinkModal,
|
||||||
requestedAttachBotInstall: AttachBotInstallModal,
|
requestedAttachBotInstall: AttachBotInstallModal,
|
||||||
reportAdModal: ReportAdModal,
|
reportAdModal: ReportAdModal,
|
||||||
|
webApps: WebAppModal,
|
||||||
collectibleInfoModal: CollectibleInfoModal,
|
collectibleInfoModal: CollectibleInfoModal,
|
||||||
mapModal: MapModal,
|
mapModal: MapModal,
|
||||||
isStarPaymentModalOpen: StarsPaymentModal,
|
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 {
|
.root {
|
||||||
--color-transition: 0.25s ease-in-out;
|
--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 {
|
:global {
|
||||||
.modal-header {
|
.modal-header {
|
||||||
color: var(--color-header-text);
|
color: var(--color-header-text);
|
||||||
border-bottom: 1px solid var(--color-dividers);
|
|
||||||
padding: 0.5rem;
|
padding: 0.5rem;
|
||||||
|
border-bottom: 1px solid var(--color-dividers);
|
||||||
transition: var(--color-transition) background-color, var(--color-transition) color;
|
transition: var(--color-transition) background-color, var(--color-transition) color;
|
||||||
}
|
}
|
||||||
|
|
||||||
.modal-dialog {
|
.modal-dialog {
|
||||||
|
pointer-events: auto;
|
||||||
height: 75%;
|
height: 75%;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
border: none;
|
border: none;
|
||||||
@ -18,6 +24,10 @@
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.modal-container {
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
.modal-content {
|
.modal-content {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
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 {
|
.more-button {
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
color: var(--color-header-text) !important;
|
color: var(--color-header-text) !important;
|
||||||
transition: var(--color-transition) color;
|
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 {
|
.close-icon {
|
||||||
opacity: 0.75;
|
opacity: 0.75;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -93,6 +334,7 @@
|
|||||||
|
|
||||||
.loading-spinner {
|
.loading-spinner {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
pointer-events: none;
|
||||||
top: 50%;
|
top: 50%;
|
||||||
left: 50%;
|
left: 50%;
|
||||||
transform: translate(-50%, -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 { useCallback, useEffect, useRef } from '../../../../lib/teact/teact';
|
||||||
import { getActions } from '../../../../global';
|
import { getActions } from '../../../../global';
|
||||||
|
|
||||||
|
import type { WebApp } from '../../../../global/types';
|
||||||
import type { WebAppInboundEvent, WebAppOutboundEvent } from '../../../../types/webapp';
|
import type { WebAppInboundEvent, WebAppOutboundEvent } from '../../../../types/webapp';
|
||||||
|
|
||||||
import { extractCurrentThemeParams } from '../../../../util/themeStyle';
|
import { extractCurrentThemeParams } from '../../../../util/themeStyle';
|
||||||
@ -35,6 +36,7 @@ const useWebAppFrame = (
|
|||||||
isOpen: boolean,
|
isOpen: boolean,
|
||||||
isSimpleView: boolean,
|
isSimpleView: boolean,
|
||||||
onEvent: (event: WebAppInboundEvent) => void,
|
onEvent: (event: WebAppInboundEvent) => void,
|
||||||
|
webApp?: WebApp,
|
||||||
onLoad?: () => void,
|
onLoad?: () => void,
|
||||||
) => {
|
) => {
|
||||||
const {
|
const {
|
||||||
@ -128,6 +130,12 @@ const useWebAppFrame = (
|
|||||||
if (ignoreEventsRef.current) {
|
if (ignoreEventsRef.current) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const contentWindow = ref.current?.contentWindow;
|
||||||
|
const sourceWindow = event.source as Window;
|
||||||
|
|
||||||
|
if (contentWindow !== sourceWindow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = JSON.parse(event.data) as WebAppInboundEvent;
|
const data = JSON.parse(event.data) as WebAppInboundEvent;
|
||||||
@ -138,7 +146,7 @@ const useWebAppFrame = (
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (eventType === 'web_app_close') {
|
if (eventType === 'web_app_close') {
|
||||||
closeWebApp();
|
if (webApp) closeWebApp({ webApp, skipClosingConfirmation: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (eventType === 'web_app_request_viewport') {
|
if (eventType === 'web_app_request_viewport') {
|
||||||
@ -175,9 +183,9 @@ const useWebAppFrame = (
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
showNotification({
|
// showNotification({
|
||||||
message: 'Clipboard access is not supported in this client yet',
|
// message: 'Clipboard access is not supported in this client yet',
|
||||||
});
|
// });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (eventType === 'web_app_open_scan_qr_popup') {
|
if (eventType === 'web_app_open_scan_qr_popup') {
|
||||||
@ -214,7 +222,10 @@ const useWebAppFrame = (
|
|||||||
} catch (err) {
|
} catch (err) {
|
||||||
// Ignore other messages
|
// Ignore other messages
|
||||||
}
|
}
|
||||||
}, [isSimpleView, sendEvent, onEvent, sendCustomStyle, sendTheme, sendViewport, onLoad, windowSize.isResizing]);
|
}, [
|
||||||
|
isSimpleView, sendEvent, onEvent, sendCustomStyle, webApp,
|
||||||
|
sendTheme, sendViewport, onLoad, windowSize.isResizing, ref,
|
||||||
|
]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const { width, height, isResizing } = windowSize;
|
const { width, height, isResizing } = windowSize;
|
||||||
@ -227,7 +238,7 @@ const useWebAppFrame = (
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener('message', handleMessage);
|
window.addEventListener('message', handleMessage);
|
||||||
return () => window.removeEventListener('message', handleMessage);
|
return () => window.removeEventListener('message', handleMessage);
|
||||||
}, [handleMessage]);
|
}, [handleMessage, ref]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isOpen && ref.current?.contentWindow) {
|
if (isOpen && ref.current?.contentWindow) {
|
||||||
|
|||||||
@ -37,6 +37,7 @@ export type OwnProps = {
|
|||||||
noBackdropClose?: boolean;
|
noBackdropClose?: boolean;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
style?: string;
|
style?: string;
|
||||||
|
dialogStyle?: string;
|
||||||
dialogRef?: React.RefObject<HTMLDivElement>;
|
dialogRef?: React.RefObject<HTMLDivElement>;
|
||||||
isLowStackPriority?: boolean;
|
isLowStackPriority?: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
@ -59,6 +60,7 @@ const Modal: FC<OwnProps> = ({
|
|||||||
noBackdropClose,
|
noBackdropClose,
|
||||||
children,
|
children,
|
||||||
style,
|
style,
|
||||||
|
dialogStyle,
|
||||||
isLowStackPriority,
|
isLowStackPriority,
|
||||||
onClose,
|
onClose,
|
||||||
onCloseAnimationEnd,
|
onCloseAnimationEnd,
|
||||||
@ -167,7 +169,7 @@ const Modal: FC<OwnProps> = ({
|
|||||||
>
|
>
|
||||||
<div className="modal-container">
|
<div className="modal-container">
|
||||||
<div className="modal-backdrop" onClick={!noBackdropClose ? onClose : undefined} />
|
<div className="modal-backdrop" onClick={!noBackdropClose ? onClose : undefined} />
|
||||||
<div className="modal-dialog" ref={dialogRef}>
|
<div className="modal-dialog" ref={dialogRef} style={dialogStyle}>
|
||||||
{renderHeader()}
|
{renderHeader()}
|
||||||
<div className={buildClassName('modal-content custom-scroll', contentClassName)} style={style}>
|
<div className={buildClassName('modal-content custom-scroll', contentClassName)} style={style}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
import type { InlineBotSettings } from '../../../types';
|
import type { InlineBotSettings } from '../../../types';
|
||||||
import type { RequiredGlobalActions } from '../../index';
|
import type { RequiredGlobalActions } from '../../index';
|
||||||
import type { ActionReturnType, GlobalState, TabArgs } from '../../types';
|
import type {
|
||||||
|
ActionReturnType, GlobalState, TabArgs, WebApp,
|
||||||
|
} from '../../types';
|
||||||
import {
|
import {
|
||||||
type ApiChat, type ApiChatType, type ApiContact, type ApiInputMessageReplyInfo, type ApiPeer, type ApiUrlAuthResult,
|
type ApiChat, type ApiChatType, type ApiContact, type ApiInputMessageReplyInfo, type ApiPeer, type ApiUrlAuthResult,
|
||||||
MAIN_THREAD_ID,
|
MAIN_THREAD_ID,
|
||||||
@ -16,13 +18,22 @@ import { debounce } from '../../../util/schedulers';
|
|||||||
import { getServerTime } from '../../../util/serverTime';
|
import { getServerTime } from '../../../util/serverTime';
|
||||||
import { extractCurrentThemeParams } from '../../../util/themeStyle';
|
import { extractCurrentThemeParams } from '../../../util/themeStyle';
|
||||||
import { callApi } from '../../../api/gramjs';
|
import { callApi } from '../../../api/gramjs';
|
||||||
|
import {
|
||||||
|
getWebAppKey,
|
||||||
|
} from '../../helpers/bots';
|
||||||
import {
|
import {
|
||||||
addActionHandler, getGlobal, setGlobal,
|
addActionHandler, getGlobal, setGlobal,
|
||||||
} from '../../index';
|
} from '../../index';
|
||||||
import {
|
import {
|
||||||
removeBlockedUser, updateManagementProgress, updateUser, updateUserFullInfo,
|
removeBlockedUser, updateManagementProgress, updateUser, updateUserFullInfo,
|
||||||
} from '../../reducers';
|
} 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 { updateTabState } from '../../reducers/tabs';
|
||||||
import {
|
import {
|
||||||
selectBot,
|
selectBot,
|
||||||
@ -482,6 +493,8 @@ addActionHandler('requestSimpleWebView', async (global, actions, payload): Promi
|
|||||||
tabId = getCurrentTabId(),
|
tabId = getCurrentTabId(),
|
||||||
} = payload;
|
} = payload;
|
||||||
|
|
||||||
|
if (checkIfOpenOrActivate(global, botId, tabId, url)) return;
|
||||||
|
|
||||||
const bot = selectUser(global, botId);
|
const bot = selectUser(global, botId);
|
||||||
if (!bot) return;
|
if (!bot) return;
|
||||||
|
|
||||||
@ -513,13 +526,13 @@ addActionHandler('requestSimpleWebView', async (global, actions, payload): Promi
|
|||||||
}
|
}
|
||||||
|
|
||||||
global = getGlobal();
|
global = getGlobal();
|
||||||
global = updateTabState(global, {
|
const newActiveApp: WebApp = {
|
||||||
webApp: {
|
requestUrl: url,
|
||||||
url: webViewUrl,
|
url: webViewUrl,
|
||||||
botId,
|
botId,
|
||||||
buttonText,
|
buttonText,
|
||||||
},
|
};
|
||||||
}, tabId);
|
global = addWebAppToOpenList(global, newActiveApp, true, true, tabId);
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -529,6 +542,8 @@ addActionHandler('requestWebView', async (global, actions, payload): Promise<voi
|
|||||||
tabId = getCurrentTabId(),
|
tabId = getCurrentTabId(),
|
||||||
} = payload;
|
} = payload;
|
||||||
|
|
||||||
|
if (checkIfOpenOrActivate(global, botId, tabId, url)) return;
|
||||||
|
|
||||||
const bot = selectUser(global, botId);
|
const bot = selectUser(global, botId);
|
||||||
if (!bot) return;
|
if (!bot) return;
|
||||||
const peer = selectPeer(global, peerId);
|
const peer = selectPeer(global, peerId);
|
||||||
@ -574,15 +589,16 @@ addActionHandler('requestWebView', async (global, actions, payload): Promise<voi
|
|||||||
const { url: webViewUrl, queryId } = result;
|
const { url: webViewUrl, queryId } = result;
|
||||||
|
|
||||||
global = getGlobal();
|
global = getGlobal();
|
||||||
global = updateTabState(global, {
|
const newActiveApp: WebApp = {
|
||||||
webApp: {
|
requestUrl: url,
|
||||||
url: webViewUrl,
|
url: webViewUrl,
|
||||||
botId,
|
botId,
|
||||||
queryId,
|
peerId,
|
||||||
replyInfo,
|
queryId,
|
||||||
buttonText,
|
replyInfo,
|
||||||
},
|
buttonText,
|
||||||
}, tabId);
|
};
|
||||||
|
global = addWebAppToOpenList(global, newActiveApp, true, true, tabId);
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -592,6 +608,8 @@ addActionHandler('requestMainWebView', async (global, actions, payload): Promise
|
|||||||
tabId = getCurrentTabId(),
|
tabId = getCurrentTabId(),
|
||||||
} = payload;
|
} = payload;
|
||||||
|
|
||||||
|
if (checkIfOpenOrActivate(global, botId, tabId)) return;
|
||||||
|
|
||||||
const bot = selectUser(global, botId);
|
const bot = selectUser(global, botId);
|
||||||
if (!bot) return;
|
if (!bot) return;
|
||||||
const peer = selectPeer(global, peerId);
|
const peer = selectPeer(global, peerId);
|
||||||
@ -629,14 +647,14 @@ addActionHandler('requestMainWebView', async (global, actions, payload): Promise
|
|||||||
const { url: webViewUrl, queryId } = result;
|
const { url: webViewUrl, queryId } = result;
|
||||||
|
|
||||||
global = getGlobal();
|
global = getGlobal();
|
||||||
global = updateTabState(global, {
|
const newActiveApp: WebApp = {
|
||||||
webApp: {
|
url: webViewUrl,
|
||||||
url: webViewUrl,
|
botId,
|
||||||
botId,
|
peerId,
|
||||||
queryId,
|
queryId,
|
||||||
buttonText: '',
|
buttonText: '',
|
||||||
},
|
};
|
||||||
}, tabId);
|
global = addWebAppToOpenList(global, newActiveApp, true, true, tabId);
|
||||||
setGlobal(global);
|
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> => {
|
addActionHandler('requestAppWebView', async (global, actions, payload): Promise<void> => {
|
||||||
const {
|
const {
|
||||||
botId, appName, startApp, theme, isWriteAllowed, isFromConfirm, shouldSkipBotTrustRequest,
|
botId, appName, startApp, theme, isWriteAllowed, isFromConfirm, shouldSkipBotTrustRequest,
|
||||||
tabId = getCurrentTabId(),
|
tabId = getCurrentTabId(),
|
||||||
} = payload;
|
} = payload;
|
||||||
|
|
||||||
|
if (checkIfOpenOrActivate(global, botId, tabId, appName)) return;
|
||||||
|
|
||||||
const bot = selectUser(global, botId);
|
const bot = selectUser(global, botId);
|
||||||
if (!bot) return;
|
if (!bot) return;
|
||||||
|
|
||||||
@ -751,13 +783,19 @@ addActionHandler('requestAppWebView', async (global, actions, payload): Promise<
|
|||||||
|
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
|
|
||||||
global = updateTabState(global, {
|
global = getGlobal();
|
||||||
webApp: {
|
|
||||||
url,
|
const peerId = (peer ? peer.id : bot!.id);
|
||||||
botId,
|
|
||||||
buttonText: '',
|
const newActiveApp: WebApp = {
|
||||||
},
|
url,
|
||||||
}, tabId);
|
peerId,
|
||||||
|
botId,
|
||||||
|
appName,
|
||||||
|
buttonText: '',
|
||||||
|
};
|
||||||
|
global = addWebAppToOpenList(global, newActiveApp, true, true, tabId);
|
||||||
|
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -783,7 +821,7 @@ addActionHandler('prolongWebView', async (global, actions, payload): Promise<voi
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!result) {
|
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 || {};
|
const { tabId = getCurrentTabId() } = payload || {};
|
||||||
|
|
||||||
return updateTabState(global, {
|
global = removeActiveWebAppFromOpenList(global, tabId);
|
||||||
webApp: undefined,
|
if (!hasOpenedWebApps(global, tabId)) return replaceIsWebAppModalOpen(global, false, tabId);
|
||||||
}, 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 => {
|
addActionHandler('setWebAppPaymentSlug', (global, actions, payload): ActionReturnType => {
|
||||||
const { tabId = getCurrentTabId() } = payload;
|
const { tabId = getCurrentTabId() } = payload;
|
||||||
const tabState = selectTabState(global, tabId);
|
const tabState = selectTabState(global, tabId);
|
||||||
if (!tabState.webApp?.url) return undefined;
|
const activeWebApp = tabState.webApps.activeWebApp;
|
||||||
|
if (!activeWebApp?.url) return undefined;
|
||||||
|
|
||||||
return updateTabState(global, {
|
const updatedApp = {
|
||||||
webApp: {
|
...activeWebApp,
|
||||||
...tabState.webApp,
|
slug: payload.slug,
|
||||||
slug: payload.slug,
|
};
|
||||||
},
|
|
||||||
}, tabId);
|
return updateWebApp(global, updatedApp, tabId);
|
||||||
});
|
});
|
||||||
|
|
||||||
addActionHandler('cancelBotTrustRequest', (global, actions, payload): ActionReturnType => {
|
addActionHandler('cancelBotTrustRequest', (global, actions, payload): ActionReturnType => {
|
||||||
@ -875,6 +948,31 @@ addActionHandler('toggleAttachBot', async (global, actions, payload): Promise<vo
|
|||||||
await callApi('toggleAttachBot', { bot, isWriteAllowed, isEnabled });
|
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) {
|
async function loadAttachBots<T extends GlobalState>(global: T, hash?: string) {
|
||||||
const result = await callApi('loadAttachBots', { hash });
|
const result = await callApi('loadAttachBots', { hash });
|
||||||
if (!result) {
|
if (!result) {
|
||||||
|
|||||||
@ -127,9 +127,9 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
|||||||
|
|
||||||
case 'updateWebViewResultSent':
|
case 'updateWebViewResultSent':
|
||||||
Object.values(global.byTabId).forEach((tabState) => {
|
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.resetDraftReplyInfo({ tabId: tabState.id });
|
||||||
actions.closeWebApp({ tabId: tabState.id });
|
actions.closeActiveWebApp({ tabId: tabState.id });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
break;
|
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 { ApiChatType, ApiPhoto } from '../../api/types';
|
||||||
|
import type {
|
||||||
|
WebApp,
|
||||||
|
} from '../types';
|
||||||
|
|
||||||
export function getBotCoverMediaHash(photo: ApiPhoto) {
|
export function getBotCoverMediaHash(photo: ApiPhoto) {
|
||||||
return `photo${photo.id}?size=x`;
|
return `photo${photo.id}?size=x`;
|
||||||
@ -11,3 +14,9 @@ export function convertToApiChatType(type: string): ApiChatType | undefined {
|
|||||||
if (type === 'bots') return 'bots';
|
if (type === 'bots') return 'bots';
|
||||||
return undefined;
|
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: {},
|
byUsername: {},
|
||||||
},
|
},
|
||||||
|
|
||||||
|
webApps: {
|
||||||
|
openedWebApps: {},
|
||||||
|
openedOrderedKeys: [],
|
||||||
|
sessionKeys: [],
|
||||||
|
modalState: 'maximized',
|
||||||
|
isModalOpen: false,
|
||||||
|
},
|
||||||
|
|
||||||
globalSearch: {},
|
globalSearch: {},
|
||||||
|
|
||||||
userSearch: {},
|
userSearch: {},
|
||||||
|
|||||||
@ -1,7 +1,10 @@
|
|||||||
import type { InlineBotSettings } from '../../types';
|
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 { getCurrentTabId } from '../../util/establishMultitabRole';
|
||||||
|
import { getWebAppKey } from '../helpers/bots';
|
||||||
import { selectTabState } from '../selectors';
|
import { selectTabState } from '../selectors';
|
||||||
import { updateTabState } from './tabs';
|
import { updateTabState } from './tabs';
|
||||||
|
|
||||||
@ -32,3 +35,239 @@ export function replaceInlineBotsIsLoading<T extends GlobalState>(
|
|||||||
},
|
},
|
||||||
}, tabId);
|
}, 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,
|
ThemeKey,
|
||||||
ThreadId,
|
ThreadId,
|
||||||
} from '../types';
|
} from '../types';
|
||||||
|
import type { WebAppOutboundEvent } from '../types/webapp';
|
||||||
import type { SearchResultKey } from '../util/keys/searchResultKey';
|
import type { SearchResultKey } from '../util/keys/searchResultKey';
|
||||||
import type { DownloadableMedia } from './helpers';
|
import type { DownloadableMedia } from './helpers';
|
||||||
|
|
||||||
@ -143,6 +144,8 @@ export type MessageListType =
|
|||||||
|
|
||||||
export type ChatListType = 'active' | 'archived' | 'saved';
|
export type ChatListType = 'active' | 'archived' | 'saved';
|
||||||
|
|
||||||
|
export type WebAppModalStateType = 'maximized' | 'minimized';
|
||||||
|
|
||||||
export interface MessageList {
|
export interface MessageList {
|
||||||
chatId: string;
|
chatId: string;
|
||||||
threadId: ThreadId;
|
threadId: ThreadId;
|
||||||
@ -649,14 +652,13 @@ export type TabState = {
|
|||||||
isQuiz?: boolean;
|
isQuiz?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
webApp?: {
|
webApps: {
|
||||||
url: string;
|
activeWebApp?: WebApp;
|
||||||
botId: string;
|
openedOrderedKeys: string[];
|
||||||
buttonText: string;
|
sessionKeys: string[];
|
||||||
queryId?: string;
|
openedWebApps: Record<string, WebApp>;
|
||||||
slug?: string;
|
modalState : WebAppModalStateType;
|
||||||
replyInfo?: ApiInputMessageReplyInfo;
|
isModalOpen: boolean;
|
||||||
canSendMessages?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
botTrustRequest?: {
|
botTrustRequest?: {
|
||||||
@ -1229,6 +1231,30 @@ export type ApiDraft = {
|
|||||||
isLocal?: boolean;
|
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 };
|
type WithTabId = { tabId?: number };
|
||||||
|
|
||||||
export interface ActionPayloads {
|
export interface ActionPayloads {
|
||||||
@ -2032,7 +2058,6 @@ export interface ActionPayloads {
|
|||||||
focusLastMessage: WithTabId | undefined;
|
focusLastMessage: WithTabId | undefined;
|
||||||
updateDraftReplyInfo: Partial<ApiInputMessageReplyInfo> & WithTabId;
|
updateDraftReplyInfo: Partial<ApiInputMessageReplyInfo> & WithTabId;
|
||||||
resetDraftReplyInfo: WithTabId | undefined;
|
resetDraftReplyInfo: WithTabId | undefined;
|
||||||
closeWebApp: WithTabId | undefined;
|
|
||||||
|
|
||||||
// Multitab
|
// Multitab
|
||||||
destroyConnection: undefined;
|
destroyConnection: undefined;
|
||||||
@ -2930,6 +2955,9 @@ export interface ActionPayloads {
|
|||||||
isFromBotMenu?: boolean;
|
isFromBotMenu?: boolean;
|
||||||
startParam?: string;
|
startParam?: string;
|
||||||
} & WithTabId;
|
} & WithTabId;
|
||||||
|
updateWebApp: {
|
||||||
|
webApp: Partial<WebApp>;
|
||||||
|
} & WithTabId;
|
||||||
requestMainWebView: {
|
requestMainWebView: {
|
||||||
botId: string;
|
botId: string;
|
||||||
peerId: string;
|
peerId: string;
|
||||||
@ -2963,6 +2991,9 @@ export interface ActionPayloads {
|
|||||||
isFromConfirm?: boolean;
|
isFromConfirm?: boolean;
|
||||||
shouldSkipBotTrustRequest?: boolean;
|
shouldSkipBotTrustRequest?: boolean;
|
||||||
} & WithTabId;
|
} & WithTabId;
|
||||||
|
openWebAppTab: {
|
||||||
|
webApp?: WebApp;
|
||||||
|
} & WithTabId;
|
||||||
loadPreviewMedias: {
|
loadPreviewMedias: {
|
||||||
botId: string;
|
botId: string;
|
||||||
};
|
};
|
||||||
@ -3066,6 +3097,13 @@ export interface ActionPayloads {
|
|||||||
chatId: string;
|
chatId: string;
|
||||||
usernames: string[];
|
usernames: string[];
|
||||||
};
|
};
|
||||||
|
closeActiveWebApp: WithTabId | undefined;
|
||||||
|
closeWebApp: {
|
||||||
|
webApp: WebApp;
|
||||||
|
skipClosingConfirmation?: boolean;
|
||||||
|
} & WithTabId;
|
||||||
|
closeWebAppModal: WithTabId | undefined;
|
||||||
|
changeWebAppModalState: WithTabId | undefined;
|
||||||
|
|
||||||
// Misc
|
// Misc
|
||||||
refreshLangPackFromCache: {
|
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);
|
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) {
|
@mixin gradient-border-top-bottom($top, $bottom) {
|
||||||
mask-image: linear-gradient(transparent 0%, black $top, black calc(100% - $bottom), transparent 100%);
|
mask-image: linear-gradient(transparent 0%, black $top, black calc(100% - $bottom), transparent 100%);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -79,198 +79,200 @@ $icons-map: (
|
|||||||
"close-topic": "\f130",
|
"close-topic": "\f130",
|
||||||
"close": "\f131",
|
"close": "\f131",
|
||||||
"cloud-download": "\f132",
|
"cloud-download": "\f132",
|
||||||
"collapse": "\f133",
|
"collapse-modal": "\f133",
|
||||||
"colorize": "\f134",
|
"collapse": "\f134",
|
||||||
"comments-sticker": "\f135",
|
"colorize": "\f135",
|
||||||
"comments": "\f136",
|
"comments-sticker": "\f136",
|
||||||
"copy-media": "\f137",
|
"comments": "\f137",
|
||||||
"copy": "\f138",
|
"copy-media": "\f138",
|
||||||
"darkmode": "\f139",
|
"copy": "\f139",
|
||||||
"data": "\f13a",
|
"darkmode": "\f13a",
|
||||||
"delete-filled": "\f13b",
|
"data": "\f13b",
|
||||||
"delete-left": "\f13c",
|
"delete-filled": "\f13c",
|
||||||
"delete-user": "\f13d",
|
"delete-left": "\f13d",
|
||||||
"delete": "\f13e",
|
"delete-user": "\f13e",
|
||||||
"document": "\f13f",
|
"delete": "\f13f",
|
||||||
"double-badge": "\f140",
|
"document": "\f140",
|
||||||
"down": "\f141",
|
"double-badge": "\f141",
|
||||||
"download": "\f142",
|
"down": "\f142",
|
||||||
"eats": "\f143",
|
"download": "\f143",
|
||||||
"edit": "\f144",
|
"eats": "\f144",
|
||||||
"email": "\f145",
|
"edit": "\f145",
|
||||||
"enter": "\f146",
|
"email": "\f146",
|
||||||
"expand": "\f147",
|
"enter": "\f147",
|
||||||
"eye-closed-outline": "\f148",
|
"expand-modal": "\f148",
|
||||||
"eye-closed": "\f149",
|
"expand": "\f149",
|
||||||
"eye-outline": "\f14a",
|
"eye-closed-outline": "\f14a",
|
||||||
"eye": "\f14b",
|
"eye-closed": "\f14b",
|
||||||
"favorite-filled": "\f14c",
|
"eye-outline": "\f14c",
|
||||||
"favorite": "\f14d",
|
"eye": "\f14d",
|
||||||
"file-badge": "\f14e",
|
"favorite-filled": "\f14e",
|
||||||
"flag": "\f14f",
|
"favorite": "\f14f",
|
||||||
"folder-badge": "\f150",
|
"file-badge": "\f150",
|
||||||
"folder": "\f151",
|
"flag": "\f151",
|
||||||
"fontsize": "\f152",
|
"folder-badge": "\f152",
|
||||||
"forums": "\f153",
|
"folder": "\f153",
|
||||||
"forward": "\f154",
|
"fontsize": "\f154",
|
||||||
"fullscreen": "\f155",
|
"forums": "\f155",
|
||||||
"gifs": "\f156",
|
"forward": "\f156",
|
||||||
"gift": "\f157",
|
"fullscreen": "\f157",
|
||||||
"group-filled": "\f158",
|
"gifs": "\f158",
|
||||||
"group": "\f159",
|
"gift": "\f159",
|
||||||
"grouped-disable": "\f15a",
|
"group-filled": "\f15a",
|
||||||
"grouped": "\f15b",
|
"group": "\f15b",
|
||||||
"hand-stop": "\f15c",
|
"grouped-disable": "\f15c",
|
||||||
"hashtag": "\f15d",
|
"grouped": "\f15d",
|
||||||
"heart-outline": "\f15e",
|
"hand-stop": "\f15e",
|
||||||
"heart": "\f15f",
|
"hashtag": "\f15f",
|
||||||
"help": "\f160",
|
"heart-outline": "\f160",
|
||||||
"info-filled": "\f161",
|
"heart": "\f161",
|
||||||
"info": "\f162",
|
"help": "\f162",
|
||||||
"install": "\f163",
|
"info-filled": "\f163",
|
||||||
"italic": "\f164",
|
"info": "\f164",
|
||||||
"key": "\f165",
|
"install": "\f165",
|
||||||
"keyboard": "\f166",
|
"italic": "\f166",
|
||||||
"lamp": "\f167",
|
"key": "\f167",
|
||||||
"language": "\f168",
|
"keyboard": "\f168",
|
||||||
"large-pause": "\f169",
|
"lamp": "\f169",
|
||||||
"large-play": "\f16a",
|
"language": "\f16a",
|
||||||
"link-badge": "\f16b",
|
"large-pause": "\f16b",
|
||||||
"link-broken": "\f16c",
|
"large-play": "\f16c",
|
||||||
"link": "\f16d",
|
"link-badge": "\f16d",
|
||||||
"location": "\f16e",
|
"link-broken": "\f16e",
|
||||||
"lock-badge": "\f16f",
|
"link": "\f16f",
|
||||||
"lock": "\f170",
|
"location": "\f170",
|
||||||
"logout": "\f171",
|
"lock-badge": "\f171",
|
||||||
"loop": "\f172",
|
"lock": "\f172",
|
||||||
"mention": "\f173",
|
"logout": "\f173",
|
||||||
"message-failed": "\f174",
|
"loop": "\f174",
|
||||||
"message-pending": "\f175",
|
"mention": "\f175",
|
||||||
"message-read": "\f176",
|
"message-failed": "\f176",
|
||||||
"message-succeeded": "\f177",
|
"message-pending": "\f177",
|
||||||
"message": "\f178",
|
"message-read": "\f178",
|
||||||
"microphone-alt": "\f179",
|
"message-succeeded": "\f179",
|
||||||
"microphone": "\f17a",
|
"message": "\f17a",
|
||||||
"monospace": "\f17b",
|
"microphone-alt": "\f17b",
|
||||||
"more-circle": "\f17c",
|
"microphone": "\f17c",
|
||||||
"more": "\f17d",
|
"monospace": "\f17d",
|
||||||
"move-caption-down": "\f17e",
|
"more-circle": "\f17e",
|
||||||
"move-caption-up": "\f17f",
|
"more": "\f17f",
|
||||||
"mute": "\f180",
|
"move-caption-down": "\f180",
|
||||||
"muted": "\f181",
|
"move-caption-up": "\f181",
|
||||||
"my-notes": "\f182",
|
"mute": "\f182",
|
||||||
"new-chat-filled": "\f183",
|
"muted": "\f183",
|
||||||
"next": "\f184",
|
"my-notes": "\f184",
|
||||||
"nochannel": "\f185",
|
"new-chat-filled": "\f185",
|
||||||
"noise-suppression": "\f186",
|
"next": "\f186",
|
||||||
"non-contacts": "\f187",
|
"nochannel": "\f187",
|
||||||
"one-filled": "\f188",
|
"noise-suppression": "\f188",
|
||||||
"open-in-new-tab": "\f189",
|
"non-contacts": "\f189",
|
||||||
"password-off": "\f18a",
|
"one-filled": "\f18a",
|
||||||
"pause": "\f18b",
|
"open-in-new-tab": "\f18b",
|
||||||
"permissions": "\f18c",
|
"password-off": "\f18c",
|
||||||
"phone-discard-outline": "\f18d",
|
"pause": "\f18d",
|
||||||
"phone-discard": "\f18e",
|
"permissions": "\f18e",
|
||||||
"phone": "\f18f",
|
"phone-discard-outline": "\f18f",
|
||||||
"photo": "\f190",
|
"phone-discard": "\f190",
|
||||||
"pin-badge": "\f191",
|
"phone": "\f191",
|
||||||
"pin-list": "\f192",
|
"photo": "\f192",
|
||||||
"pin": "\f193",
|
"pin-badge": "\f193",
|
||||||
"pinned-chat": "\f194",
|
"pin-list": "\f194",
|
||||||
"pinned-message": "\f195",
|
"pin": "\f195",
|
||||||
"pip": "\f196",
|
"pinned-chat": "\f196",
|
||||||
"play-story": "\f197",
|
"pinned-message": "\f197",
|
||||||
"play": "\f198",
|
"pip": "\f198",
|
||||||
"poll": "\f199",
|
"play-story": "\f199",
|
||||||
"previous": "\f19a",
|
"play": "\f19a",
|
||||||
"privacy-policy": "\f19b",
|
"poll": "\f19b",
|
||||||
"quote-text": "\f19c",
|
"previous": "\f19c",
|
||||||
"quote": "\f19d",
|
"privacy-policy": "\f19d",
|
||||||
"readchats": "\f19e",
|
"quote-text": "\f19e",
|
||||||
"recent": "\f19f",
|
"quote": "\f19f",
|
||||||
"reload": "\f1a0",
|
"readchats": "\f1a0",
|
||||||
"remove-quote": "\f1a1",
|
"recent": "\f1a1",
|
||||||
"remove": "\f1a2",
|
"reload": "\f1a2",
|
||||||
"reopen-topic": "\f1a3",
|
"remove-quote": "\f1a3",
|
||||||
"replace": "\f1a4",
|
"remove": "\f1a4",
|
||||||
"replies": "\f1a5",
|
"reopen-topic": "\f1a5",
|
||||||
"reply-filled": "\f1a6",
|
"replace": "\f1a6",
|
||||||
"reply": "\f1a7",
|
"replies": "\f1a7",
|
||||||
"revenue-split": "\f1a8",
|
"reply-filled": "\f1a8",
|
||||||
"revote": "\f1a9",
|
"reply": "\f1a9",
|
||||||
"save-story": "\f1aa",
|
"revenue-split": "\f1aa",
|
||||||
"saved-messages": "\f1ab",
|
"revote": "\f1ab",
|
||||||
"schedule": "\f1ac",
|
"save-story": "\f1ac",
|
||||||
"search": "\f1ad",
|
"saved-messages": "\f1ad",
|
||||||
"select": "\f1ae",
|
"schedule": "\f1ae",
|
||||||
"send-outline": "\f1af",
|
"search": "\f1af",
|
||||||
"send": "\f1b0",
|
"select": "\f1b0",
|
||||||
"settings-filled": "\f1b1",
|
"send-outline": "\f1b1",
|
||||||
"settings": "\f1b2",
|
"send": "\f1b2",
|
||||||
"share-filled": "\f1b3",
|
"settings-filled": "\f1b3",
|
||||||
"share-screen-outlined": "\f1b4",
|
"settings": "\f1b4",
|
||||||
"share-screen-stop": "\f1b5",
|
"share-filled": "\f1b5",
|
||||||
"share-screen": "\f1b6",
|
"share-screen-outlined": "\f1b6",
|
||||||
"show-message": "\f1b7",
|
"share-screen-stop": "\f1b7",
|
||||||
"sidebar": "\f1b8",
|
"share-screen": "\f1b8",
|
||||||
"skip-next": "\f1b9",
|
"show-message": "\f1b9",
|
||||||
"skip-previous": "\f1ba",
|
"sidebar": "\f1ba",
|
||||||
"smallscreen": "\f1bb",
|
"skip-next": "\f1bb",
|
||||||
"smile": "\f1bc",
|
"skip-previous": "\f1bc",
|
||||||
"sort": "\f1bd",
|
"smallscreen": "\f1bd",
|
||||||
"speaker-muted-story": "\f1be",
|
"smile": "\f1be",
|
||||||
"speaker-outline": "\f1bf",
|
"sort": "\f1bf",
|
||||||
"speaker-story": "\f1c0",
|
"speaker-muted-story": "\f1c0",
|
||||||
"speaker": "\f1c1",
|
"speaker-outline": "\f1c1",
|
||||||
"spoiler-disable": "\f1c2",
|
"speaker-story": "\f1c2",
|
||||||
"spoiler": "\f1c3",
|
"speaker": "\f1c3",
|
||||||
"sport": "\f1c4",
|
"spoiler-disable": "\f1c4",
|
||||||
"star": "\f1c5",
|
"spoiler": "\f1c5",
|
||||||
"stars-lock": "\f1c6",
|
"sport": "\f1c6",
|
||||||
"stats": "\f1c7",
|
"star": "\f1c7",
|
||||||
"stealth-future": "\f1c8",
|
"stars-lock": "\f1c8",
|
||||||
"stealth-past": "\f1c9",
|
"stats": "\f1c9",
|
||||||
"stickers": "\f1ca",
|
"stealth-future": "\f1ca",
|
||||||
"stop-raising-hand": "\f1cb",
|
"stealth-past": "\f1cb",
|
||||||
"stop": "\f1cc",
|
"stickers": "\f1cc",
|
||||||
"story-caption": "\f1cd",
|
"stop-raising-hand": "\f1cd",
|
||||||
"story-expired": "\f1ce",
|
"stop": "\f1ce",
|
||||||
"story-priority": "\f1cf",
|
"story-caption": "\f1cf",
|
||||||
"story-reply": "\f1d0",
|
"story-expired": "\f1d0",
|
||||||
"strikethrough": "\f1d1",
|
"story-priority": "\f1d1",
|
||||||
"tag-add": "\f1d2",
|
"story-reply": "\f1d2",
|
||||||
"tag-crossed": "\f1d3",
|
"strikethrough": "\f1d3",
|
||||||
"tag-filter": "\f1d4",
|
"tag-add": "\f1d4",
|
||||||
"tag-name": "\f1d5",
|
"tag-crossed": "\f1d5",
|
||||||
"tag": "\f1d6",
|
"tag-filter": "\f1d6",
|
||||||
"timer": "\f1d7",
|
"tag-name": "\f1d7",
|
||||||
"toncoin": "\f1d8",
|
"tag": "\f1d8",
|
||||||
"transcribe": "\f1d9",
|
"timer": "\f1d9",
|
||||||
"truck": "\f1da",
|
"toncoin": "\f1da",
|
||||||
"unarchive": "\f1db",
|
"transcribe": "\f1db",
|
||||||
"underlined": "\f1dc",
|
"truck": "\f1dc",
|
||||||
"unlock-badge": "\f1dd",
|
"unarchive": "\f1dd",
|
||||||
"unlock": "\f1de",
|
"underlined": "\f1de",
|
||||||
"unmute": "\f1df",
|
"unlock-badge": "\f1df",
|
||||||
"unpin": "\f1e0",
|
"unlock": "\f1e0",
|
||||||
"unread": "\f1e1",
|
"unmute": "\f1e1",
|
||||||
"up": "\f1e2",
|
"unpin": "\f1e2",
|
||||||
"user-filled": "\f1e3",
|
"unread": "\f1e3",
|
||||||
"user-online": "\f1e4",
|
"up": "\f1e4",
|
||||||
"user": "\f1e5",
|
"user-filled": "\f1e5",
|
||||||
"video-outlined": "\f1e6",
|
"user-online": "\f1e6",
|
||||||
"video-stop": "\f1e7",
|
"user": "\f1e7",
|
||||||
"video": "\f1e8",
|
"video-outlined": "\f1e8",
|
||||||
"view-once": "\f1e9",
|
"video-stop": "\f1e9",
|
||||||
"voice-chat": "\f1ea",
|
"video": "\f1ea",
|
||||||
"volume-1": "\f1eb",
|
"view-once": "\f1eb",
|
||||||
"volume-2": "\f1ec",
|
"voice-chat": "\f1ec",
|
||||||
"volume-3": "\f1ed",
|
"volume-1": "\f1ed",
|
||||||
"web": "\f1ee",
|
"volume-2": "\f1ee",
|
||||||
"webapp": "\f1ef",
|
"volume-3": "\f1ef",
|
||||||
"word-wrap": "\f1f0",
|
"web": "\f1f0",
|
||||||
"zoom-in": "\f1f1",
|
"webapp": "\f1f1",
|
||||||
"zoom-out": "\f1f2",
|
"word-wrap": "\f1f2",
|
||||||
|
"zoom-in": "\f1f3",
|
||||||
|
"zoom-out": "\f1f4",
|
||||||
);
|
);
|
||||||
|
|
||||||
.icon-active-sessions::before {
|
.icon-active-sessions::before {
|
||||||
@ -423,6 +425,9 @@ $icons-map: (
|
|||||||
.icon-cloud-download::before {
|
.icon-cloud-download::before {
|
||||||
content: map.get($icons-map, "cloud-download");
|
content: map.get($icons-map, "cloud-download");
|
||||||
}
|
}
|
||||||
|
.icon-collapse-modal::before {
|
||||||
|
content: map.get($icons-map, "collapse-modal");
|
||||||
|
}
|
||||||
.icon-collapse::before {
|
.icon-collapse::before {
|
||||||
content: map.get($icons-map, "collapse");
|
content: map.get($icons-map, "collapse");
|
||||||
}
|
}
|
||||||
@ -483,6 +488,9 @@ $icons-map: (
|
|||||||
.icon-enter::before {
|
.icon-enter::before {
|
||||||
content: map.get($icons-map, "enter");
|
content: map.get($icons-map, "enter");
|
||||||
}
|
}
|
||||||
|
.icon-expand-modal::before {
|
||||||
|
content: map.get($icons-map, "expand-modal");
|
||||||
|
}
|
||||||
.icon-expand::before {
|
.icon-expand::before {
|
||||||
content: map.get($icons-map, "expand");
|
content: map.get($icons-map, "expand");
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@ -6,6 +6,7 @@
|
|||||||
"--color-primary-shade": ["#4a95d6", "#7b71c6"],
|
"--color-primary-shade": ["#4a95d6", "#7b71c6"],
|
||||||
"--color-background": ["#FFFFFF", "#212121"],
|
"--color-background": ["#FFFFFF", "#212121"],
|
||||||
"--color-background-compact-menu": ["#FFFFFFBB", "#212121DD"],
|
"--color-background-compact-menu": ["#FFFFFFBB", "#212121DD"],
|
||||||
|
"--color-web-app-browser": ["#FFFFFFBB", "#0303038F"],
|
||||||
"--color-background-compact-menu-reactions": ["#FFFFFFEB", "#212121DD"],
|
"--color-background-compact-menu-reactions": ["#FFFFFFEB", "#212121DD"],
|
||||||
"--color-background-compact-menu-hover": ["#00000011", "#00000066"],
|
"--color-background-compact-menu-hover": ["#00000011", "#00000066"],
|
||||||
"--color-background-secondary": ["#f4f4f5", "#0F0F0F"],
|
"--color-background-secondary": ["#f4f4f5", "#0F0F0F"],
|
||||||
|
|||||||
@ -49,6 +49,7 @@ export type FontIconName =
|
|||||||
| 'close-topic'
|
| 'close-topic'
|
||||||
| 'close'
|
| 'close'
|
||||||
| 'cloud-download'
|
| 'cloud-download'
|
||||||
|
| 'collapse-modal'
|
||||||
| 'collapse'
|
| 'collapse'
|
||||||
| 'colorize'
|
| 'colorize'
|
||||||
| 'comments-sticker'
|
| 'comments-sticker'
|
||||||
@ -69,6 +70,7 @@ export type FontIconName =
|
|||||||
| 'edit'
|
| 'edit'
|
||||||
| 'email'
|
| 'email'
|
||||||
| 'enter'
|
| 'enter'
|
||||||
|
| 'expand-modal'
|
||||||
| 'expand'
|
| 'expand'
|
||||||
| 'eye-closed-outline'
|
| 'eye-closed-outline'
|
||||||
| 'eye-closed'
|
| 'eye-closed'
|
||||||
|
|||||||
3
src/types/language.d.ts
vendored
3
src/types/language.d.ts
vendored
@ -1539,6 +1539,9 @@ export interface LangPair {
|
|||||||
'GiftStarsOutgoing': {
|
'GiftStarsOutgoing': {
|
||||||
'user': string | number;
|
'user': string | number;
|
||||||
};
|
};
|
||||||
|
MiniAppsMoreTabs: {
|
||||||
|
'botName': string | number;
|
||||||
|
};
|
||||||
'PrizeCredits': {
|
'PrizeCredits': {
|
||||||
'count': string | number;
|
'count': string | number;
|
||||||
};
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user