Search: Display popular bot apps (#4864)

This commit is contained in:
zubiden 2024-08-29 15:52:28 +02:00 committed by Alexander Zinchuk
parent 9daa5f1a19
commit a4b33eb43a
37 changed files with 427 additions and 164 deletions

View File

@ -60,6 +60,7 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined {
const {
id, firstName, lastName, fake, scam, support, closeFriend, storiesUnavailable, storiesMaxId,
bot, botActiveUsers, botInlinePlaceholder, botAttachMenu, botCanEdit,
} = mtpUser;
const hasVideoAvatar = mtpUser.photo instanceof GramJs.UserProfilePhoto ? Boolean(mtpUser.photo.hasVideo) : undefined;
const avatarPhotoId = mtpUser.photo && buildAvatarPhotoId(mtpUser.photo);
@ -80,7 +81,7 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined {
type: userType,
firstName,
lastName,
canEditBot: Boolean(mtpUser.botCanEdit),
canEditBot: botCanEdit,
...(userType === 'userTypeBot' && { canBeInvitedToGroup: !mtpUser.botNochats }),
...(usernames && { usernames }),
phoneNumber: mtpUser.phone || '',
@ -92,8 +93,9 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined {
areStoriesHidden: Boolean(mtpUser.storiesHidden),
maxStoryId: storiesMaxId,
hasStories: Boolean(storiesMaxId) && !storiesUnavailable,
...(mtpUser.bot && mtpUser.botInlinePlaceholder && { botPlaceholder: mtpUser.botInlinePlaceholder }),
...(mtpUser.bot && mtpUser.botAttachMenu && { isAttachBot: mtpUser.botAttachMenu }),
...(bot && botInlinePlaceholder && { botPlaceholder: botInlinePlaceholder }),
...(bot && botAttachMenu && { isAttachBot: botAttachMenu }),
botActiveUsers,
color: mtpUser.color && buildApiPeerColor(mtpUser.color),
};
}

View File

@ -82,6 +82,24 @@ export async function fetchTopInlineBots() {
};
}
export async function fetchTopBotApps() {
const topPeers = await invokeRequest(new GramJs.contacts.GetTopPeers({
botsApp: true,
}));
if (!(topPeers instanceof GramJs.contacts.TopPeers)) {
return undefined;
}
const users = topPeers.users.map(buildApiUser).filter(Boolean);
const ids = users.map(({ id }) => id);
return {
ids,
users,
};
}
export async function fetchInlineBot({ username }: { username: string }) {
const resolvedPeer = await invokeRequest(new GramJs.contacts.ResolveUsername({ username }));
@ -582,3 +600,30 @@ export function setBotInfo({
shouldReturnTrue: true,
});
}
export async function fetchPopularAppBots({
offset = '', limit,
}: {
offset?: string;
limit?: number;
}) {
const result = await invokeRequest(new GramJs.bots.GetPopularAppBots({
offset,
limit,
}));
if (!result) {
return undefined;
}
addEntitiesToLocalDb(result.users);
const users = result.users.map(buildApiUser).filter(Boolean);
const chats = result.users.map((c) => buildApiChatFromPreview(c)).filter(Boolean);
return {
users,
chats,
nextOffset: result.nextOffset,
};
}

View File

@ -66,7 +66,7 @@ export {
export {
answerCallbackButton, setBotInfo, fetchTopInlineBots, fetchInlineBot, fetchInlineBotResults,
sendInlineBotResult, startBot,
sendInlineBotResult, startBot, fetchPopularAppBots, fetchTopBotApps,
requestWebView, requestSimpleWebView, sendWebViewData, prolongWebView, loadAttachBots, toggleAttachBot, fetchBotApp,
requestBotUrlAuth, requestLinkUrlAuth, acceptBotUrlAuth, acceptLinkUrlAuth, loadAttachBot, requestAppWebView,
allowBotSendMessages, fetchBotCanSendMessage, invokeWebViewCustomMethod,

View File

@ -40,6 +40,7 @@ export interface ApiUser {
maxStoryId?: number;
color?: ApiPeerColor;
canEditBot?: boolean;
botActiveUsers?: number;
}
export interface ApiUserFullInfo {

View File

@ -462,6 +462,7 @@
}
.bot-menu-text {
--emoji-size: 1rem;
font-size: 0.875rem;
font-weight: 500;
white-space: nowrap;

View File

@ -29,8 +29,6 @@
.TabList {
justify-content: flex-start;
padding-left: 0.5625rem;
padding-bottom: 1px;
border-bottom: 0;
z-index: 1;
@ -45,15 +43,6 @@
.Tab {
flex: 0 0 auto;
padding-left: 0.625rem;
padding-right: 0.625rem;
/* stylelint-disable-next-line */
> span {
padding-left: 0.5rem;
padding-right: 0.5rem;
white-space: pre;
}
}
> .Transition {

View File

@ -8,7 +8,6 @@ import { AudioOrigin, LoadMoreDirection } from '../../../types';
import { SLIDE_TRANSITION_DURATION } from '../../../config';
import { getIsDownloading, getMessageDownloadableMedia } from '../../../global/helpers';
import buildClassName from '../../../util/buildClassName';
import { formatMonthAndYear, toYearMonth } from '../../../util/dates/dateFormat';
import { parseSearchResultKey } from '../../../util/keys/searchResultKey';
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
@ -58,7 +57,7 @@ const AudioResults: FC<OwnProps & StateProps> = ({
});
});
}
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps -- `searchQuery` is required to prevent infinite message loading
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps -- `searchQuery` is required to prevent infinite message loading
}, [currentType, searchMessagesGlobal, searchQuery]);
const foundMessages = useMemo(() => {
@ -89,36 +88,35 @@ const AudioResults: FC<OwnProps & StateProps> = ({
const media = getMessageDownloadableMedia(message)!;
return (
<div
className="ListItem small-icon"
key={message.id}
>
<>
{shouldDrawDateDivider && (
<p
className={buildClassName(
'section-heading',
isFirst && 'section-heading-first',
!isFirst && 'section-heading-with-border',
)}
className="section-heading"
key={message.date}
dir={lang.isRtl ? 'rtl' : undefined}
>
{formatMonthAndYear(lang, new Date(message.date * 1000))}
</p>
)}
<Audio
<div
className="ListItem small-icon"
key={message.id}
theme={theme}
message={message}
origin={AudioOrigin.Search}
senderTitle={getSenderName(lang, message, chatsById, usersById)}
date={message.date}
className="scroll-item"
onPlay={handlePlayAudio}
onDateClick={handleMessageFocus}
canDownload={!chatsById[message.chatId]?.isProtected && !message.isProtected}
isDownloading={getIsDownloading(activeDownloads, media)}
/>
</div>
>
<Audio
key={message.id}
theme={theme}
message={message}
origin={AudioOrigin.Search}
senderTitle={getSenderName(lang, message, chatsById, usersById)}
date={message.date}
className="scroll-item"
onPlay={handlePlayAudio}
onDateClick={handleMessageFocus}
canDownload={!chatsById[message.chatId]?.isProtected && !message.isProtected}
isDownloading={getIsDownloading(activeDownloads, media)}
/>
</div>
</>
);
});
}
@ -126,7 +124,7 @@ const AudioResults: FC<OwnProps & StateProps> = ({
const canRenderContents = useAsyncRendering([searchQuery], SLIDE_TRANSITION_DURATION) && !isLoading;
return (
<div className="LeftSearch">
<div className="LeftSearch--content">
<InfiniteScroll
className="search-content documents-list custom-scroll"
items={foundMessages}

View File

@ -0,0 +1,153 @@
import type { FC } from '../../../lib/teact/teact';
import React, {
memo, useCallback, useMemo, useRef,
useState,
} from '../../../lib/teact/teact';
import { getActions, getGlobal, withGlobal } from '../../../global';
import { LoadMoreDirection } from '../../../types';
import { SLIDE_TRANSITION_DURATION } from '../../../config';
import { filterUsersByName } from '../../../global/helpers';
import { selectTabState } from '../../../global/selectors';
import { throttle } from '../../../util/schedulers';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import useAsyncRendering from '../../right/hooks/useAsyncRendering';
import NothingFound from '../../common/NothingFound';
import InfiniteScroll from '../../ui/InfiniteScroll';
import Link from '../../ui/Link';
import Loading from '../../ui/Loading';
import LeftSearchResultChat from './LeftSearchResultChat';
export type OwnProps = {
searchQuery?: string;
};
type StateProps = {
isSynced?: boolean;
isLoading?: boolean;
foundIds?: string[];
recentBotIds?: string[];
};
const LESS_LIST_ITEMS_AMOUNT = 5;
const runThrottled = throttle((cb) => cb(), 500, true);
const BotAppResults: FC<OwnProps & StateProps> = ({
searchQuery,
isSynced,
isLoading,
foundIds,
recentBotIds,
}) => {
const {
searchPopularBotApps,
openChatWithInfo,
} = getActions();
// eslint-disable-next-line no-null/no-null
const containerRef = useRef<HTMLDivElement>(null);
const lang = useOldLang();
const [shouldShowMoreMine, setShouldShowMoreMine] = useState<boolean>(false);
const filteredFoundIds = useMemo(() => {
if (!foundIds) return [];
const recentSet = new Set(recentBotIds);
const withoutRecent = foundIds.filter((id) => !recentSet.has(id));
const usersById = getGlobal().users.byId;
return filterUsersByName(withoutRecent, usersById, searchQuery);
}, [foundIds, recentBotIds, searchQuery]);
const handleChatClick = useLastCallback((id: string) => {
openChatWithInfo({ id, shouldReplaceHistory: true });
});
const handleLoadMore = useCallback(({ direction }: { direction: LoadMoreDirection }) => {
if (!isSynced) return;
if (direction === LoadMoreDirection.Backwards) {
runThrottled(() => {
searchPopularBotApps();
});
}
}, [isSynced]);
const handleToggleShowMoreMine = useLastCallback(() => {
setShouldShowMoreMine((prev) => !prev);
});
const canRenderContents = useAsyncRendering([searchQuery], SLIDE_TRANSITION_DURATION) && !isLoading;
return (
<div ref={containerRef} className="LeftSearch--content">
<InfiniteScroll
className="search-content custom-scroll"
items={filteredFoundIds}
onLoadMore={handleLoadMore}
noFastList
>
{!canRenderContents && <Loading />}
{canRenderContents && !filteredFoundIds?.length && (
<NothingFound
text={lang('ChatList.Search.NoResults')}
description={lang('ChatList.Search.NoResultsDescription')}
/>
)}
{canRenderContents && !searchQuery && recentBotIds?.length && (
<div className="search-section">
<h3 className="section-heading">
{recentBotIds.length > LESS_LIST_ITEMS_AMOUNT && (
<Link className="Link" onClick={handleToggleShowMoreMine}>
{lang(shouldShowMoreMine ? 'ChatList.Search.ShowLess' : 'ChatList.Search.ShowMore')}
</Link>
)}
{lang('SearchAppsMine')}
</h3>
{recentBotIds.map((id, index) => {
if (!shouldShowMoreMine && index >= LESS_LIST_ITEMS_AMOUNT) {
return undefined;
}
return (
<LeftSearchResultChat
chatId={id}
onClick={handleChatClick}
/>
);
})}
</div>
)}
{canRenderContents && filteredFoundIds?.length && (
<div className="search-section">
<h3 className="section-heading">{lang('SearchAppsPopular')}</h3>
{filteredFoundIds.map((id) => {
return (
<LeftSearchResultChat
chatId={id}
onClick={handleChatClick}
/>
);
})}
</div>
)}
</InfiniteScroll>
</div>
);
};
export default memo(withGlobal<OwnProps>((global) => {
const globalSearch = selectTabState(global).globalSearch;
const foundIds = globalSearch.popularBotApps?.peerIds;
return {
isSynced: global.isSynced,
isLoading: !foundIds && globalSearch.fetchingStatus?.botApps,
foundIds,
recentBotIds: global.topBotApps.userIds,
};
})(BotAppResults));

View File

@ -115,7 +115,7 @@ const ChatMessageResults: FC<OwnProps & StateProps> = ({
&& !foundTopicIds?.length;
return (
<div className="LeftSearch">
<div className="LeftSearch--content">
<InfiniteScroll
className="search-content custom-scroll chat-list"
items={foundMessages}

View File

@ -238,7 +238,7 @@ const ChatResults: FC<OwnProps & StateProps> = ({
return (
<InfiniteScroll
className="LeftSearch custom-scroll"
className="LeftSearch--content custom-scroll"
items={foundMessages}
onLoadMore={handleLoadMore}
// To prevent scroll jumps caused by delayed local results rendering

View File

@ -10,7 +10,6 @@ import { LoadMoreDirection } from '../../../types';
import { SLIDE_TRANSITION_DURATION } from '../../../config';
import { getIsDownloading, getMessageDocument } from '../../../global/helpers';
import buildClassName from '../../../util/buildClassName';
import { formatMonthAndYear, toYearMonth } from '../../../util/dates/dateFormat';
import { parseSearchResultKey } from '../../../util/keys/searchResultKey';
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
@ -69,7 +68,7 @@ const FileResults: FC<OwnProps & StateProps> = ({
});
});
}
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps -- `searchQuery` is required to prevent infinite message loading
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps -- `searchQuery` is required to prevent infinite message loading
}, [searchQuery]);
const foundMessages = useMemo(() => {
@ -95,36 +94,35 @@ const FileResults: FC<OwnProps & StateProps> = ({
const shouldDrawDateDivider = isFirst
|| toYearMonth(message.date) !== toYearMonth(foundMessages[index - 1].date);
return (
<div
className="ListItem small-icon"
key={message.id}
>
<>
{shouldDrawDateDivider && (
<p
className={buildClassName(
'section-heading',
isFirst && 'section-heading-first',
!isFirst && 'section-heading-with-border',
)}
className="section-heading"
dir={lang.isRtl ? 'rtl' : undefined}
key={message.date}
>
{formatMonthAndYear(lang, new Date(message.date * 1000))}
</p>
)}
<Document
document={getMessageDocument(message)!}
message={message}
withDate
datetime={message.date}
smaller
sender={getSenderName(lang, message, chatsById, usersById)}
className="scroll-item"
isDownloading={getIsDownloading(activeDownloads, message.content.document!)}
shouldWarnAboutSvg={shouldWarnAboutSvg}
observeIntersection={observeIntersectionForMedia}
onDateClick={handleMessageFocus}
/>
</div>
<div
className="ListItem small-icon"
key={message.id}
>
<Document
document={getMessageDocument(message)!}
message={message}
withDate
datetime={message.date}
smaller
sender={getSenderName(lang, message, chatsById, usersById)}
className="scroll-item"
isDownloading={getIsDownloading(activeDownloads, message.content.document!)}
shouldWarnAboutSvg={shouldWarnAboutSvg}
observeIntersection={observeIntersectionForMedia}
onDateClick={handleMessageFocus}
/>
</div>
</>
);
});
}
@ -132,7 +130,7 @@ const FileResults: FC<OwnProps & StateProps> = ({
const canRenderContents = useAsyncRendering([searchQuery], SLIDE_TRANSITION_DURATION) && !isLoading;
return (
<div ref={containerRef} className="LeftSearch">
<div ref={containerRef} className="LeftSearch--content">
<InfiniteScroll
className="search-content documents-list custom-scroll"
items={foundMessages}

View File

@ -1,3 +1,5 @@
@use "../../../styles/mixins";
.LeftSearch {
display: flex;
flex-direction: column;
@ -10,23 +12,36 @@
}
.TabList {
padding-bottom: 1px;
z-index: 1;
}
&--content {
padding-top: 0.5rem;
overflow-y: auto;
}
&--media {
padding-top: 0;
}
.documents-list {
padding: 0 1.25rem 1.25rem;
padding-bottom: 1.25rem;
padding-left: 0.75rem;
.ListItem {
padding: 0.625rem 0;
padding-inline: 0.5rem;
}
.ListItem + .ListItem {
padding-block: 0.5rem;
}
}
.section-heading {
position: relative;
padding-top: 0.25rem;
padding-left: 1.25rem;
margin: 0 0 0.5rem -1.25rem !important;
padding-top: 0.625rem;
padding-left: 1.75rem;
margin: 0 0 1rem -1.25rem !important;
font-weight: 500;
font-size: 0.9375rem;
@ -36,16 +51,6 @@
padding-top: 0.75rem;
}
&-with-border::before {
content: "";
position: absolute;
width: calc(100% + 1.25rem);
height: 1px;
background: var(--color-borders);
left: 0;
top: 0;
}
&[dir="rtl"],
&[dir="auto"] {
padding-left: 0;
@ -68,9 +73,8 @@
.LeftSearch .search-section .section-heading,
.RecentContacts .search-section .section-heading {
margin-left: -0.5rem !important;
padding-left: 1.5rem;
padding-left: 1.125rem;
width: calc(100% + 0.625rem);
box-shadow: 0 -1px 0 0 var(--color-borders);
&::before {
display: none;
@ -104,6 +108,9 @@
.media-list {
display: grid;
padding: 0.5rem;
@include mixins.adapt-padding-to-scrollbar(0.5rem);
grid-template-columns: repeat(3, 1fr);
grid-auto-rows: 1fr;
grid-gap: 0.25rem;
@ -177,27 +184,20 @@
}
}
@media (max-width: 600px) {
.ListItem {
margin: 0 -0.125rem 0 -0.5rem;
}
}
.search-section {
padding: 0 0.5rem 0.5rem 0.5rem;
padding: 0 0.125rem 0 1rem;
.section-heading {
color: var(--color-text-secondary);
font-size: 0.9375rem;
font-weight: 500;
margin-bottom: 0 !important;
padding-top: 0.875rem;
margin-bottom: 0.625rem !important;
.Link {
float: right;
color: var(--color-links);
font-weight: 400;
margin-right: 1rem;
margin-right: 1.5rem;
&:focus,
&:hover {
@ -222,12 +222,14 @@
}
.chat-selection {
padding-block: 0.5rem;
display: flex;
flex-shrink: 0;
flex-wrap: nowrap;
justify-content: space-between;
align-items: flex-end;
margin-bottom: 0.5rem;
box-shadow: inset 0 -1px 0 0 var(--color-borders);
background-color: var(--color-background);
-webkit-overflow-scrolling: touch;

View File

@ -20,6 +20,7 @@ import useOldLang from '../../../hooks/useOldLang';
import TabList from '../../ui/TabList';
import Transition from '../../ui/Transition';
import AudioResults from './AudioResults';
import BotAppResults from './BotAppResults';
import ChatMessageResults from './ChatMessageResults';
import ChatResults from './ChatResults';
import FileResults from './FileResults';
@ -43,6 +44,7 @@ type StateProps = {
const TABS = [
{ type: GlobalSearchContent.ChatList, title: 'SearchAllChatsShort' },
{ type: GlobalSearchContent.ChannelList, title: 'ChannelsTab' },
{ type: GlobalSearchContent.BotApps, title: 'AppsTab' },
{ type: GlobalSearchContent.Media, title: 'SharedMediaTab2' },
{ type: GlobalSearchContent.Links, title: 'SharedLinksTab2' },
{ type: GlobalSearchContent.Files, title: 'SharedFilesTab2' },
@ -52,7 +54,7 @@ const TABS = [
const CHAT_TABS = [
{ type: GlobalSearchContent.ChatList, title: 'All Messages' },
...TABS.slice(2), // Skip ChatList and ChannelList, replaced with All Messages
...TABS.slice(3), // Skip ChatList, ChannelList and BotApps, replaced with All Messages
];
const LeftSearch: FC<OwnProps & StateProps> = ({
@ -146,6 +148,13 @@ const LeftSearch: FC<OwnProps & StateProps> = ({
searchQuery={searchQuery}
/>
);
case GlobalSearchContent.BotApps:
return (
<BotAppResults
key="botApps"
searchQuery={searchQuery}
/>
);
default:
return undefined;
}

View File

@ -5,7 +5,7 @@ import { withGlobal } from '../../../global';
import type { ApiChat, ApiUser } from '../../../api/types';
import { StoryViewerOrigin } from '../../../types';
import { getPrivateChatUserId, isUserId, selectIsChatMuted } from '../../../global/helpers';
import { isUserId, selectIsChatMuted } from '../../../global/helpers';
import {
selectChat, selectIsChatPinned, selectNotifyExceptions,
selectNotifySettings, selectUser,
@ -76,10 +76,6 @@ const LeftSearchResultChat: FC<OwnProps & StateProps> = ({
const buttonRef = useSelectWithEnter(handleClick);
if (!chat) {
return undefined;
}
return (
<ListItem
className="chat-item-clickable search-result"
@ -92,14 +88,14 @@ const LeftSearchResultChat: FC<OwnProps & StateProps> = ({
userId={chatId}
withUsername={withUsername}
withStory
avatarSize="large"
avatarSize="medium"
storyViewerOrigin={StoryViewerOrigin.SearchResult}
/>
) : (
<GroupChatInfo
chatId={chatId}
withUsername={withUsername}
avatarSize="large"
avatarSize="medium"
withStory
storyViewerOrigin={StoryViewerOrigin.SearchResult}
/>
@ -127,8 +123,7 @@ const LeftSearchResultChat: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>(
(global, { chatId }): StateProps => {
const chat = selectChat(global, chatId);
const privateChatUserId = chat && getPrivateChatUserId(chat);
const user = privateChatUserId ? selectUser(global, privateChatUserId) : undefined;
const user = selectUser(global, chatId);
const isPinned = selectIsChatPinned(global, chatId);
const isMuted = chat
? selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global))

View File

@ -9,7 +9,6 @@ import type { StateProps } from './helpers/createMapStateToProps';
import { LoadMoreDirection } from '../../../types';
import { SLIDE_TRANSITION_DURATION } from '../../../config';
import buildClassName from '../../../util/buildClassName';
import { formatMonthAndYear, toYearMonth } from '../../../util/dates/dateFormat';
import { parseSearchResultKey } from '../../../util/keys/searchResultKey';
import { MEMO_EMPTY_ARRAY } from '../../../util/memo';
@ -92,32 +91,31 @@ const LinkResults: FC<OwnProps & StateProps> = ({
const shouldDrawDateDivider = isFirst
|| toYearMonth(message.date) !== toYearMonth(foundMessages[index - 1].date);
return (
<div
className="ListItem small-icon"
dir={lang.isRtl ? 'rtl' : undefined}
key={message.id}
>
<>
{shouldDrawDateDivider && (
<p
className={buildClassName(
'section-heading',
isFirst && 'section-heading-first',
!isFirst && 'section-heading-with-border',
)}
className="section-heading"
key={message.date}
dir={lang.isRtl ? 'rtl' : undefined}
>
{formatMonthAndYear(lang, new Date(message.date * 1000))}
</p>
)}
<WebLink
<div
className="ListItem small-icon"
dir={lang.isRtl ? 'rtl' : undefined}
key={message.id}
message={message}
senderTitle={getSenderName(lang, message, chatsById, usersById)}
isProtected={isChatProtected || message.isProtected}
observeIntersection={observeIntersectionForMedia}
onMessageClick={handleMessageFocus}
/>
</div>
>
<WebLink
key={message.id}
message={message}
senderTitle={getSenderName(lang, message, chatsById, usersById)}
isProtected={isChatProtected || message.isProtected}
observeIntersection={observeIntersectionForMedia}
onMessageClick={handleMessageFocus}
/>
</div>
</>
);
});
}
@ -125,7 +123,7 @@ const LinkResults: FC<OwnProps & StateProps> = ({
const canRenderContents = useAsyncRendering([searchQuery], SLIDE_TRANSITION_DURATION) && !isLoading;
return (
<div ref={containerRef} className="LeftSearch">
<div ref={containerRef} className="LeftSearch--content">
<InfiniteScroll
className="search-content documents-list custom-scroll"
items={foundMessages}

View File

@ -123,7 +123,7 @@ const MediaResults: FC<OwnProps & StateProps> = ({
);
return (
<div ref={containerRef} className="LeftSearch">
<div ref={containerRef} className="LeftSearch--content LeftSearch--media">
<InfiniteScroll
className={classNames}
items={foundMessages}

View File

@ -77,16 +77,10 @@
.recent-chats-header {
display: flex;
align-items: center;
}
.Button {
margin-left: auto;
}
&[dir="rtl"] {
.Button {
margin-left: 0;
margin-right: auto;
}
}
.clear-recent-chats {
position: absolute;
inset-inline-end: 0.5rem;
}
}

View File

@ -105,10 +105,11 @@ const RecentContacts: FC<OwnProps & StateProps> = ({
{lang('Recent')}
<Button
className="clear-recent-chats"
round
size="smaller"
color="translucent"
ariaLabel="Clear recent chats"
ariaLabel={lang('Clear')}
onClick={handleClearRecentlyFoundChats}
isRtl={lang.isRtl}
>

View File

@ -259,6 +259,7 @@ const Main: FC<OwnProps & StateProps> = ({
loadQuickReplies,
loadStarStatus,
loadAvailableEffects,
loadTopBotApps,
} = getActions();
if (DEBUG && !DEBUG_isLogged) {
@ -341,6 +342,7 @@ const Main: FC<OwnProps & StateProps> = ({
loadGenericEmojiEffects();
loadSavedReactionTags();
loadAuthorizations();
loadTopBotApps();
}
}, [isMasterTab, isSynced]);

View File

@ -2,6 +2,7 @@ import type { FC } from '../../../lib/teact/teact';
import React, { memo, useEffect, useRef } from '../../../lib/teact/teact';
import buildClassName from '../../../util/buildClassName';
import renderText from '../../common/helpers/renderText';
import Button from '../../ui/Button';
@ -52,7 +53,7 @@ const BotMenuButton: FC<OwnProps> = ({
ariaLabel="Open bot command keyboard"
>
<i className={buildClassName('bot-menu-icon', 'icon', 'icon-webapp', isOpen && 'open')} />
<span ref={textRef} className="bot-menu-text">{text}</span>
<span ref={textRef} className="bot-menu-text">{renderText(text)}</span>
</Button>
);
};

View File

@ -46,18 +46,6 @@
background: var(--color-background);
top: -1px;
z-index: 1;
.Tab {
padding: 1rem 0.75rem;
&_inner {
white-space: nowrap;
}
.platform {
bottom: -1rem;
}
}
}
.Transition {

View File

@ -225,7 +225,7 @@ function getNodes(origin: StoryViewerOrigin, userId: string) {
containerSelector = '#LeftColumn .chat-list';
break;
case StoryViewerOrigin.SearchResult:
containerSelector = '#LeftColumn .LeftSearch';
containerSelector = '#LeftColumn .LeftSearch--container';
break;
}

View File

@ -223,7 +223,7 @@
}
.ListItem-button {
padding: 0.5625rem;
padding: 0.375rem;
}
&.contact-list-item {
@ -237,7 +237,7 @@
}
.Avatar {
margin-right: 0.5rem;
margin-right: 0.75rem;
}
.info {
@ -270,6 +270,7 @@
.typing-status {
font-size: 1rem;
margin: 0;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
text-align: initial;

View File

@ -7,7 +7,7 @@
width: auto;
margin: 0;
border: none;
padding: 0.625rem 0.25rem;
padding: 0.625rem 1.125rem;
font-weight: 500;
color: var(--color-text-secondary);
border-top-left-radius: var(--border-radius-messages-small);
@ -44,7 +44,7 @@
}
}
> span {
.Tab_inner {
position: relative;
display: flex;
align-items: center;
@ -85,7 +85,7 @@
.platform {
position: absolute;
bottom: calc(-0.625rem - 1px);
bottom: -0.625rem;
left: 0;
opacity: 0;
background-color: var(--color-primary);

View File

@ -5,13 +5,16 @@
display: flex;
justify-content: space-between;
align-items: flex-end;
font-size: 0.875rem;
flex-wrap: nowrap;
box-shadow: 0 2px 2px var(--color-light-shadow);
background-color: var(--color-background);
overflow-x: auto;
overflow-y: hidden;
font-size: 0.875rem;
padding-inline: 0.5rem;
scrollbar-width: none;
scrollbar-color: rgba(0, 0, 0, 0);

View File

@ -261,6 +261,32 @@ addActionHandler('loadTopInlineBots', async (global): Promise<void> => {
setGlobal(global);
});
addActionHandler('loadTopBotApps', async (global): Promise<void> => {
const { lastRequestedAt } = global.topBotApps;
if (lastRequestedAt && getServerTime() - lastRequestedAt < TOP_PEERS_REQUEST_COOLDOWN) {
return;
}
const result = await callApi('fetchTopBotApps');
if (!result) {
return;
}
const { ids, users } = result;
global = getGlobal();
global = addUsers(global, buildCollectionByKey(users, 'id'));
global = {
...global,
topBotApps: {
...global.topBotApps,
userIds: ids,
lastRequestedAt: getServerTime(),
},
};
setGlobal(global);
});
addActionHandler('queryInlineBot', async (global, actions, payload): Promise<void> => {
const {
chatId, username, query, offset,

View File

@ -112,6 +112,38 @@ addActionHandler('searchMessagesGlobal', (global, actions, payload): ActionRetur
});
});
addActionHandler('searchPopularBotApps', async (global, actions, payload): Promise<void> => {
const { tabId = getCurrentTabId() } = payload || {};
const popularBotApps = selectTabState(global, tabId).globalSearch.popularBotApps;
const offset = popularBotApps?.nextOffset;
if (popularBotApps?.peerIds && !offset) return; // Already fetched all
global = updateGlobalSearchFetchingStatus(global, { botApps: true }, tabId);
setGlobal(global);
const result = await callApi('fetchPopularAppBots', { offset });
global = getGlobal();
if (!result) {
global = updateGlobalSearchFetchingStatus(global, { botApps: false }, tabId);
setGlobal(global);
return;
}
global = addUsers(global, buildCollectionByKey(result.users, 'id'));
global = addChats(global, buildCollectionByKey(result.chats, 'id'));
const peerIds = result.users.map(({ id }) => id);
global = updateGlobalSearch(global, {
popularBotApps: {
peerIds: [...(popularBotApps?.peerIds || []), ...peerIds],
nextOffset: result.nextOffset,
},
}, tabId);
global = updateGlobalSearchFetchingStatus(global, { botApps: false }, tabId);
setGlobal(global);
});
async function searchMessagesGlobal<T extends GlobalState>(global: T, params: {
query?: string;
type: ApiGlobalMessageSearchType;

View File

@ -1,4 +1,5 @@
import type { ActionReturnType } from '../../types';
import { GlobalSearchContent } from '../../../types';
import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { addActionHandler } from '../../index';
@ -8,14 +9,17 @@ import { selectTabState } from '../../selectors';
const MAX_RECENTLY_FOUND_IDS = 10;
addActionHandler('setGlobalSearchQuery', (global, actions, payload): ActionReturnType => {
const { query, tabId = getCurrentTabId() } = payload!;
const { chatId } = selectTabState(global, tabId).globalSearch;
const { query, tabId = getCurrentTabId() } = payload;
const { chatId, currentContent } = selectTabState(global, tabId).globalSearch;
const fetchingStatus = query && currentContent !== GlobalSearchContent.BotApps
? { chats: !chatId, messages: true } : undefined;
return updateGlobalSearch(global, {
globalResults: {},
localResults: {},
resultsByType: undefined,
...(query ? { fetchingStatus: { chats: !chatId, messages: true } } : { fetchingStatus: undefined }),
fetchingStatus,
query,
}, tabId);
});

View File

@ -300,6 +300,7 @@ function reduceGlobal<T extends GlobalState>(global: T) {
'contactList',
'topPeers',
'topInlineBots',
'topBotApps',
'recentEmojis',
'recentCustomEmojis',
'push',

View File

@ -16,7 +16,7 @@ export function getRequestInputInvoice<T extends GlobalState>(
const {
userId, stars, amount, currency,
} = inputInvoice;
const user = selectUser(global, userId!);
const user = selectUser(global, userId);
if (!user) return undefined;

View File

@ -77,6 +77,9 @@ export function getUserStatus(
}
if (user.type && user.type === 'userTypeBot') {
if (user.botActiveUsers) {
return lang('BotUsers', user.botActiveUsers, 'i');
}
return lang('Bot');
}

View File

@ -213,6 +213,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
topPeers: {},
topInlineBots: {},
topBotApps: {},
activeSessions: {
byHash: {},

View File

@ -79,7 +79,7 @@ export function updateGlobalSearchResults<T extends GlobalState>(
}
export function updateGlobalSearchFetchingStatus<T extends GlobalState>(
global: T, newState: { chats?: boolean; messages?: boolean },
global: T, newState: { chats?: boolean; messages?: boolean; botApps?: boolean },
...[tabId = getCurrentTabId()]: TabArgs<T>
): T {
return updateGlobalSearch(global, {

View File

@ -376,6 +376,7 @@ export type TabState = {
fetchingStatus?: {
chats?: boolean;
messages?: boolean;
botApps?: boolean;
};
isClosing?: boolean;
localResults?: {
@ -384,6 +385,10 @@ export type TabState = {
globalResults?: {
peerIds?: string[];
};
popularBotApps?: {
peerIds: string[];
nextOffset?: string;
};
resultsByType?: Partial<Record<ApiGlobalMessageSearchType, {
totalCount?: number;
nextOffsetId?: number;
@ -1120,6 +1125,11 @@ export type GlobalState = {
lastRequestedAt?: number;
};
topBotApps: {
userIds?: string[];
lastRequestedAt?: number;
};
activeSessions: {
byHash: Record<string, ApiSession>;
orderedHashes: string[];
@ -1491,6 +1501,7 @@ export interface ActionPayloads {
searchMessagesGlobal: {
type: ApiGlobalMessageSearchType;
} & WithTabId;
searchPopularBotApps: WithTabId | undefined;
addRecentlyFoundChatId: {
id: string;
};
@ -2800,6 +2811,7 @@ export interface ActionPayloads {
chatId?: string;
} & WithTabId;
loadTopInlineBots: undefined;
loadTopBotApps: undefined;
queryInlineBot: {
chatId: string;
username: string;

View File

@ -1617,6 +1617,7 @@ bots.setBotInfo#10cf3123 flags:# bot:flags.2?InputUser lang_code:string name:fla
bots.canSendMessage#1359f4e6 bot:InputUser = Bool;
bots.allowSendMessage#f132e3ef bot:InputUser = Updates;
bots.invokeWebViewCustomMethod#87fc5e7 bot:InputUser custom_method:string params:DataJSON = DataJSON;
bots.getPopularAppBots#c2510192 offset:string limit:int = bots.PopularAppBots;
payments.getPaymentForm#37148dbb flags:# invoice:InputInvoice theme_params:flags.0?DataJSON = payments.PaymentForm;
payments.getPaymentReceipt#2478d1cc peer:InputPeer msg_id:int = payments.PaymentReceipt;
payments.validateRequestedInfo#b6c8f12b flags:# save:flags.0?true invoice:InputInvoice info:PaymentRequestedInfo = payments.ValidatedRequestedInfo;

View File

@ -228,6 +228,7 @@
"bots.canSendMessage",
"bots.allowSendMessage",
"bots.invokeWebViewCustomMethod",
"bots.getPopularAppBots",
"bots.setBotInfo",
"payments.getPaymentForm",
"payments.getPaymentReceipt",

View File

@ -291,6 +291,7 @@ export enum LeftColumnContent {
export enum GlobalSearchContent {
ChatList,
ChannelList,
BotApps,
Media,
Links,
Files,