MiniApps: More apps tab (#5137)
This commit is contained in:
parent
c0be156229
commit
92ebef4f49
@ -1370,6 +1370,10 @@
|
|||||||
"StarsSubscribeInfoLinkText" = "Terms of Service";
|
"StarsSubscribeInfoLinkText" = "Terms of Service";
|
||||||
"StarsSubscribeInfoLink" = "https://telegram.org/tos/stars";
|
"StarsSubscribeInfoLink" = "https://telegram.org/tos/stars";
|
||||||
"StarsPerMonth" = "⭐️{amount}/month";
|
"StarsPerMonth" = "⭐️{amount}/month";
|
||||||
|
"OpenApp" = "Open App";
|
||||||
|
"PopularApps" = "Popular Apps";
|
||||||
|
"SearchApps" = "Search Apps";
|
||||||
|
"Apps" = "Apps";
|
||||||
"AreYouSureCloseMiniApps" = "Are you sure you want to close all Mini Apps?";
|
"AreYouSureCloseMiniApps" = "Are you sure you want to close all Mini Apps?";
|
||||||
"CloseMiniApps" = "Close Mini Apps";
|
"CloseMiniApps" = "Close Mini Apps";
|
||||||
"DoNotAskAgain" = "Don't ask again";
|
"DoNotAskAgain" = "Don't ask again";
|
||||||
|
|||||||
@ -18,6 +18,8 @@ type OwnProps = {
|
|||||||
badgeIcon?: IconName;
|
badgeIcon?: IconName;
|
||||||
className?: string;
|
className?: string;
|
||||||
badgeClassName?: string;
|
badgeClassName?: string;
|
||||||
|
badgeIconClassName?: string;
|
||||||
|
textClassName?: string;
|
||||||
onClick?: NoneToVoidFunction;
|
onClick?: NoneToVoidFunction;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -28,6 +30,8 @@ const PeerBadge = ({
|
|||||||
badgeIcon,
|
badgeIcon,
|
||||||
className,
|
className,
|
||||||
badgeClassName,
|
badgeClassName,
|
||||||
|
badgeIconClassName,
|
||||||
|
textClassName,
|
||||||
onClick,
|
onClick,
|
||||||
}: OwnProps) => {
|
}: OwnProps) => {
|
||||||
return (
|
return (
|
||||||
@ -39,12 +43,12 @@ const PeerBadge = ({
|
|||||||
<Avatar size="large" peer={peer} />
|
<Avatar size="large" peer={peer} />
|
||||||
{badgeText && (
|
{badgeText && (
|
||||||
<div className={buildClassName(styles.badge, badgeClassName)}>
|
<div className={buildClassName(styles.badge, badgeClassName)}>
|
||||||
{badgeIcon && <Icon name={badgeIcon} />}
|
{badgeIcon && <Icon name={badgeIcon} className={badgeIconClassName} />}
|
||||||
{badgeText}
|
{badgeText}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{text && <p className={styles.text}>{text}</p>}
|
{text && <p className={buildClassName(styles.text, textClassName)}>{text}</p>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -207,12 +207,14 @@
|
|||||||
.Link {
|
.Link {
|
||||||
float: right;
|
float: right;
|
||||||
color: var(--color-links);
|
color: var(--color-links);
|
||||||
font-weight: 400;
|
font-weight: 500;
|
||||||
margin-right: 1.5rem;
|
margin-right: 1.5rem;
|
||||||
|
transition: opacity 0.15s ease-in;
|
||||||
|
|
||||||
&:focus,
|
&:active,
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: underline;
|
text-decoration: none;
|
||||||
|
opacity: 0.85;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -67,7 +67,7 @@ const MinimizedWebAppModal = ({
|
|||||||
if (!isMinimizedState) return undefined;
|
if (!isMinimizedState) return undefined;
|
||||||
|
|
||||||
function renderTitle() {
|
function renderTitle() {
|
||||||
const activeTabName = activeTabBot?.firstName;
|
const activeTabName = peers.length > 0 && peers[0]?.firstName;
|
||||||
const title = openedTabsCount && activeTabName && openedTabsCount > 1
|
const title = openedTabsCount && activeTabName && openedTabsCount > 1
|
||||||
? `${lang('MiniAppsMoreTabs',
|
? `${lang('MiniAppsMoreTabs',
|
||||||
{
|
{
|
||||||
|
|||||||
49
src/components/modals/webApp/MoreAppsTabContent.module.scss
Normal file
49
src/components/modals/webApp/MoreAppsTabContent.module.scss
Normal file
@ -0,0 +1,49 @@
|
|||||||
|
@use "../../../styles/mixins";
|
||||||
|
|
||||||
|
.root {
|
||||||
|
height: 100%;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
z-index: 0;
|
||||||
|
padding-inline: 1.25rem;
|
||||||
|
padding-top: 1.25rem;
|
||||||
|
overflow-y: scroll;
|
||||||
|
|
||||||
|
@include mixins.adapt-padding-to-scrollbar(1rem);
|
||||||
|
}
|
||||||
|
|
||||||
|
.search {
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title {
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: 500;
|
||||||
|
padding: 0.5rem;
|
||||||
|
padding-block: 0.25rem;
|
||||||
|
justify-content: space-between;
|
||||||
|
display: flex;
|
||||||
|
padding-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-content {
|
||||||
|
display: grid;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.125rem;
|
||||||
|
grid-template-columns: repeat(5, 1fr);
|
||||||
|
padding: 0.125rem;
|
||||||
|
padding-bottom: 1.25em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.showMoreLink {
|
||||||
|
color: var(--color-links);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: opacity 0.2s ease-in;
|
||||||
|
|
||||||
|
&:hover, &:active {
|
||||||
|
background-color: transparent !important;
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
}
|
||||||
143
src/components/modals/webApp/MoreAppsTabContent.tsx
Normal file
143
src/components/modals/webApp/MoreAppsTabContent.tsx
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
import React, {
|
||||||
|
memo,
|
||||||
|
useCallback,
|
||||||
|
useMemo,
|
||||||
|
useState,
|
||||||
|
} from '../../../lib/teact/teact';
|
||||||
|
import { getActions, getGlobal, withGlobal } from '../../../global';
|
||||||
|
|
||||||
|
import { LoadMoreDirection } from '../../../types';
|
||||||
|
|
||||||
|
import { filterUsersByName } from '../../../global/helpers';
|
||||||
|
import { selectTabState } from '../../../global/selectors';
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import { throttle } from '../../../util/schedulers';
|
||||||
|
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
|
import useOldLang from '../../../hooks/useOldLang';
|
||||||
|
|
||||||
|
import InfiniteScroll from '../../ui/InfiniteScroll';
|
||||||
|
import SearchInput from '../../ui/SearchInput';
|
||||||
|
import WebAppGridItem from './WebAppGridtem';
|
||||||
|
|
||||||
|
import styles from './MoreAppsTabContent.module.scss';
|
||||||
|
|
||||||
|
const POPULAR_APPS_SLICE = 30;
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
isLoading?: boolean;
|
||||||
|
foundIds?: string[];
|
||||||
|
recentBotIds?: string[];
|
||||||
|
};
|
||||||
|
const LESS_GRID_ITEMS_AMOUNT = 5;
|
||||||
|
const runThrottled = throttle((cb) => cb(), 500, true);
|
||||||
|
|
||||||
|
const MoreAppsTabContent: FC<OwnProps & StateProps> = ({
|
||||||
|
foundIds,
|
||||||
|
recentBotIds,
|
||||||
|
}) => {
|
||||||
|
const oldLang = useOldLang();
|
||||||
|
const lang = useLang();
|
||||||
|
const [shouldShowMoreMine, setShouldShowMoreMine] = useState<boolean>(false);
|
||||||
|
const {
|
||||||
|
searchPopularBotApps,
|
||||||
|
} = getActions();
|
||||||
|
|
||||||
|
const handleToggleShowMoreMine = useLastCallback(() => {
|
||||||
|
setShouldShowMoreMine((prev) => !prev);
|
||||||
|
});
|
||||||
|
|
||||||
|
const [searchQuery, setSearchQuery] = useState('');
|
||||||
|
|
||||||
|
const filteredFoundIds = useMemo(() => {
|
||||||
|
if (!foundIds) return [];
|
||||||
|
|
||||||
|
const usersById = getGlobal().users.byId;
|
||||||
|
return filterUsersByName(foundIds, usersById, searchQuery);
|
||||||
|
}, [foundIds, searchQuery]);
|
||||||
|
|
||||||
|
const handleLoadMore = useCallback(({ direction }: { direction: LoadMoreDirection }) => {
|
||||||
|
if (direction === LoadMoreDirection.Backwards) {
|
||||||
|
runThrottled(() => {
|
||||||
|
searchPopularBotApps();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleSearchInputReset = useCallback(() => {
|
||||||
|
setSearchQuery('');
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<InfiniteScroll
|
||||||
|
className={buildClassName(styles.root, 'custom-scroll')}
|
||||||
|
items={filteredFoundIds}
|
||||||
|
onLoadMore={handleLoadMore}
|
||||||
|
itemSelector=".PopularAppGridItem"
|
||||||
|
noFastList
|
||||||
|
preloadBackwards={POPULAR_APPS_SLICE}
|
||||||
|
>
|
||||||
|
<SearchInput
|
||||||
|
className={styles.search}
|
||||||
|
value={searchQuery}
|
||||||
|
onChange={setSearchQuery}
|
||||||
|
onReset={handleSearchInputReset}
|
||||||
|
placeholder={lang('SearchApps')}
|
||||||
|
/>
|
||||||
|
{recentBotIds && !searchQuery && (
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.sectionTitle}>
|
||||||
|
<span>{oldLang('SearchAppsMine')}</span>
|
||||||
|
<span className={styles.showMoreLink} onClick={handleToggleShowMoreMine}>
|
||||||
|
{oldLang(shouldShowMoreMine ? 'ChatList.Search.ShowLess' : 'ChatList.Search.ShowMore')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className={styles.sectionContent}>
|
||||||
|
{recentBotIds.map((id, index) => {
|
||||||
|
if (!shouldShowMoreMine && index >= LESS_GRID_ITEMS_AMOUNT) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<WebAppGridItem
|
||||||
|
chatId={id}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<div className={styles.section}>
|
||||||
|
<div className={styles.sectionTitle}>
|
||||||
|
{searchQuery ? lang('Apps') : lang('PopularApps')}
|
||||||
|
</div>
|
||||||
|
<div className={styles.sectionContent}>
|
||||||
|
{filteredFoundIds && filteredFoundIds.map((id) => {
|
||||||
|
return (
|
||||||
|
<WebAppGridItem
|
||||||
|
chatId={id}
|
||||||
|
isPopularApp={!searchQuery}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</InfiniteScroll>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>((global) => {
|
||||||
|
const globalSearch = selectTabState(global).globalSearch;
|
||||||
|
const foundIds = globalSearch.popularBotApps?.peerIds;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isLoading: !foundIds && globalSearch.fetchingStatus?.botApps,
|
||||||
|
foundIds,
|
||||||
|
recentBotIds: global.topBotApps.userIds,
|
||||||
|
};
|
||||||
|
})(MoreAppsTabContent));
|
||||||
39
src/components/modals/webApp/WebAppGridItem.module.scss
Normal file
39
src/components/modals/webApp/WebAppGridItem.module.scss
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
.container {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding: 0.375rem;
|
||||||
|
border-radius: 0.625rem;
|
||||||
|
cursor: var(--custom-cursor, pointer);
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background-color: var(--color-background-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-count-badge {
|
||||||
|
font-family: var(--font-family-condensed);
|
||||||
|
font-weight: 600 !important;
|
||||||
|
font-size: 0.5rem !important;
|
||||||
|
border-width: 1px !important;
|
||||||
|
bottom: 0 !important;
|
||||||
|
padding-block: 0.1875rem !important;
|
||||||
|
|
||||||
|
background: rgba(0, 0, 0, 0.2) !important;
|
||||||
|
backdrop-filter: blur(50px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.user-badge-icon {
|
||||||
|
font-size: 0.4375rem !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
white-space: inherit !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
width: 4rem !important;
|
||||||
|
font-size: 0.625rem !important;
|
||||||
|
|
||||||
|
height: 1.625rem;
|
||||||
|
font-weight: 500;
|
||||||
|
line-height: 0.75rem;
|
||||||
|
}
|
||||||
87
src/components/modals/webApp/WebAppGridtem.tsx
Normal file
87
src/components/modals/webApp/WebAppGridtem.tsx
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
import React, { memo } from '../../../lib/teact/teact';
|
||||||
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
|
import type {
|
||||||
|
ApiUser,
|
||||||
|
} from '../../../api/types';
|
||||||
|
|
||||||
|
import {
|
||||||
|
selectUser,
|
||||||
|
} from '../../../global/selectors';
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import { formatIntegerCompact } from '../../../util/textFormat';
|
||||||
|
import { extractCurrentThemeParams } from '../../../util/themeStyle';
|
||||||
|
|
||||||
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
|
|
||||||
|
import PeerBadge from '../../common/PeerBadge';
|
||||||
|
|
||||||
|
import styles from './WebAppGridItem.module.scss';
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
// eslint-disable-next-line react/no-unused-prop-types
|
||||||
|
chatId: string;
|
||||||
|
isPopularApp?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type StateProps = {
|
||||||
|
user?: ApiUser;
|
||||||
|
};
|
||||||
|
|
||||||
|
function WebAppGridItem({ user, isPopularApp }: OwnProps & StateProps) {
|
||||||
|
const {
|
||||||
|
requestMainWebView,
|
||||||
|
} = getActions();
|
||||||
|
|
||||||
|
const handleClick = useLastCallback(() => {
|
||||||
|
if (!user) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const botId = user?.id;
|
||||||
|
if (!botId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const theme = extractCurrentThemeParams();
|
||||||
|
requestMainWebView({
|
||||||
|
botId,
|
||||||
|
peerId: botId,
|
||||||
|
theme,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!user) return undefined;
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-null/no-null
|
||||||
|
|
||||||
|
const title = user?.firstName;
|
||||||
|
const activeUserCount = user?.botActiveUsers;
|
||||||
|
const badgeText = activeUserCount && isPopularApp ? formatIntegerCompact(activeUserCount) : undefined;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.container}
|
||||||
|
onClick={handleClick}
|
||||||
|
>
|
||||||
|
<PeerBadge
|
||||||
|
className={buildClassName(styles.avatarContainer, isPopularApp && 'PopularAppGridItem')}
|
||||||
|
textClassName={styles.name}
|
||||||
|
badgeClassName={styles.userCountBadge}
|
||||||
|
badgeIconClassName={styles.userBadgeIcon}
|
||||||
|
peer={user}
|
||||||
|
text={title}
|
||||||
|
badgeText={badgeText}
|
||||||
|
badgeIcon="user-filled"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global, { chatId }): StateProps => {
|
||||||
|
const user = selectUser(global, chatId);
|
||||||
|
|
||||||
|
return {
|
||||||
|
user,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(WebAppGridItem));
|
||||||
@ -161,7 +161,7 @@
|
|||||||
padding-left: 1rem;
|
padding-left: 1rem;
|
||||||
padding-right: 1rem;
|
padding-right: 1rem;
|
||||||
|
|
||||||
font-size: 0.875rem;
|
font-size: 1rem;
|
||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
|
|
||||||
@ -220,6 +220,7 @@
|
|||||||
transform: scaleX(-1);
|
transform: scaleX(-1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.more-apps-tab-icon,
|
||||||
.avatar-container {
|
.avatar-container {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -227,6 +228,11 @@
|
|||||||
margin-right: 0.5rem;
|
margin-right: 0.5rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.more-apps-tab-icon {
|
||||||
|
font-size: 1.5rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
.web-app-tab-more-menu {
|
.web-app-tab-more-menu {
|
||||||
z-index: 1;
|
z-index: 1;
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@ -255,6 +261,7 @@
|
|||||||
transform: translate(-50%, -50%);
|
transform: translate(-50%, -50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.more-apps-button,
|
||||||
.window-state-button,
|
.window-state-button,
|
||||||
.header-button {
|
.header-button {
|
||||||
width: 1.75rem !important;
|
width: 1.75rem !important;
|
||||||
|
|||||||
@ -26,6 +26,7 @@ import useAppLayout from '../../../hooks/useAppLayout';
|
|||||||
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
||||||
import useDraggable from '../../../hooks/useDraggable';
|
import useDraggable from '../../../hooks/useDraggable';
|
||||||
import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
|
import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
import useLastCallback from '../../../hooks/useLastCallback';
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
import useOldLang from '../../../hooks/useOldLang';
|
import useOldLang from '../../../hooks/useOldLang';
|
||||||
|
|
||||||
@ -37,6 +38,7 @@ import Menu from '../../ui/Menu';
|
|||||||
import MenuItem from '../../ui/MenuItem';
|
import MenuItem from '../../ui/MenuItem';
|
||||||
import Modal from '../../ui/Modal';
|
import Modal from '../../ui/Modal';
|
||||||
import MinimizedWebAppModal from './MinimizedWebAppModal';
|
import MinimizedWebAppModal from './MinimizedWebAppModal';
|
||||||
|
import MoreAppsTabContent from './MoreAppsTabContent';
|
||||||
import WebAppModalTabContent from './WebAppModalTabContent';
|
import WebAppModalTabContent from './WebAppModalTabContent';
|
||||||
|
|
||||||
import styles from './WebAppModal.module.scss';
|
import styles from './WebAppModal.module.scss';
|
||||||
@ -77,6 +79,8 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
|
|||||||
changeWebAppModalState,
|
changeWebAppModalState,
|
||||||
openWebAppTab,
|
openWebAppTab,
|
||||||
updateWebApp,
|
updateWebApp,
|
||||||
|
openMoreAppsTab,
|
||||||
|
closeMoreAppsTab,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const maximizedStateSize = useMemo(() => {
|
const maximizedStateSize = useMemo(() => {
|
||||||
@ -94,7 +98,7 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const {
|
const {
|
||||||
openedWebApps, activeWebApp, openedOrderedKeys, sessionKeys,
|
openedWebApps, activeWebApp, openedOrderedKeys, sessionKeys, isMoreAppsTabActive,
|
||||||
} = modal || {};
|
} = modal || {};
|
||||||
const {
|
const {
|
||||||
isBackButtonVisible, headerColor, backgroundColor, isSettingsButtonVisible,
|
isBackButtonVisible, headerColor, backgroundColor, isSettingsButtonVisible,
|
||||||
@ -168,7 +172,8 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
|
|||||||
}
|
}
|
||||||
}, [currentWidth, currentHeight, isMaximizedState, minimizedStateSize, setFrameSize]);
|
}, [currentWidth, currentHeight, isMaximizedState, minimizedStateSize, setFrameSize]);
|
||||||
|
|
||||||
const lang = useOldLang();
|
const oldLang = useOldLang();
|
||||||
|
const lang = useLang();
|
||||||
const {
|
const {
|
||||||
queryId,
|
queryId,
|
||||||
} = activeWebApp || {};
|
} = activeWebApp || {};
|
||||||
@ -229,6 +234,10 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
|
|||||||
closeWebAppModal();
|
closeWebAppModal();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleCloseMoreAppsTab = useLastCallback(() => {
|
||||||
|
closeMoreAppsTab();
|
||||||
|
});
|
||||||
|
|
||||||
const handleTabClose = useLastCallback(() => {
|
const handleTabClose = useLastCallback(() => {
|
||||||
if (openTabsCount > 1) {
|
if (openTabsCount > 1) {
|
||||||
closeActiveWebApp();
|
closeActiveWebApp();
|
||||||
@ -268,6 +277,10 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
|
|||||||
changeWebAppModalState();
|
changeWebAppModalState();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const handleOpenMoreAppsTabClick = useLastCallback(() => {
|
||||||
|
openMoreAppsTab();
|
||||||
|
});
|
||||||
|
|
||||||
const handleTabClick = useLastCallback((tab: WebAppModalTab) => {
|
const handleTabClick = useLastCallback((tab: WebAppModalTab) => {
|
||||||
openWebAppTab({ webApp: tab.webApp });
|
openWebAppTab({ webApp: tab.webApp });
|
||||||
});
|
});
|
||||||
@ -304,12 +317,12 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{chat && bot && chat.id !== bot.id && (
|
{chat && bot && chat.id !== bot.id && (
|
||||||
<MenuItem icon="bots" onClick={openBotChat}>{lang('BotWebViewOpenBot')}</MenuItem>
|
<MenuItem icon="bots" onClick={openBotChat}>{oldLang('BotWebViewOpenBot')}</MenuItem>
|
||||||
)}
|
)}
|
||||||
<MenuItem icon="reload" onClick={handleRefreshClick}>{lang('WebApp.ReloadPage')}</MenuItem>
|
<MenuItem icon="reload" onClick={handleRefreshClick}>{oldLang('WebApp.ReloadPage')}</MenuItem>
|
||||||
{isSettingsButtonVisible && (
|
{isSettingsButtonVisible && (
|
||||||
<MenuItem icon="settings" onClick={handleSettingsButtonClick}>
|
<MenuItem icon="settings" onClick={handleSettingsButtonClick}>
|
||||||
{lang('Settings')}
|
{oldLang('Settings')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
{bot?.isAttachBot && (
|
{bot?.isAttachBot && (
|
||||||
@ -318,7 +331,7 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
|
|||||||
onClick={handleToggleClick}
|
onClick={handleToggleClick}
|
||||||
destructive={Boolean(attachBot)}
|
destructive={Boolean(attachBot)}
|
||||||
>
|
>
|
||||||
{lang(attachBot ? 'WebApp.RemoveBot' : 'WebApp.AddToAttachmentAdd')}
|
{oldLang(attachBot ? 'WebApp.RemoveBot' : 'WebApp.AddToAttachmentAdd')}
|
||||||
</MenuItem>
|
</MenuItem>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
@ -368,12 +381,13 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const headerTextVar = useMemo(() => {
|
const headerTextVar = useMemo(() => {
|
||||||
|
if (isMoreAppsTabActive) return 'color-text';
|
||||||
if (!headerColor) return undefined;
|
if (!headerColor) return undefined;
|
||||||
const { r, g, b } = hexToRgb(headerColor);
|
const { r, g, b } = hexToRgb(headerColor);
|
||||||
const luma = getColorLuma([r, g, b]);
|
const luma = getColorLuma([r, g, b]);
|
||||||
const adaptedLuma = theme === 'dark' ? 255 - luma : luma;
|
const adaptedLuma = theme === 'dark' ? 255 - luma : luma;
|
||||||
return adaptedLuma > LUMA_THRESHOLD ? 'color-text' : 'color-background';
|
return adaptedLuma > LUMA_THRESHOLD ? 'color-text' : 'color-background';
|
||||||
}, [headerColor, theme]);
|
}, [headerColor, theme, isMoreAppsTabActive]);
|
||||||
|
|
||||||
function renderTabCurveBorder(className: string) {
|
function renderTabCurveBorder(className: string) {
|
||||||
return (
|
return (
|
||||||
@ -422,7 +436,7 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
|
|||||||
round
|
round
|
||||||
color="translucent"
|
color="translucent"
|
||||||
size="tiny"
|
size="tiny"
|
||||||
ariaLabel={lang('Close')}
|
ariaLabel={oldLang('Close')}
|
||||||
onClick={handleTabClose}
|
onClick={handleTabClose}
|
||||||
>
|
>
|
||||||
<Icon className={styles.tabCloseIcon} name="close" />
|
<Icon className={styles.tabCloseIcon} name="close" />
|
||||||
@ -433,6 +447,53 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderMoreAppsTab() {
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={styles.tabButtonWrapper}
|
||||||
|
>
|
||||||
|
{renderTabCurveBorder(styles.tabButtonLeftCurve)}
|
||||||
|
<div
|
||||||
|
className={styles.tabButton}
|
||||||
|
>
|
||||||
|
<div className={styles.moreAppsTabIcon}>
|
||||||
|
<Icon className={styles.icon} name="add" />
|
||||||
|
</div>
|
||||||
|
{lang('OpenApp')}
|
||||||
|
<div className={styles.tabRightMask} />
|
||||||
|
<Button
|
||||||
|
className={styles.tabCloseButton}
|
||||||
|
round
|
||||||
|
color="translucent"
|
||||||
|
size="tiny"
|
||||||
|
ariaLabel={oldLang('Close')}
|
||||||
|
onClick={handleCloseMoreAppsTab}
|
||||||
|
>
|
||||||
|
<Icon className={styles.tabCloseIcon} name="close" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
{renderTabCurveBorder(styles.tabButtonRightCurve)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderMoreAppsButton() {
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
className={buildClassName(
|
||||||
|
styles.moreAppsButton,
|
||||||
|
'no-drag',
|
||||||
|
)}
|
||||||
|
round
|
||||||
|
color="translucent"
|
||||||
|
size="tiny"
|
||||||
|
onClick={handleOpenMoreAppsTabClick}
|
||||||
|
>
|
||||||
|
<Icon className={styles.icon} name="add" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-null/no-null
|
// eslint-disable-next-line no-null/no-null
|
||||||
const containerRef = useRef<HTMLDivElement>(null);
|
const containerRef = useRef<HTMLDivElement>(null);
|
||||||
useHorizontalScroll(containerRef, !isOpen || !isMaximizedState || !(containerRef.current));
|
useHorizontalScroll(containerRef, !isOpen || !isMaximizedState || !(containerRef.current));
|
||||||
@ -456,6 +517,8 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
|
|||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
))}
|
))}
|
||||||
|
{isMoreAppsTabActive && renderMoreAppsTab()}
|
||||||
|
{!isMoreAppsTabActive && renderMoreAppsButton()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -488,7 +551,7 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
|
|||||||
round
|
round
|
||||||
color="translucent"
|
color="translucent"
|
||||||
size="tiny"
|
size="tiny"
|
||||||
ariaLabel={lang(isBackButtonVisible ? 'Back' : 'Close')}
|
ariaLabel={oldLang(isBackButtonVisible ? 'Back' : 'Close')}
|
||||||
onClick={handleBackClick}
|
onClick={handleBackClick}
|
||||||
>
|
>
|
||||||
<div className={backButtonClassName} />
|
<div className={backButtonClassName} />
|
||||||
@ -496,15 +559,6 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
|
|||||||
{renderTabs()}
|
{renderTabs()}
|
||||||
{renderMoreMenu()}
|
{renderMoreMenu()}
|
||||||
|
|
||||||
{/* <Button
|
|
||||||
round
|
|
||||||
color="translucent"
|
|
||||||
size="tiny"
|
|
||||||
>
|
|
||||||
<Icon className={styles.icon} name="add" />
|
|
||||||
</Button>
|
|
||||||
*/}
|
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
className={buildClassName(
|
className={buildClassName(
|
||||||
styles.windowStateButton,
|
styles.windowStateButton,
|
||||||
@ -534,13 +588,13 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
|
|||||||
round
|
round
|
||||||
color="translucent"
|
color="translucent"
|
||||||
size="smaller"
|
size="smaller"
|
||||||
ariaLabel={lang(isBackButtonVisible ? 'Back' : 'Close')}
|
ariaLabel={oldLang(isBackButtonVisible ? 'Back' : 'Close')}
|
||||||
onClick={handleBackClick}
|
onClick={handleBackClick}
|
||||||
>
|
>
|
||||||
<div className={backButtonClassName} />
|
<div className={backButtonClassName} />
|
||||||
</Button>
|
</Button>
|
||||||
<div className="modal-title">{attachBot?.shortName ?? bot?.firstName}</div>
|
<div className="modal-title">{attachBot?.shortName ?? bot?.firstName}</div>
|
||||||
{renderDropdownMoreMenu()}
|
{!isMoreAppsTabActive && renderDropdownMoreMenu()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -574,6 +628,7 @@ const WebAppModal: FC<OwnProps & StateProps> = ({
|
|||||||
isMultiTabSupported={supportMultiTabMode}
|
isMultiTabSupported={supportMultiTabMode}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
{ isMoreAppsTabActive && (<MoreAppsTabContent />)}
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@ -888,6 +888,43 @@ addActionHandler('closeActiveWebApp', (global, actions, payload): ActionReturnTy
|
|||||||
return global;
|
return global;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('openMoreAppsTab', (global, actions, payload): ActionReturnType => {
|
||||||
|
const { tabId = getCurrentTabId() } = payload || {};
|
||||||
|
|
||||||
|
const tabState = selectTabState(global, tabId);
|
||||||
|
global = updateTabState(global, {
|
||||||
|
webApps: {
|
||||||
|
...tabState.webApps,
|
||||||
|
activeWebApp: undefined,
|
||||||
|
isMoreAppsTabActive: true,
|
||||||
|
},
|
||||||
|
}, tabId);
|
||||||
|
|
||||||
|
return global;
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('closeMoreAppsTab', (global, actions, payload): ActionReturnType => {
|
||||||
|
const { tabId = getCurrentTabId() } = payload || {};
|
||||||
|
|
||||||
|
const tabState = selectTabState(global, tabId);
|
||||||
|
|
||||||
|
const openedWebApps = tabState.webApps.openedWebApps;
|
||||||
|
|
||||||
|
const openedWebAppsValues = Object.values(openedWebApps);
|
||||||
|
const openedWebAppsCount = openedWebAppsValues.length;
|
||||||
|
|
||||||
|
global = updateTabState(global, {
|
||||||
|
webApps: {
|
||||||
|
...tabState.webApps,
|
||||||
|
isMoreAppsTabActive: false,
|
||||||
|
activeWebApp: openedWebAppsCount ? openedWebAppsValues[openedWebAppsCount - 1] : undefined,
|
||||||
|
isModalOpen: openedWebAppsCount > 0,
|
||||||
|
},
|
||||||
|
}, tabId);
|
||||||
|
|
||||||
|
return global;
|
||||||
|
});
|
||||||
|
|
||||||
addActionHandler('closeWebApp', (global, actions, payload): ActionReturnType => {
|
addActionHandler('closeWebApp', (global, actions, payload): ActionReturnType => {
|
||||||
const { webApp, skipClosingConfirmation, tabId = getCurrentTabId() } = payload || {};
|
const { webApp, skipClosingConfirmation, tabId = getCurrentTabId() } = payload || {};
|
||||||
|
|
||||||
|
|||||||
@ -346,6 +346,7 @@ export const INITIAL_TAB_STATE: TabState = {
|
|||||||
sessionKeys: [],
|
sessionKeys: [],
|
||||||
modalState: 'maximized',
|
modalState: 'maximized',
|
||||||
isModalOpen: false,
|
isModalOpen: false,
|
||||||
|
isMoreAppsTabActive: false,
|
||||||
},
|
},
|
||||||
|
|
||||||
globalSearch: {},
|
globalSearch: {},
|
||||||
|
|||||||
@ -90,6 +90,7 @@ export function activateWebAppIfOpen<T extends GlobalState>(
|
|||||||
global = updateTabState(global, {
|
global = updateTabState(global, {
|
||||||
webApps: {
|
webApps: {
|
||||||
...currentTabState.webApps,
|
...currentTabState.webApps,
|
||||||
|
isMoreAppsTabActive: false,
|
||||||
activeWebApp: newActiveWebApp,
|
activeWebApp: newActiveWebApp,
|
||||||
modalState: 'maximized',
|
modalState: 'maximized',
|
||||||
},
|
},
|
||||||
@ -120,6 +121,7 @@ export function addWebAppToOpenList<T extends GlobalState>(
|
|||||||
webApps: {
|
webApps: {
|
||||||
...currentTabState.webApps,
|
...currentTabState.webApps,
|
||||||
...makeActive && { activeWebApp: webApp },
|
...makeActive && { activeWebApp: webApp },
|
||||||
|
isMoreAppsTabActive: false,
|
||||||
isModalOpen: openModalIfNotOpen,
|
isModalOpen: openModalIfNotOpen,
|
||||||
modalState: 'maximized',
|
modalState: 'maximized',
|
||||||
openedWebApps: {
|
openedWebApps: {
|
||||||
@ -234,6 +236,7 @@ export function clearOpenedWebApps<T extends GlobalState>(
|
|||||||
webApps: {
|
webApps: {
|
||||||
...currentTabState.webApps,
|
...currentTabState.webApps,
|
||||||
activeWebApp: newActiveWebApp,
|
activeWebApp: newActiveWebApp,
|
||||||
|
isMoreAppsTabActive: false,
|
||||||
openedWebApps: webAppsNotAllowedToClose,
|
openedWebApps: webAppsNotAllowedToClose,
|
||||||
openedOrderedKeys: newOpenedKeys,
|
openedOrderedKeys: newOpenedKeys,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -697,6 +697,7 @@ export type TabState = {
|
|||||||
openedWebApps: Record<string, WebApp>;
|
openedWebApps: Record<string, WebApp>;
|
||||||
modalState : WebAppModalStateType;
|
modalState : WebAppModalStateType;
|
||||||
isModalOpen: boolean;
|
isModalOpen: boolean;
|
||||||
|
isMoreAppsTabActive: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
botTrustRequest?: {
|
botTrustRequest?: {
|
||||||
@ -3241,6 +3242,8 @@ export interface ActionPayloads {
|
|||||||
usernames: string[];
|
usernames: string[];
|
||||||
};
|
};
|
||||||
closeActiveWebApp: WithTabId | undefined;
|
closeActiveWebApp: WithTabId | undefined;
|
||||||
|
openMoreAppsTab: WithTabId | undefined;
|
||||||
|
closeMoreAppsTab: WithTabId | undefined;
|
||||||
closeWebApp: {
|
closeWebApp: {
|
||||||
webApp: WebApp;
|
webApp: WebApp;
|
||||||
skipClosingConfirmation?: boolean;
|
skipClosingConfirmation?: boolean;
|
||||||
|
|||||||
@ -1,15 +1,15 @@
|
|||||||
import { useEffect, useState } from '../lib/teact/teact';
|
import { useEffect, useState } from '../lib/teact/teact';
|
||||||
|
|
||||||
import type { ApiChat } from '../api/types';
|
import type { ApiPeer } from '../api/types';
|
||||||
import { ApiMediaFormat } from '../api/types';
|
import { ApiMediaFormat } from '../api/types';
|
||||||
|
|
||||||
import { getChatAvatarHash } from '../global/helpers';
|
import { getChatAvatarHash } from '../global/helpers';
|
||||||
import { getAverageColor, rgb2hex } from '../util/colors';
|
import { getAverageColor, rgb2hex } from '../util/colors';
|
||||||
import useMedia from './useMedia';
|
import useMedia from './useMedia';
|
||||||
|
|
||||||
function useAverageColor(chat: ApiChat, fallbackColor = '#00000000') {
|
function useAverageColor(peer: ApiPeer, fallbackColor = '#00000000') {
|
||||||
const [color, setColor] = useState(fallbackColor);
|
const [color, setColor] = useState(fallbackColor);
|
||||||
const imgBlobUrl = useMedia(getChatAvatarHash(chat), false, ApiMediaFormat.BlobUrl);
|
const imgBlobUrl = useMedia(getChatAvatarHash(peer), false, ApiMediaFormat.BlobUrl);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
(async () => {
|
(async () => {
|
||||||
|
|||||||
4
src/types/language.d.ts
vendored
4
src/types/language.d.ts
vendored
@ -1141,6 +1141,10 @@ export interface LangPair {
|
|||||||
'StarGiftAvailability': undefined;
|
'StarGiftAvailability': undefined;
|
||||||
'StarsSubscribeInfoLinkText': undefined;
|
'StarsSubscribeInfoLinkText': undefined;
|
||||||
'StarsSubscribeInfoLink': undefined;
|
'StarsSubscribeInfoLink': undefined;
|
||||||
|
'OpenApp': undefined;
|
||||||
|
'PopularApps': undefined;
|
||||||
|
'SearchApps': undefined;
|
||||||
|
'Apps': undefined;
|
||||||
'AreYouSureCloseMiniApps': undefined;
|
'AreYouSureCloseMiniApps': undefined;
|
||||||
'CloseMiniApps': undefined;
|
'CloseMiniApps': undefined;
|
||||||
'DoNotAskAgain': undefined;
|
'DoNotAskAgain': undefined;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user