diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings
index d54464dbd..79a877f21 100644
--- a/src/assets/localization/fallback.strings
+++ b/src/assets/localization/fallback.strings
@@ -1370,6 +1370,10 @@
"StarsSubscribeInfoLinkText" = "Terms of Service";
"StarsSubscribeInfoLink" = "https://telegram.org/tos/stars";
"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?";
"CloseMiniApps" = "Close Mini Apps";
"DoNotAskAgain" = "Don't ask again";
diff --git a/src/components/common/PeerBadge.tsx b/src/components/common/PeerBadge.tsx
index fd91ca7ef..a6868833b 100644
--- a/src/components/common/PeerBadge.tsx
+++ b/src/components/common/PeerBadge.tsx
@@ -18,6 +18,8 @@ type OwnProps = {
badgeIcon?: IconName;
className?: string;
badgeClassName?: string;
+ badgeIconClassName?: string;
+ textClassName?: string;
onClick?: NoneToVoidFunction;
};
@@ -28,6 +30,8 @@ const PeerBadge = ({
badgeIcon,
className,
badgeClassName,
+ badgeIconClassName,
+ textClassName,
onClick,
}: OwnProps) => {
return (
@@ -39,12 +43,12 @@ const PeerBadge = ({
{badgeText && (
- {badgeIcon && }
+ {badgeIcon && }
{badgeText}
)}
- {text && {text}
}
+ {text && {text}
}
);
};
diff --git a/src/components/left/search/LeftSearch.scss b/src/components/left/search/LeftSearch.scss
index 790f6951f..9041e2476 100644
--- a/src/components/left/search/LeftSearch.scss
+++ b/src/components/left/search/LeftSearch.scss
@@ -207,12 +207,14 @@
.Link {
float: right;
color: var(--color-links);
- font-weight: 400;
+ font-weight: 500;
margin-right: 1.5rem;
+ transition: opacity 0.15s ease-in;
- &:focus,
+ &:active,
&:hover {
- text-decoration: underline;
+ text-decoration: none;
+ opacity: 0.85;
}
}
diff --git a/src/components/modals/webApp/MinimizedWebAppModal.tsx b/src/components/modals/webApp/MinimizedWebAppModal.tsx
index 05bafb9d9..104f5bd95 100644
--- a/src/components/modals/webApp/MinimizedWebAppModal.tsx
+++ b/src/components/modals/webApp/MinimizedWebAppModal.tsx
@@ -67,7 +67,7 @@ const MinimizedWebAppModal = ({
if (!isMinimizedState) return undefined;
function renderTitle() {
- const activeTabName = activeTabBot?.firstName;
+ const activeTabName = peers.length > 0 && peers[0]?.firstName;
const title = openedTabsCount && activeTabName && openedTabsCount > 1
? `${lang('MiniAppsMoreTabs',
{
diff --git a/src/components/modals/webApp/MoreAppsTabContent.module.scss b/src/components/modals/webApp/MoreAppsTabContent.module.scss
new file mode 100644
index 000000000..001186c56
--- /dev/null
+++ b/src/components/modals/webApp/MoreAppsTabContent.module.scss
@@ -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;
+ }
+}
diff --git a/src/components/modals/webApp/MoreAppsTabContent.tsx b/src/components/modals/webApp/MoreAppsTabContent.tsx
new file mode 100644
index 000000000..b34681374
--- /dev/null
+++ b/src/components/modals/webApp/MoreAppsTabContent.tsx
@@ -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 = ({
+ foundIds,
+ recentBotIds,
+}) => {
+ const oldLang = useOldLang();
+ const lang = useLang();
+ const [shouldShowMoreMine, setShouldShowMoreMine] = useState(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 (
+
+
+ {recentBotIds && !searchQuery && (
+
+
+ {oldLang('SearchAppsMine')}
+
+ {oldLang(shouldShowMoreMine ? 'ChatList.Search.ShowLess' : 'ChatList.Search.ShowMore')}
+
+
+
+ {recentBotIds.map((id, index) => {
+ if (!shouldShowMoreMine && index >= LESS_GRID_ITEMS_AMOUNT) {
+ return undefined;
+ }
+
+ return (
+
+ );
+ })}
+
+
+ )}
+
+
+ {searchQuery ? lang('Apps') : lang('PopularApps')}
+
+
+ {filteredFoundIds && filteredFoundIds.map((id) => {
+ return (
+
+ );
+ })}
+
+
+
+ );
+};
+
+export default memo(withGlobal((global) => {
+ const globalSearch = selectTabState(global).globalSearch;
+ const foundIds = globalSearch.popularBotApps?.peerIds;
+
+ return {
+ isLoading: !foundIds && globalSearch.fetchingStatus?.botApps,
+ foundIds,
+ recentBotIds: global.topBotApps.userIds,
+ };
+})(MoreAppsTabContent));
diff --git a/src/components/modals/webApp/WebAppGridItem.module.scss b/src/components/modals/webApp/WebAppGridItem.module.scss
new file mode 100644
index 000000000..46da71fa3
--- /dev/null
+++ b/src/components/modals/webApp/WebAppGridItem.module.scss
@@ -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;
+}
diff --git a/src/components/modals/webApp/WebAppGridtem.tsx b/src/components/modals/webApp/WebAppGridtem.tsx
new file mode 100644
index 000000000..33dd431c7
--- /dev/null
+++ b/src/components/modals/webApp/WebAppGridtem.tsx
@@ -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 (
+
+ );
+}
+
+export default memo(withGlobal(
+ (global, { chatId }): StateProps => {
+ const user = selectUser(global, chatId);
+
+ return {
+ user,
+ };
+ },
+)(WebAppGridItem));
diff --git a/src/components/modals/webApp/WebAppModal.module.scss b/src/components/modals/webApp/WebAppModal.module.scss
index 28b53cfe3..5652b0d12 100644
--- a/src/components/modals/webApp/WebAppModal.module.scss
+++ b/src/components/modals/webApp/WebAppModal.module.scss
@@ -161,7 +161,7 @@
padding-left: 1rem;
padding-right: 1rem;
- font-size: 0.875rem;
+ font-size: 1rem;
font-weight: 500;
text-overflow: ellipsis;
@@ -220,6 +220,7 @@
transform: scaleX(-1);
}
+.more-apps-tab-icon,
.avatar-container {
position: relative;
display: flex;
@@ -227,6 +228,11 @@
margin-right: 0.5rem;
}
+.more-apps-tab-icon {
+ font-size: 1.5rem;
+ color: var(--color-text-secondary);
+}
+
.web-app-tab-more-menu {
z-index: 1;
position: absolute;
@@ -255,6 +261,7 @@
transform: translate(-50%, -50%);
}
+.more-apps-button,
.window-state-button,
.header-button {
width: 1.75rem !important;
diff --git a/src/components/modals/webApp/WebAppModal.tsx b/src/components/modals/webApp/WebAppModal.tsx
index 275cd86e8..5a12fbce2 100644
--- a/src/components/modals/webApp/WebAppModal.tsx
+++ b/src/components/modals/webApp/WebAppModal.tsx
@@ -26,6 +26,7 @@ import useAppLayout from '../../../hooks/useAppLayout';
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
import useDraggable from '../../../hooks/useDraggable';
import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
+import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
@@ -37,6 +38,7 @@ import Menu from '../../ui/Menu';
import MenuItem from '../../ui/MenuItem';
import Modal from '../../ui/Modal';
import MinimizedWebAppModal from './MinimizedWebAppModal';
+import MoreAppsTabContent from './MoreAppsTabContent';
import WebAppModalTabContent from './WebAppModalTabContent';
import styles from './WebAppModal.module.scss';
@@ -77,6 +79,8 @@ const WebAppModal: FC = ({
changeWebAppModalState,
openWebAppTab,
updateWebApp,
+ openMoreAppsTab,
+ closeMoreAppsTab,
} = getActions();
const maximizedStateSize = useMemo(() => {
@@ -94,7 +98,7 @@ const WebAppModal: FC = ({
}
const {
- openedWebApps, activeWebApp, openedOrderedKeys, sessionKeys,
+ openedWebApps, activeWebApp, openedOrderedKeys, sessionKeys, isMoreAppsTabActive,
} = modal || {};
const {
isBackButtonVisible, headerColor, backgroundColor, isSettingsButtonVisible,
@@ -168,7 +172,8 @@ const WebAppModal: FC = ({
}
}, [currentWidth, currentHeight, isMaximizedState, minimizedStateSize, setFrameSize]);
- const lang = useOldLang();
+ const oldLang = useOldLang();
+ const lang = useLang();
const {
queryId,
} = activeWebApp || {};
@@ -229,6 +234,10 @@ const WebAppModal: FC = ({
closeWebAppModal();
});
+ const handleCloseMoreAppsTab = useLastCallback(() => {
+ closeMoreAppsTab();
+ });
+
const handleTabClose = useLastCallback(() => {
if (openTabsCount > 1) {
closeActiveWebApp();
@@ -268,6 +277,10 @@ const WebAppModal: FC = ({
changeWebAppModalState();
});
+ const handleOpenMoreAppsTabClick = useLastCallback(() => {
+ openMoreAppsTab();
+ });
+
const handleTabClick = useLastCallback((tab: WebAppModalTab) => {
openWebAppTab({ webApp: tab.webApp });
});
@@ -304,12 +317,12 @@ const WebAppModal: FC = ({
return (
<>
{chat && bot && chat.id !== bot.id && (
-
+
)}
-
+
{isSettingsButtonVisible && (
)}
{bot?.isAttachBot && (
@@ -318,7 +331,7 @@ const WebAppModal: FC = ({
onClick={handleToggleClick}
destructive={Boolean(attachBot)}
>
- {lang(attachBot ? 'WebApp.RemoveBot' : 'WebApp.AddToAttachmentAdd')}
+ {oldLang(attachBot ? 'WebApp.RemoveBot' : 'WebApp.AddToAttachmentAdd')}
)}
>
@@ -368,12 +381,13 @@ const WebAppModal: FC = ({
);
const headerTextVar = useMemo(() => {
+ if (isMoreAppsTabActive) return 'color-text';
if (!headerColor) return undefined;
const { r, g, b } = hexToRgb(headerColor);
const luma = getColorLuma([r, g, b]);
const adaptedLuma = theme === 'dark' ? 255 - luma : luma;
return adaptedLuma > LUMA_THRESHOLD ? 'color-text' : 'color-background';
- }, [headerColor, theme]);
+ }, [headerColor, theme, isMoreAppsTabActive]);
function renderTabCurveBorder(className: string) {
return (
@@ -422,7 +436,7 @@ const WebAppModal: FC = ({
round
color="translucent"
size="tiny"
- ariaLabel={lang('Close')}
+ ariaLabel={oldLang('Close')}
onClick={handleTabClose}
>
@@ -433,6 +447,53 @@ const WebAppModal: FC = ({
);
}
+ function renderMoreAppsTab() {
+ return (
+
+ {renderTabCurveBorder(styles.tabButtonLeftCurve)}
+
+
+
+
+ {lang('OpenApp')}
+
+
+
+ {renderTabCurveBorder(styles.tabButtonRightCurve)}
+
+ );
+ }
+
+ function renderMoreAppsButton() {
+ return (
+
+ );
+ }
+
// eslint-disable-next-line no-null/no-null
const containerRef = useRef(null);
useHorizontalScroll(containerRef, !isOpen || !isMaximizedState || !(containerRef.current));
@@ -456,6 +517,8 @@ const WebAppModal: FC = ({
/>
)
))}
+ {isMoreAppsTabActive && renderMoreAppsTab()}
+ {!isMoreAppsTabActive && renderMoreAppsButton()}
);
}
@@ -488,7 +551,7 @@ const WebAppModal: FC = ({
round
color="translucent"
size="tiny"
- ariaLabel={lang(isBackButtonVisible ? 'Back' : 'Close')}
+ ariaLabel={oldLang(isBackButtonVisible ? 'Back' : 'Close')}
onClick={handleBackClick}
>
@@ -496,15 +559,6 @@ const WebAppModal: FC = ({
{renderTabs()}
{renderMoreMenu()}
- {/*
- */}
-
{attachBot?.shortName ?? bot?.firstName}
- {renderDropdownMoreMenu()}
+ {!isMoreAppsTabActive && renderDropdownMoreMenu()}
);
}
@@ -574,6 +628,7 @@ const WebAppModal: FC = ({
isMultiTabSupported={supportMultiTabMode}
/>
))}
+ { isMoreAppsTabActive && ()}
);
};
diff --git a/src/global/actions/api/bots.ts b/src/global/actions/api/bots.ts
index aed18b1d2..763d03ede 100644
--- a/src/global/actions/api/bots.ts
+++ b/src/global/actions/api/bots.ts
@@ -888,6 +888,43 @@ addActionHandler('closeActiveWebApp', (global, actions, payload): ActionReturnTy
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 => {
const { webApp, skipClosingConfirmation, tabId = getCurrentTabId() } = payload || {};
diff --git a/src/global/initialState.ts b/src/global/initialState.ts
index d31a21fd8..b0bca22d2 100644
--- a/src/global/initialState.ts
+++ b/src/global/initialState.ts
@@ -346,6 +346,7 @@ export const INITIAL_TAB_STATE: TabState = {
sessionKeys: [],
modalState: 'maximized',
isModalOpen: false,
+ isMoreAppsTabActive: false,
},
globalSearch: {},
diff --git a/src/global/reducers/bots.ts b/src/global/reducers/bots.ts
index c417d112b..f310b1804 100644
--- a/src/global/reducers/bots.ts
+++ b/src/global/reducers/bots.ts
@@ -90,6 +90,7 @@ export function activateWebAppIfOpen(
global = updateTabState(global, {
webApps: {
...currentTabState.webApps,
+ isMoreAppsTabActive: false,
activeWebApp: newActiveWebApp,
modalState: 'maximized',
},
@@ -120,6 +121,7 @@ export function addWebAppToOpenList(
webApps: {
...currentTabState.webApps,
...makeActive && { activeWebApp: webApp },
+ isMoreAppsTabActive: false,
isModalOpen: openModalIfNotOpen,
modalState: 'maximized',
openedWebApps: {
@@ -234,6 +236,7 @@ export function clearOpenedWebApps(
webApps: {
...currentTabState.webApps,
activeWebApp: newActiveWebApp,
+ isMoreAppsTabActive: false,
openedWebApps: webAppsNotAllowedToClose,
openedOrderedKeys: newOpenedKeys,
},
diff --git a/src/global/types.ts b/src/global/types.ts
index 4f2435d18..412b0ba4d 100644
--- a/src/global/types.ts
+++ b/src/global/types.ts
@@ -697,6 +697,7 @@ export type TabState = {
openedWebApps: Record;
modalState : WebAppModalStateType;
isModalOpen: boolean;
+ isMoreAppsTabActive: boolean;
};
botTrustRequest?: {
@@ -3241,6 +3242,8 @@ export interface ActionPayloads {
usernames: string[];
};
closeActiveWebApp: WithTabId | undefined;
+ openMoreAppsTab: WithTabId | undefined;
+ closeMoreAppsTab: WithTabId | undefined;
closeWebApp: {
webApp: WebApp;
skipClosingConfirmation?: boolean;
diff --git a/src/hooks/useAverageColor.ts b/src/hooks/useAverageColor.ts
index d9b92de13..dc03145bc 100644
--- a/src/hooks/useAverageColor.ts
+++ b/src/hooks/useAverageColor.ts
@@ -1,15 +1,15 @@
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 { getChatAvatarHash } from '../global/helpers';
import { getAverageColor, rgb2hex } from '../util/colors';
import useMedia from './useMedia';
-function useAverageColor(chat: ApiChat, fallbackColor = '#00000000') {
+function useAverageColor(peer: ApiPeer, fallbackColor = '#00000000') {
const [color, setColor] = useState(fallbackColor);
- const imgBlobUrl = useMedia(getChatAvatarHash(chat), false, ApiMediaFormat.BlobUrl);
+ const imgBlobUrl = useMedia(getChatAvatarHash(peer), false, ApiMediaFormat.BlobUrl);
useEffect(() => {
(async () => {
diff --git a/src/types/language.d.ts b/src/types/language.d.ts
index f21eb0a31..fb7a1c151 100644
--- a/src/types/language.d.ts
+++ b/src/types/language.d.ts
@@ -1141,6 +1141,10 @@ export interface LangPair {
'StarGiftAvailability': undefined;
'StarsSubscribeInfoLinkText': undefined;
'StarsSubscribeInfoLink': undefined;
+ 'OpenApp': undefined;
+ 'PopularApps': undefined;
+ 'SearchApps': undefined;
+ 'Apps': undefined;
'AreYouSureCloseMiniApps': undefined;
'CloseMiniApps': undefined;
'DoNotAskAgain': undefined;