Composer: Support guest bot mentions (#6961)

This commit is contained in:
zubiden 2026-06-01 01:15:58 +02:00 committed by Alexander Zinchuk
parent 37c3156493
commit fb5a7cfb5a
23 changed files with 410 additions and 233 deletions

View File

@ -211,7 +211,7 @@ export function buildApiUrlAuthResult(result: GramJs.TypeUrlAuthResult): ApiUrlA
export function buildApiConfig(config: GramJs.Config): ApiConfig { export function buildApiConfig(config: GramJs.Config): ApiConfig {
const { const {
testMode, expires, gifSearchUsername, chatSizeMax, autologinToken, reactionsDefault, testMode, expires, gifSearchUsername, chatSizeMax, autologinToken, reactionsDefault,
messageLengthMax, editTimeLimit, forwardedCountMax, messageLengthMax, editTimeLimit, forwardedCountMax, ratingEDecay,
} = config; } = config;
const defaultReaction = reactionsDefault && buildApiReaction(reactionsDefault); const defaultReaction = reactionsDefault && buildApiReaction(reactionsDefault);
return { return {
@ -224,6 +224,7 @@ export function buildApiConfig(config: GramJs.Config): ApiConfig {
maxMessageLength: messageLengthMax, maxMessageLength: messageLengthMax,
editTimeLimit, editTimeLimit,
maxForwardedCount: forwardedCountMax, maxForwardedCount: forwardedCountMax,
ratingEDecay,
}; };
} }

View File

@ -23,7 +23,6 @@ import {
buildBotSwitchPm, buildBotSwitchPm,
buildBotSwitchWebview, buildBotSwitchWebview,
} from '../apiBuilders/bots'; } from '../apiBuilders/bots';
import { buildApiChatFromPreview } from '../apiBuilders/chats';
import { omitVirtualClassFields } from '../apiBuilders/helpers'; import { omitVirtualClassFields } from '../apiBuilders/helpers';
import { buildMessageMediaContent } from '../apiBuilders/messageContent'; import { buildMessageMediaContent } from '../apiBuilders/messageContent';
import { buildApiUrlAuthResult } from '../apiBuilders/misc'; import { buildApiUrlAuthResult } from '../apiBuilders/misc';
@ -39,7 +38,6 @@ import {
import { import {
addDocumentToLocalDb, addDocumentToLocalDb,
addPhotoToLocalDb, addPhotoToLocalDb,
addUserToLocalDb,
addWebDocumentToLocalDb, addWebDocumentToLocalDb,
} from '../helpers/localDb'; } from '../helpers/localDb';
import { deserializeBytes } from '../helpers/misc'; import { deserializeBytes } from '../helpers/misc';
@ -61,68 +59,6 @@ export async function answerCallbackButton({
return result ? omitVirtualClassFields(result) : undefined; return result ? omitVirtualClassFields(result) : undefined;
} }
export async function fetchTopInlineBots() {
const topPeers = await invokeRequest(new GramJs.contacts.GetTopPeers({
botsInline: true,
limit: DEFAULT_PRIMITIVES.INT,
offset: DEFAULT_PRIMITIVES.INT,
hash: DEFAULT_PRIMITIVES.BIGINT,
}));
if (!(topPeers instanceof GramJs.contacts.TopPeers)) {
return undefined;
}
const users = topPeers.users.map(buildApiUser).filter(Boolean);
const ids = users.map(({ id }) => id);
return {
ids,
};
}
export async function fetchTopBotApps() {
const topPeers = await invokeRequest(new GramJs.contacts.GetTopPeers({
botsApp: true,
limit: DEFAULT_PRIMITIVES.INT,
offset: DEFAULT_PRIMITIVES.INT,
hash: DEFAULT_PRIMITIVES.BIGINT,
}));
if (!(topPeers instanceof GramJs.contacts.TopPeers)) {
return undefined;
}
const users = topPeers.users.map(buildApiUser).filter(Boolean);
const ids = users.map(({ id }) => id);
return {
ids,
};
}
export async function fetchInlineBot({ username }: { username: string }) {
const resolvedPeer = await invokeRequest(new GramJs.contacts.ResolveUsername({ username }));
if (
!resolvedPeer
|| !(
resolvedPeer.users[0] instanceof GramJs.User
&& resolvedPeer.users[0].bot
&& resolvedPeer.users[0].botInlinePlaceholder
)
) {
return undefined;
}
addUserToLocalDb(resolvedPeer.users[0]);
return {
user: buildApiUser(resolvedPeer.users[0]),
chat: buildApiChatFromPreview(resolvedPeer.users[0]),
};
}
export async function fetchInlineBotResults({ export async function fetchInlineBotResults({
bot, chat, query, offset = DEFAULT_PRIMITIVES.STRING, bot, chat, query, offset = DEFAULT_PRIMITIVES.STRING,
}: { }: {

View File

@ -21,6 +21,8 @@ export * from './messages';
export * from './users'; export * from './users';
export * from './topPeers';
export * from './symbols'; export * from './symbols';
export * from './management'; export * from './management';

View File

@ -0,0 +1,103 @@
import { Api as GramJs } from '../../../lib/gramjs';
import type {
ApiPeer,
ApiTopPeer,
ApiTopPeerCategory,
ApiTopPeersResult,
} from '../../types';
import { getApiChatIdFromMtpPeer } from '../apiBuilders/peers';
import { buildInputPeer, DEFAULT_PRIMITIVES } from '../gramjsBuilders';
import { addChatToLocalDb, addUserToLocalDb } from '../helpers/localDb';
import { invokeRequest } from './client';
const TOP_PEER_LIMIT = 50;
export async function fetchTopPeers({
category,
}: {
category: ApiTopPeerCategory;
}): Promise<ApiTopPeersResult | undefined> {
const result = await invokeRequest(new GramJs.contacts.GetTopPeers({
correspondents: category === 'correspondents' || undefined,
botsInline: category === 'botsInline' || undefined,
botsApp: category === 'botsApp' || undefined,
botsGuestchat: category === 'botsGuestChat' || undefined,
offset: DEFAULT_PRIMITIVES.INT,
limit: TOP_PEER_LIMIT,
hash: DEFAULT_PRIMITIVES.BIGINT,
}));
if (result instanceof GramJs.contacts.TopPeersNotModified) {
return { type: 'unchanged' };
}
if (result instanceof GramJs.contacts.TopPeersDisabled) {
return { type: 'disabled' };
}
if (!(result instanceof GramJs.contacts.TopPeers)) {
return undefined;
}
result.users.forEach(addUserToLocalDb);
result.chats.forEach((chat) => {
if (chat instanceof GramJs.Chat || chat instanceof GramJs.Channel) {
addChatToLocalDb(chat);
}
});
const topPeerCategory = result.categories.find(({ category: mtpCategory }) => {
return getTopPeerCategory(mtpCategory) === category;
});
const topPeers: ApiTopPeer[] = topPeerCategory
? topPeerCategory.peers.map(({ peer, rating }) => ({
peerId: getApiChatIdFromMtpPeer(peer),
rating,
})) : [];
return {
type: 'topPeers',
category,
topPeers,
};
}
export function resetTopPeerRating({ category, peer }: { category: ApiTopPeerCategory; peer: ApiPeer }) {
return invokeRequest(new GramJs.contacts.ResetTopPeerRating({
category: buildTopPeerCategory(category),
peer: buildInputPeer(peer.id, peer.accessHash),
}));
}
function getTopPeerCategory(category: GramJs.TypeTopPeerCategory): ApiTopPeerCategory | undefined {
if (category instanceof GramJs.TopPeerCategoryCorrespondents) {
return 'correspondents';
}
if (category instanceof GramJs.TopPeerCategoryBotsInline) {
return 'botsInline';
}
if (category instanceof GramJs.TopPeerCategoryBotsApp) {
return 'botsApp';
}
if (category instanceof GramJs.TopPeerCategoryBotsGuestChat) {
return 'botsGuestChat';
}
return undefined;
}
function buildTopPeerCategory(category: ApiTopPeerCategory): GramJs.TypeTopPeerCategory {
switch (category) {
case 'correspondents':
return new GramJs.TopPeerCategoryCorrespondents();
case 'botsInline':
return new GramJs.TopPeerCategoryBotsInline();
case 'botsApp':
return new GramJs.TopPeerCategoryBotsApp();
case 'botsGuestChat':
return new GramJs.TopPeerCategoryBotsGuestChat();
}
}

View File

@ -132,25 +132,6 @@ export async function fetchNearestCountry() {
return dcInfo?.country; return dcInfo?.country;
} }
export async function fetchTopUsers() {
const topPeers = await invokeRequest(new GramJs.contacts.GetTopPeers({
correspondents: true,
offset: DEFAULT_PRIMITIVES.INT,
limit: DEFAULT_PRIMITIVES.INT,
hash: DEFAULT_PRIMITIVES.BIGINT,
}));
if (!(topPeers instanceof GramJs.contacts.TopPeers)) {
return undefined;
}
const users = topPeers.users.map(buildApiUser).filter((user): user is ApiUser => Boolean(user) && !user.isSelf);
const ids = users.map(({ id }) => id);
return {
ids,
};
}
export async function fetchContactList() { export async function fetchContactList() {
const result = await invokeRequest(new GramJs.contacts.GetContacts({ hash: DEFAULT_PRIMITIVES.BIGINT })); const result = await invokeRequest(new GramJs.contacts.GetContacts({ hash: DEFAULT_PRIMITIVES.BIGINT }));
if (!result || result instanceof GramJs.contacts.ContactsNotModified) { if (!result || result instanceof GramJs.contacts.ContactsNotModified) {

View File

@ -351,8 +351,26 @@ export interface ApiConfig {
maxMessageLength: number; maxMessageLength: number;
editTimeLimit: number; editTimeLimit: number;
maxForwardedCount: number; maxForwardedCount: number;
ratingEDecay: number;
} }
export type ApiTopPeerCategory = 'correspondents' | 'botsInline' | 'botsApp' | 'botsGuestChat';
export type ApiTopPeer = {
peerId: string;
rating: number;
};
export type ApiTopPeersResult = {
type: 'topPeers';
category: ApiTopPeerCategory;
topPeers: ApiTopPeer[];
} | {
type: 'unchanged';
} | {
type: 'disabled';
};
export interface ApiPromoData { export interface ApiPromoData {
expires: number; expires: number;
pendingSuggestions: string[]; pendingSuggestions: string[];

View File

@ -67,6 +67,7 @@ import {
isChatSuperGroup, isChatSuperGroup,
isSameReaction, isSameReaction,
isSystemBot, isSystemBot,
isUserRightBanned,
} from '../../global/helpers'; } from '../../global/helpers';
import { getChatNotifySettings } from '../../global/helpers/notifications'; import { getChatNotifySettings } from '../../global/helpers/notifications';
import { getPeerTitle } from '../../global/helpers/peers'; import { getPeerTitle } from '../../global/helpers/peers';
@ -269,6 +270,7 @@ type StateProps = {
baseEmojiKeywords?: Record<string, string[]>; baseEmojiKeywords?: Record<string, string[]>;
emojiKeywords?: Record<string, string[]>; emojiKeywords?: Record<string, string[]>;
topInlineBotIds?: string[]; topInlineBotIds?: string[];
topGuestBotIds?: string[];
isInlineBotLoading: boolean; isInlineBotLoading: boolean;
inlineBots?: Record<string, false | InlineBotSettings>; inlineBots?: Record<string, false | InlineBotSettings>;
botCommands?: ApiBotCommand[] | false; botCommands?: ApiBotCommand[] | false;
@ -387,6 +389,7 @@ const Composer = ({
stickersForEmoji, stickersForEmoji,
customEmojiForEmoji, customEmojiForEmoji,
topInlineBotIds, topInlineBotIds,
topGuestBotIds,
currentUserId, currentUserId,
currentUser, currentUser,
captionLimit, captionLimit,
@ -591,6 +594,7 @@ const Composer = ({
), ),
[chat, chatFullInfo, isChatWithBot, isChatWithSelf, isInStoryViewer, paidMessagesStars, isInScheduledList], [chat, chatFullInfo, isChatWithBot, isChatWithSelf, isInStoryViewer, paidMessagesStars, isInScheduledList],
); );
const canUseInlineBots = !chat || isChatAdmin(chat) || !isUserRightBanned(chat, 'sendInline', chatFullInfo);
const isNeedPremium = isContactRequirePremium && isInStoryViewer; const isNeedPremium = isContactRequirePremium && isInStoryViewer;
const isSendTextBlocked = isNeedPremium || !canSendPlainText; const isSendTextBlocked = isNeedPremium || !canSendPlainText;
@ -844,7 +848,8 @@ const Composer = ({
getSelectionRange, getSelectionRange,
inputRef, inputRef,
groupChatMembers, groupChatMembers,
topInlineBotIds, canUseInlineBots ? topInlineBotIds : undefined,
topGuestBotIds,
currentUserId, currentUserId,
); );
@ -887,7 +892,7 @@ const Composer = ({
help: inlineBotHelp, help: inlineBotHelp,
loadMore: loadMoreForInlineBot, loadMore: loadMoreForInlineBot,
} = useInlineBotTooltip( } = useInlineBotTooltip(
Boolean(isInMessageList && isReady && isForCurrentMessageList && !hasAttachments), Boolean(canUseInlineBots && isInMessageList && isReady && isForCurrentMessageList && !hasAttachments),
chatId, chatId,
getHtml, getHtml,
inlineBots, inlineBots,
@ -1595,7 +1600,7 @@ const Composer = ({
const handleInlineBotSelect = useLastCallback(( const handleInlineBotSelect = useLastCallback((
inlineResult: ApiBotInlineResult | ApiBotInlineMediaResult, isSilent?: boolean, isScheduleRequested?: boolean, inlineResult: ApiBotInlineResult | ApiBotInlineMediaResult, isSilent?: boolean, isScheduleRequested?: boolean,
) => { ) => {
if (!currentMessageList && !storyId) { if ((!currentMessageList && !storyId) || !inlineBotId) {
return; return;
} }
@ -2786,7 +2791,8 @@ export default memo(withGlobal<OwnProps>(
stickersForEmoji: global.stickers.forEmoji.stickers, stickersForEmoji: global.stickers.forEmoji.stickers,
customEmojiForEmoji: global.customEmojis.forEmoji.stickers, customEmojiForEmoji: global.customEmojis.forEmoji.stickers,
chatFullInfo, chatFullInfo,
topInlineBotIds: global.topInlineBots?.userIds, topInlineBotIds: global.topPeerCategories.botsInline?.peerIds,
topGuestBotIds: global.topPeerCategories.botsGuestChat?.peerIds,
currentUserId, currentUserId,
currentUser, currentUser,
contentToBeScheduled: tabState.contentToBeScheduled, contentToBeScheduled: tabState.contentToBeScheduled,

View File

@ -152,6 +152,6 @@ export default memo(withGlobal<OwnProps>((global): Complete<StateProps> => {
return { return {
isLoading: !foundIds && globalSearch.fetchingStatus?.botApps, isLoading: !foundIds && globalSearch.fetchingStatus?.botApps,
foundIds, foundIds,
recentBotIds: global.topBotApps.userIds, recentBotIds: global.topPeerCategories.botsApp?.peerIds,
}; };
})(BotAppResults)); })(BotAppResults));

View File

@ -1,19 +1,20 @@
import type { FC } from '../../../lib/teact/teact';
import { import {
memo, memo,
useCallback, useEffect, useRef, useCallback, useEffect, useRef,
} from '../../../lib/teact/teact'; } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global'; import { getActions, withGlobal } from '../../../global';
import type { ApiUser } from '../../../api/types'; import type { GlobalState } from '../../../global/types';
import { getUserFirstOrLastName } from '../../../global/helpers'; import { getPeerTitle } from '../../../global/helpers/peers';
import { selectPeer } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import { throttle } from '../../../util/schedulers'; import { throttle } from '../../../util/schedulers';
import renderText from '../../common/helpers/renderText'; import renderText from '../../common/helpers/renderText';
import { useShallowSelector } from '../../../hooks/data/useSelector';
import useHorizontalScroll from '../../../hooks/useHorizontalScroll'; import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
import useOldLang from '../../../hooks/useOldLang'; import useLang from '../../../hooks/useLang';
import Avatar from '../../common/Avatar'; import Avatar from '../../common/Avatar';
import Button from '../../ui/Button'; import Button from '../../ui/Button';
@ -26,8 +27,7 @@ type OwnProps = {
}; };
type StateProps = { type StateProps = {
topUserIds?: string[]; topPeerIds?: string[];
usersById: Record<string, ApiUser>;
recentlyFoundChatIds?: string[]; recentlyFoundChatIds?: string[];
}; };
@ -36,28 +36,33 @@ const NBSP = '\u00A0';
const runThrottled = throttle((cb) => cb(), 60000, true); const runThrottled = throttle((cb) => cb(), 60000, true);
const RecentContacts: FC<OwnProps & StateProps> = ({ const RecentContacts = ({
topUserIds, topPeerIds,
usersById,
recentlyFoundChatIds, recentlyFoundChatIds,
onReset, onReset,
}) => { }: OwnProps & StateProps) => {
const { const {
loadTopUsers, openChat, loadTopPeers, openChat,
addRecentlyFoundChatId, clearRecentlyFoundChats, addRecentlyFoundChatId, clearRecentlyFoundChats,
} = getActions(); } = getActions();
const topUsersRef = useRef<HTMLDivElement>(); const topPeersRef = useRef<HTMLDivElement>();
// Due to the parent Transition, this component never gets unmounted, // Due to the parent Transition, this component never gets unmounted,
// that's why we use throttled API call on every update. // that's why we use throttled API call on every update.
useEffect(() => { useEffect(() => {
runThrottled(() => { runThrottled(() => {
loadTopUsers(); loadTopPeers({ category: 'correspondents' });
}); });
}, [loadTopUsers]); }, [loadTopPeers]);
useHorizontalScroll(topUsersRef, !topUserIds); const topPeersSelector = useCallback((global: GlobalState) => {
return topPeerIds?.map((peerId) => selectPeer(global, peerId)).filter(Boolean);
}, [topPeerIds]);
const topPeers = useShallowSelector(topPeersSelector);
const shouldRenderTopPeers = Boolean(topPeers?.length);
useHorizontalScroll(topPeersRef, !shouldRenderTopPeers);
const handleClick = useCallback((id: string) => { const handleClick = useCallback((id: string) => {
openChat({ id, shouldReplaceHistory: true }); openChat({ id, shouldReplaceHistory: true });
@ -71,22 +76,22 @@ const RecentContacts: FC<OwnProps & StateProps> = ({
clearRecentlyFoundChats(); clearRecentlyFoundChats();
}, [clearRecentlyFoundChats]); }, [clearRecentlyFoundChats]);
const lang = useOldLang(); const lang = useLang();
return ( return (
<div className="RecentContacts custom-scroll"> <div className="RecentContacts custom-scroll">
{topUserIds && ( {shouldRenderTopPeers && (
<div className="top-peers-section" dir={lang.isRtl ? 'rtl' : undefined}> <div className="top-peers-section" dir={lang.isRtl ? 'rtl' : undefined}>
<div ref={topUsersRef} className="top-peers"> <div ref={topPeersRef} className="top-peers">
{topUserIds.map((userId) => ( {topPeers?.map((peer) => (
<div <div
key={userId} key={peer.id}
className="top-peer-item" className="top-peer-item"
onClick={() => handleClick(userId)} onClick={() => handleClick(peer.id)}
dir={lang.isRtl ? 'rtl' : undefined} dir={lang.isRtl ? 'rtl' : undefined}
> >
<Avatar peer={usersById[userId]} /> <Avatar peer={peer} />
<div className="top-peer-name">{renderText(getUserFirstOrLastName(usersById[userId]) || NBSP)}</div> <div className="top-peer-name">{renderText(getPeerTitle(lang, peer) || NBSP)}</div>
</div> </div>
))} ))}
</div> </div>
@ -97,7 +102,7 @@ const RecentContacts: FC<OwnProps & StateProps> = ({
<h3 <h3
className={buildClassName( className={buildClassName(
'section-heading mt-0 recent-chats-header', 'section-heading mt-0 recent-chats-header',
!topUserIds && 'without-border', !shouldRenderTopPeers && 'without-border',
)} )}
dir={lang.isRtl ? 'rtl' : undefined} dir={lang.isRtl ? 'rtl' : undefined}
> >
@ -129,13 +134,11 @@ const RecentContacts: FC<OwnProps & StateProps> = ({
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
(global): Complete<StateProps> => { (global): Complete<StateProps> => {
const { userIds: topUserIds } = global.topPeers; const topPeerIds = global.topPeerCategories.correspondents?.peerIds;
const usersById = global.users.byId;
const { recentlyFoundChatIds } = global; const { recentlyFoundChatIds } = global;
return { return {
topUserIds, topPeerIds,
usersById,
recentlyFoundChatIds, recentlyFoundChatIds,
}; };
}, },

View File

@ -213,7 +213,7 @@ const Main = ({
loadNotificationExceptions, loadNotificationExceptions,
updateIsOnline, updateIsOnline,
onTabFocusChange, onTabFocusChange,
loadTopInlineBots, loadTopPeers,
loadEmojiKeywords, loadEmojiKeywords,
loadCountryList, loadCountryList,
loadAvailableReactions, loadAvailableReactions,
@ -257,7 +257,6 @@ const Main = ({
loadQuickReplies, loadQuickReplies,
loadStarStatus, loadStarStatus,
loadAvailableEffects, loadAvailableEffects,
loadTopBotApps,
loadPaidReactionPrivacy, loadPaidReactionPrivacy,
loadPasswordInfo, loadPasswordInfo,
loadBotFreezeAppeal, loadBotFreezeAppeal,
@ -327,13 +326,14 @@ const Main = ({
loadAttachBots(); loadAttachBots();
loadNotificationSettings(); loadNotificationSettings();
loadNotificationExceptions(); loadNotificationExceptions();
loadTopInlineBots(); loadTopPeers({ category: 'botsInline' });
loadTopReactions(); loadTopReactions();
loadStarStatus(); loadStarStatus();
loadEmojiKeywords({ language: BASE_EMOJI_KEYWORD_LANG }); loadEmojiKeywords({ language: BASE_EMOJI_KEYWORD_LANG });
loadFeaturedEmojiStickers(); loadFeaturedEmojiStickers();
loadSavedReactionTags(); loadSavedReactionTags();
loadTopBotApps(); loadTopPeers({ category: 'botsApp' });
loadTopPeers({ category: 'botsGuestChat' });
loadPaidReactionPrivacy(); loadPaidReactionPrivacy();
loadDefaultTopicIcons(); loadDefaultTopicIcons();
loadAnimatedEmojis(); loadAnimatedEmojis();

View File

@ -103,6 +103,7 @@ type StateProps = {
emojiKeywords?: Record<string, string[]>; emojiKeywords?: Record<string, string[]>;
shouldSuggestCustomEmoji?: boolean; shouldSuggestCustomEmoji?: boolean;
customEmojiForEmoji?: ApiSticker[]; customEmojiForEmoji?: ApiSticker[];
topGuestBotIds?: string[];
captionLimit: number; captionLimit: number;
attachmentSettings: GlobalState['attachmentSettings']; attachmentSettings: GlobalState['attachmentSettings'];
shouldSaveAttachmentsCompression?: boolean; shouldSaveAttachmentsCompression?: boolean;
@ -127,6 +128,7 @@ const AttachmentModal = ({
isChatWithSelf, isChatWithSelf,
currentUserId, currentUserId,
groupChatMembers, groupChatMembers,
topGuestBotIds,
recentEmojis, recentEmojis,
baseEmojiKeywords, baseEmojiKeywords,
emojiKeywords, emojiKeywords,
@ -283,6 +285,7 @@ const AttachmentModal = ({
inputRef, inputRef,
groupChatMembers, groupChatMembers,
undefined, undefined,
topGuestBotIds,
currentUserId, currentUserId,
); );
@ -944,6 +947,7 @@ export default memo(withGlobal<OwnProps>(
isChatWithSelf, isChatWithSelf,
currentUserId, currentUserId,
groupChatMembers: chatFullInfo?.members, groupChatMembers: chatFullInfo?.members,
topGuestBotIds: global.topPeerCategories.botsGuestChat?.peerIds,
recentEmojis, recentEmojis,
baseEmojiKeywords: baseEmojiKeywords?.keywords, baseEmojiKeywords: baseEmojiKeywords?.keywords,
emojiKeywords: emojiKeywords?.keywords, emojiKeywords: emojiKeywords?.keywords,

View File

@ -39,6 +39,7 @@ export default function useMentionTooltip(
inputRef: ElementRef<HTMLDivElement>, inputRef: ElementRef<HTMLDivElement>,
groupChatMembers?: ApiChatMember[], groupChatMembers?: ApiChatMember[],
topInlineBotIds?: string[], topInlineBotIds?: string[],
topGuestBotIds?: string[],
currentUserId?: string, currentUserId?: string,
) { ) {
const lang = useLang(); const lang = useLang();
@ -65,7 +66,7 @@ export default function useMentionTooltip(
useEffect(() => { useEffect(() => {
const usernameTag = getUsernameTag(); const usernameTag = getUsernameTag();
if (!usernameTag || !(groupChatMembers || topInlineBotIds)) { if (!usernameTag || !(groupChatMembers || topInlineBotIds || topGuestBotIds)) {
setFilteredUsers(undefined); setFilteredUsers(undefined);
return; return;
} }
@ -90,13 +91,14 @@ export default function useMentionTooltip(
ids: unique([ ids: unique([
...((getWithInlineBots() && topInlineBotIds) || []), ...((getWithInlineBots() && topInlineBotIds) || []),
...(memberIds || []), ...(memberIds || []),
...(topGuestBotIds || []),
]), ]),
query: filter, query: filter,
type: 'user', type: 'user',
}); });
setFilteredUsers(Object.values(pickTruthy(usersById, filteredIds))); setFilteredUsers(Object.values(pickTruthy(usersById, filteredIds)));
}, [currentUserId, groupChatMembers, topInlineBotIds, getUsernameTag, getWithInlineBots]); }, [currentUserId, groupChatMembers, topInlineBotIds, topGuestBotIds, getUsernameTag, getWithInlineBots]);
const insertMention = useLastCallback(( const insertMention = useLastCallback((
peer: ApiPeer, peer: ApiPeer,

View File

@ -133,6 +133,6 @@ export default memo(withGlobal((global): Complete<StateProps> => {
return { return {
isLoading: !foundIds && globalSearch.fetchingStatus?.botApps, isLoading: !foundIds && globalSearch.fetchingStatus?.botApps,
foundIds, foundIds,
recentBotIds: global.topBotApps.userIds, recentBotIds: global.topPeerCategories.botsApp?.peerIds,
}; };
})(MoreAppsTabContent)); })(MoreAppsTabContent));

View File

@ -9,6 +9,7 @@ import './api/sync';
import './api/accounts'; import './api/accounts';
import './api/ai'; import './api/ai';
import './api/users'; import './api/users';
import './api/topPeers';
import './api/bots'; import './api/bots';
import './api/settings'; import './api/settings';
import './api/twoFaSettings'; import './api/twoFaSettings';

View File

@ -23,12 +23,14 @@ import { formatStarsAsText } from '../../../util/localization/format';
import { oldTranslate } from '../../../util/oldLangProvider'; import { oldTranslate } from '../../../util/oldLangProvider';
import requestActionTimeout from '../../../util/requestActionTimeout'; import requestActionTimeout from '../../../util/requestActionTimeout';
import { debounce } from '../../../util/schedulers'; import { debounce } from '../../../util/schedulers';
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 { import {
getMainUsername, getMainUsername,
getWebAppKey, getWebAppKey,
isChatAdmin,
isUserBot,
isUserRightBanned,
prepareMessageReplyInfo, prepareMessageReplyInfo,
} from '../../helpers'; } from '../../helpers';
import { import {
@ -52,6 +54,7 @@ import { updateTabState } from '../../reducers/tabs';
import { import {
selectBot, selectBot,
selectChat, selectChat,
selectChatFullInfo,
selectChatLastMessageId, selectChatLastMessageId,
selectChatMessage, selectChatMessage,
selectCurrentChat, selectCurrentChat,
@ -73,10 +76,13 @@ import { getPeerStarsForMessage } from './messages';
import { getIsWebAppsFullscreenSupported } from '../../../hooks/useAppLayout'; import { getIsWebAppsFullscreenSupported } from '../../../hooks/useAppLayout';
const TOP_PEERS_REQUEST_COOLDOWN = 60; // 1 min
const runDebouncedForSearch = debounce((cb) => cb(), 500, false); const runDebouncedForSearch = debounce((cb) => cb(), 500, false);
let botFatherId: string | null; let botFatherId: string | null;
function canUseInlineBots<T extends GlobalState>(global: T, chat: ApiChat) {
return isChatAdmin(chat) || !isUserRightBanned(chat, 'sendInline', selectChatFullInfo(global, chat.id));
}
addActionHandler('clickSuggestedMessageButton', (global, actions, payload): ActionReturnType => { addActionHandler('clickSuggestedMessageButton', (global, actions, payload): ActionReturnType => {
const { const {
chatId, messageId, button, tabId = getCurrentTabId(), chatId, messageId, button, tabId = getCurrentTabId(),
@ -285,71 +291,26 @@ addActionHandler('restartBot', async (global, actions, payload): Promise<void> =
void sendBotCommand(chat, MAIN_THREAD_ID, '/start', undefined, selectSendAs(global, chatId), lastMessageId); void sendBotCommand(chat, MAIN_THREAD_ID, '/start', undefined, selectSendAs(global, chatId), lastMessageId);
}); });
addActionHandler('loadTopInlineBots', async (global): Promise<void> => {
const { lastRequestedAt } = global.topInlineBots;
if (lastRequestedAt && getServerTime() - lastRequestedAt < TOP_PEERS_REQUEST_COOLDOWN) {
return;
}
const result = await callApi('fetchTopInlineBots');
if (!result) {
return;
}
const { ids } = result;
global = getGlobal();
global = {
...global,
topInlineBots: {
...global.topInlineBots,
userIds: ids,
lastRequestedAt: getServerTime(),
},
};
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 } = result;
global = getGlobal();
global = {
...global,
topBotApps: {
...global.topBotApps,
userIds: ids,
lastRequestedAt: getServerTime(),
},
};
setGlobal(global);
});
addActionHandler('queryInlineBot', async (global, actions, payload): Promise<void> => { addActionHandler('queryInlineBot', async (global, actions, payload): Promise<void> => {
const { const {
chatId, username, query, offset, chatId, username, query, offset,
tabId = getCurrentTabId(), tabId = getCurrentTabId(),
} = payload; } = payload;
const chat = selectChat(global, chatId);
if (!chat || !canUseInlineBots(global, chat)) {
return;
}
let inlineBotData = selectTabState(global, tabId).inlineBots.byUsername[username]; let inlineBotData = selectTabState(global, tabId).inlineBots.byUsername[username];
if (inlineBotData === false) { if (inlineBotData === false) {
return; return;
} }
if (inlineBotData === undefined) { if (inlineBotData === undefined) {
const { user: inlineBot, chat } = await callApi('fetchInlineBot', { username }) || {}; const { user: inlineBot } = await callApi('getChatByUsername', username) || {};
global = getGlobal(); global = getGlobal();
if (!inlineBot || !chat) { if (!inlineBot || !isUserBot(inlineBot) || !inlineBot.botPlaceholder) {
global = replaceInlineBotSettings(global, username, false, tabId); global = replaceInlineBotSettings(global, username, false, tabId);
setGlobal(global); setGlobal(global);
return; return;
@ -818,6 +779,7 @@ addActionHandler('requestMainWebView', async (global, actions, payload): Promise
}; };
global = addWebAppToOpenList(global, newActiveApp, true, true, tabId); global = addWebAppToOpenList(global, newActiveApp, true, true, tabId);
setGlobal(global); setGlobal(global);
actions.bumpTopPeerRating({ category: 'botsApp', peerId: botId });
if (isFullscreen && getIsWebAppsFullscreenSupported()) { if (isFullscreen && getIsWebAppsFullscreenSupported()) {
actions.changeWebAppModalState({ state: 'fullScreen', tabId }); actions.changeWebAppModalState({ state: 'fullScreen', tabId });

View File

@ -0,0 +1,127 @@
import type { ApiTopPeerCategory } from '../../../api/types';
import type { ActionReturnType, GlobalState } from '../../types';
import { unique } from '../../../util/iteratees';
import { getServerTime } from '../../../util/serverTime';
import { callApi } from '../../../api/gramjs';
import { addActionHandler, getGlobal, setGlobal } from '../../index';
import { selectPeer } from '../../selectors';
const TOP_PEERS_CACHE_TTL = 24 * 60 * 60; // 24 hours
addActionHandler('loadTopPeers', async (global, actions, payload): Promise<void> => {
const { category, force } = payload;
const current = global.topPeerCategories[category];
const now = getServerTime();
if (!force && current?.lastRequestedAt && now - current.lastRequestedAt < TOP_PEERS_CACHE_TTL) {
return;
}
const result = await callApi('fetchTopPeers', { category });
if (!result) {
return;
}
global = getGlobal();
const nextNow = getServerTime();
const nextCurrent = global.topPeerCategories[category];
if (result.type === 'unchanged') {
global = updateTopPeerCategory(global, category, {
...nextCurrent,
peerIds: nextCurrent?.peerIds || [],
ratingsByPeerId: nextCurrent?.ratingsByPeerId || {},
lastRequestedAt: nextNow,
});
setGlobal(global);
return;
}
if (result.type === 'disabled') {
global = updateTopPeerCategory(global, category, {
peerIds: [],
ratingsByPeerId: {},
lastRequestedAt: nextNow,
isDisabled: true,
});
setGlobal(global);
return;
}
const ratingsByPeerId = result.topPeers.reduce((acc, { peerId, rating }) => {
acc[peerId] = rating;
return acc;
}, {} as Record<string, number>);
const peerIds = result.topPeers.map(({ peerId }) => peerId);
global = updateTopPeerCategory(global, category, {
peerIds,
ratingsByPeerId,
lastRequestedAt: nextNow,
isDisabled: undefined,
});
setGlobal(global);
});
addActionHandler('removeTopPeer', async (global, actions, payload): Promise<void> => {
const { category, peerId } = payload;
const current = global.topPeerCategories[category];
if (!current) {
return;
}
const peerIds = current.peerIds.filter((id) => id !== peerId);
const { [peerId]: removedRating, ...ratingsByPeerId } = current.ratingsByPeerId;
global = updateTopPeerCategory(global, category, {
...current,
peerIds,
ratingsByPeerId,
});
setGlobal(global);
const peer = selectPeer(global, peerId);
if (peer) {
await callApi('resetTopPeerRating', { category, peer });
}
});
addActionHandler('bumpTopPeerRating', (global, actions, payload): ActionReturnType => {
const { category, peerId, date } = payload;
const current = global.topPeerCategories[category];
const ratingEDecay = global.config?.ratingEDecay;
if (!ratingEDecay || current?.isDisabled) {
return;
}
const ratingDate = date || getServerTime();
const basePeerIds = current?.peerIds || [];
const peerIds = unique([...basePeerIds, peerId]);
const ratingsByPeerId = { ...current?.ratingsByPeerId };
const normalizeRate = current?.lastRequestedAt || ratingDate;
ratingsByPeerId[peerId] = (ratingsByPeerId[peerId] || 0) + Math.exp((ratingDate - normalizeRate) / ratingEDecay);
peerIds.sort((firstId, secondId) => (ratingsByPeerId[secondId] || 0) - (ratingsByPeerId[firstId] || 0));
return updateTopPeerCategory(global, category, {
peerIds,
ratingsByPeerId,
lastRequestedAt: normalizeRate,
isDisabled: undefined,
});
});
function updateTopPeerCategory<T extends GlobalState>(
global: T,
category: ApiTopPeerCategory,
categoryState: NonNullable<GlobalState['topPeerCategories'][ApiTopPeerCategory]>,
): T {
return {
...global,
topPeerCategories: {
...global.topPeerCategories,
[category]: categoryState,
},
};
}

View File

@ -8,7 +8,6 @@ import { getCurrentTabId } from '../../../util/establishMultitabRole';
import { buildCollectionByKey, unique } from '../../../util/iteratees'; import { buildCollectionByKey, unique } from '../../../util/iteratees';
import * as langProvider from '../../../util/oldLangProvider'; import * as langProvider from '../../../util/oldLangProvider';
import { throttle } from '../../../util/schedulers'; import { throttle } from '../../../util/schedulers';
import { getServerTime } from '../../../util/serverTime';
import { callApi } from '../../../api/gramjs'; import { callApi } from '../../../api/gramjs';
import { isUserBot } from '../../helpers'; import { isUserBot } from '../../helpers';
import { addActionHandler, getGlobal, setGlobal } from '../../index'; import { addActionHandler, getGlobal, setGlobal } from '../../index';
@ -43,7 +42,6 @@ import {
} from '../../selectors'; } from '../../selectors';
const PROFILE_PHOTOS_FIRST_LOAD_LIMIT = 10; const PROFILE_PHOTOS_FIRST_LOAD_LIMIT = 10;
const TOP_PEERS_REQUEST_COOLDOWN = 60; // 1 min
const runThrottledForSearch = throttle((cb) => cb(), 500, false); const runThrottledForSearch = throttle((cb) => cb(), 500, false);
addActionHandler('loadFullUser', async (global, actions, payload): Promise<void> => { addActionHandler('loadFullUser', async (global, actions, payload): Promise<void> => {
@ -105,32 +103,6 @@ addActionHandler('loadUser', async (global, actions, payload): Promise<void> =>
setGlobal(global); setGlobal(global);
}); });
addActionHandler('loadTopUsers', async (global): Promise<void> => {
const { topPeers: { lastRequestedAt } } = global;
if (!(!lastRequestedAt || getServerTime() - lastRequestedAt > TOP_PEERS_REQUEST_COOLDOWN)) {
return;
}
const result = await callApi('fetchTopUsers');
if (!result) {
return;
}
const { ids } = result;
global = getGlobal();
global = {
...global,
topPeers: {
...global.topPeers,
userIds: ids,
lastRequestedAt: getServerTime(),
},
};
setGlobal(global);
});
addActionHandler('loadContactList', async (global): Promise<void> => { addActionHandler('loadContactList', async (global): Promise<void> => {
const contactList = await callApi('fetchContactList'); const contactList = await callApi('fetchContactList');
if (!contactList) { if (!contactList) {

View File

@ -30,7 +30,9 @@ import {
getMessageText, getMessageText,
groupMessageIdsByThreadId, groupMessageIdsByThreadId,
isActionMessage, isActionMessage,
isDeletedUser,
isMessageLocal, isMessageLocal,
isUserBot,
pickMatchingTypingDraftMessage, pickMatchingTypingDraftMessage,
} from '../../helpers'; } from '../../helpers';
import { getMessageReplyInfo, getStoryReplyInfo } from '../../helpers/replies'; import { getMessageReplyInfo, getStoryReplyInfo } from '../../helpers/replies';
@ -99,6 +101,7 @@ import {
selectTabState, selectTabState,
selectTopic, selectTopic,
selectTopicFromMessage, selectTopicFromMessage,
selectUser,
selectViewportIds, selectViewportIds,
} from '../../selectors'; } from '../../selectors';
import { import {
@ -179,6 +182,25 @@ function removeTypingDraftEntries<T extends GlobalState>(
return global; return global;
} }
function shouldBumpGuestBotTopPeer<T extends GlobalState>(global: T, message: ApiMessage) {
const { guestChatViaId, senderId } = message;
if (message.isOutgoing || message.content.action || guestChatViaId !== global.currentUserId || !senderId) {
return false;
}
const sender = selectUser(global, senderId);
return Boolean(sender?.isGuestChatBot);
}
function shouldBumpInlineBotTopPeer(message: ApiMessage) {
return Boolean(message.isOutgoing && !message.content.action && message.viaBotId);
}
function shouldBumpCorrespondentTopPeer<T extends GlobalState>(global: T, chatId: string) {
const user = selectUser(global, chatId);
return Boolean(user && !user.isSelf && !isUserBot(user) && !isDeletedUser(user));
}
addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => { addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
switch (update['@type']) { switch (update['@type']) {
case 'newMessage': { case 'newMessage': {
@ -345,6 +367,22 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
setGlobal(global); setGlobal(global);
if (shouldBumpGuestBotTopPeer(global, newMessage)) {
actions.bumpTopPeerRating({
category: 'botsGuestChat',
peerId: newMessage.senderId!,
date: newMessage.date,
});
}
if (shouldBumpInlineBotTopPeer(newMessage)) {
actions.bumpTopPeerRating({
category: 'botsInline',
peerId: newMessage.viaBotId!,
date: newMessage.date,
});
}
// Reload dialogs if chat is not present in the list // Reload dialogs if chat is not present in the list
if (!isLocal && !chat?.isNotJoined && !selectIsChatListed(global, chatId)) { if (!isLocal && !chat?.isNotJoined && !selectIsChatListed(global, chatId)) {
actions.loadTopChats(); actions.loadTopChats();
@ -690,6 +728,10 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
setGlobal(global); setGlobal(global);
if (shouldBumpCorrespondentTopPeer(global, chatId)) {
actions.bumpTopPeerRating({ category: 'correspondents', peerId: chatId });
}
break; break;
} }
@ -725,6 +767,9 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
} }
setGlobal(global); setGlobal(global);
if (shouldBumpCorrespondentTopPeer(global, chatId)) {
actions.bumpTopPeerRating({ category: 'correspondents', peerId: chatId });
}
break; break;
} }

View File

@ -252,8 +252,8 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
if (!cached.chats.loadingParameters) { if (!cached.chats.loadingParameters) {
cached.chats.loadingParameters = initialState.chats.loadingParameters; cached.chats.loadingParameters = initialState.chats.loadingParameters;
} }
if (!cached.topBotApps) { if (!cached.topPeerCategories) {
cached.topBotApps = initialState.topBotApps; cached.topPeerCategories = initialState.topPeerCategories;
} }
if (!cached.reactions.defaultTags?.[0]?.type) { if (!cached.reactions.defaultTags?.[0]?.type) {
@ -443,9 +443,7 @@ function reduceGlobal<T extends GlobalState>(global: T) {
'attachMenu', 'attachMenu',
'currentUserId', 'currentUserId',
'contactList', 'contactList',
'topPeers', 'topPeerCategories',
'topInlineBots',
'topBotApps',
'recentEmojis', 'recentEmojis',
'recentCustomEmojis', 'recentCustomEmojis',
'push', 'push',
@ -560,6 +558,7 @@ function reduceUsers<T extends GlobalState>(global: T): GlobalState['users'] {
.filter((id): id is string => Boolean(id) && isUserId(id)); .filter((id): id is string => Boolean(id) && isUserId(id));
const attachBotIds = Object.keys(global.attachMenu?.bots || {}); const attachBotIds = Object.keys(global.attachMenu?.bots || {});
const topPeerIds = getTopPeerIds(global);
const idsToSave = unique([ const idsToSave = unique([
...currentUserId ? [currentUserId] : [], ...currentUserId ? [currentUserId] : [],
@ -567,7 +566,7 @@ function reduceUsers<T extends GlobalState>(global: T): GlobalState['users'] {
...chatStoriesUserIds, ...chatStoriesUserIds,
...visibleUserIds || [], ...visibleUserIds || [],
...attachBotIds, ...attachBotIds,
...global.topPeers.userIds || [], ...topPeerIds.filter(isUserId),
...global.recentlyFoundChatIds?.filter(isUserId) || [], ...global.recentlyFoundChatIds?.filter(isUserId) || [],
...getOrderedIds(ARCHIVED_FOLDER_ID)?.slice(0, GLOBAL_STATE_CACHE_ARCHIVED_CHAT_LIST_LIMIT).filter(isUserId) || [], ...getOrderedIds(ARCHIVED_FOLDER_ID)?.slice(0, GLOBAL_STATE_CACHE_ARCHIVED_CHAT_LIST_LIMIT).filter(isUserId) || [],
...getOrderedIds(ALL_FOLDER_ID)?.filter(isUserId) || [], ...getOrderedIds(ALL_FOLDER_ID)?.filter(isUserId) || [],
@ -608,11 +607,13 @@ function reduceChats<T extends GlobalState>(global: T): GlobalState['chats'] {
return content.storyData?.peerId || webPage?.story?.peerId || replyPeer; return content.storyData?.peerId || webPage?.story?.peerId || replyPeer;
}); });
})); }));
const topPeerIds = getTopPeerIds(global);
const unlinkedIdsToSave = [ const unlinkedIdsToSave = [
...currentUserId ? [currentUserId] : [], ...currentUserId ? [currentUserId] : [],
...currentChatIds, ...currentChatIds,
...messagesChatIds, ...messagesChatIds,
...topPeerIds,
...global.recentlyFoundChatIds || [], ...global.recentlyFoundChatIds || [],
...getOrderedIds(ARCHIVED_FOLDER_ID)?.slice(0, GLOBAL_STATE_CACHE_ARCHIVED_CHAT_LIST_LIMIT) || [], ...getOrderedIds(ARCHIVED_FOLDER_ID)?.slice(0, GLOBAL_STATE_CACHE_ARCHIVED_CHAT_LIST_LIMIT) || [],
...getOrderedIds(ALL_FOLDER_ID) || [], ...getOrderedIds(ALL_FOLDER_ID) || [],
@ -652,6 +653,10 @@ function reduceChats<T extends GlobalState>(global: T): GlobalState['chats'] {
}; };
} }
function getTopPeerIds<T extends GlobalState>(global: T) {
return unique(Object.values(global.topPeerCategories).flatMap((category) => category?.peerIds || []));
}
function reduceMessages<T extends GlobalState>(global: T): GlobalState['messages'] { function reduceMessages<T extends GlobalState>(global: T): GlobalState['messages'] {
const { currentUserId } = global; const { currentUserId } = global;
const byChatId: GlobalState['messages']['byChatId'] = {}; const byChatId: GlobalState['messages']['byChatId'] = {};

View File

@ -268,10 +268,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
saved: {}, saved: {},
}, },
topPeers: {}, topPeerCategories: {},
topInlineBots: {},
topBotApps: {},
activeSessions: { activeSessions: {
byHash: {}, byHash: {},

View File

@ -58,6 +58,7 @@ import type {
ApiStickerSetInfo, ApiStickerSetInfo,
ApiThemeParameters, ApiThemeParameters,
ApiTodoItem, ApiTodoItem,
ApiTopPeerCategory,
ApiTypeCurrencyAmount, ApiTypeCurrencyAmount,
ApiTypePrepaidGiveaway, ApiTypePrepaidGiveaway,
ApiUpdate, ApiUpdate,
@ -1914,7 +1915,6 @@ export interface ActionPayloads {
// Users // Users
loadNearestCountry: undefined; loadNearestCountry: undefined;
loadTopUsers: undefined;
loadContactList: undefined; loadContactList: undefined;
loadCurrentUser: undefined; loadCurrentUser: undefined;
@ -2164,8 +2164,19 @@ export interface ActionPayloads {
command: string; command: string;
chatId?: string; chatId?: string;
} & WithTabId; } & WithTabId;
loadTopInlineBots: undefined; loadTopPeers: {
loadTopBotApps: undefined; category: ApiTopPeerCategory;
force?: boolean;
};
removeTopPeer: {
category: ApiTopPeerCategory;
peerId: string;
};
bumpTopPeerRating: {
category: ApiTopPeerCategory;
peerId: string;
date?: number;
};
queryInlineBot: { queryInlineBot: {
chatId: string; chatId: string;
username: string; username: string;
@ -2175,6 +2186,7 @@ export interface ActionPayloads {
sendInlineBotResult: { sendInlineBotResult: {
id: string; id: string;
queryId: string; queryId: string;
botId?: string;
chatId: string; chatId: string;
threadId: ThreadId; threadId: ThreadId;
isSilent?: boolean; isSilent?: boolean;
@ -2185,6 +2197,7 @@ export interface ActionPayloads {
chat: ApiChat; chat: ApiChat;
id: string; id: string;
queryId: string; queryId: string;
botId?: string;
replyInfo?: ApiInputMessageReplyInfo; replyInfo?: ApiInputMessageReplyInfo;
sendAs?: ApiPeer; sendAs?: ApiPeer;
isSilent?: boolean; isSilent?: boolean;

View File

@ -46,6 +46,7 @@ import type {
ApiStoryAlbum, ApiStoryAlbum,
ApiTimezone, ApiTimezone,
ApiTonAmount, ApiTonAmount,
ApiTopPeerCategory,
ApiTranscription, ApiTranscription,
ApiUpdateAuthorizationStateType, ApiUpdateAuthorizationStateType,
ApiUpdateConnectionStateType, ApiUpdateConnectionStateType,
@ -418,20 +419,12 @@ export type GlobalState = {
}; };
}; };
topPeers: { topPeerCategories: Partial<Record<ApiTopPeerCategory, {
userIds?: string[]; peerIds: string[];
ratingsByPeerId: Record<string, number>;
lastRequestedAt?: number; lastRequestedAt?: number;
}; isDisabled?: boolean;
}>>;
topInlineBots: {
userIds?: string[];
lastRequestedAt?: number;
};
topBotApps: {
userIds?: string[];
lastRequestedAt?: number;
};
activeSessions: { activeSessions: {
byHash: Record<string, ApiSession>; byHash: Record<string, ApiSession>;

View File

@ -145,7 +145,13 @@ class TelegramClient {
if (request instanceof Api.contacts.GetTopPeers) { if (request instanceof Api.contacts.GetTopPeers) {
return new Api.contacts.TopPeers({ return new Api.contacts.TopPeers({
categories: [new Api.TopPeerCategoryPeers({ categories: [new Api.TopPeerCategoryPeers({
category: new Api.TopPeerCategoryCorrespondents(), category: request.botsInline
? new Api.TopPeerCategoryBotsInline()
: request.botsApp
? new Api.TopPeerCategoryBotsApp()
: request.botsGuestchat
? new Api.TopPeerCategoryBotsGuestChat()
: new Api.TopPeerCategoryCorrespondents(),
count: this.mockData.topPeers.length, count: this.mockData.topPeers.length,
peers: this.mockData.topPeers.map((id) => { peers: this.mockData.topPeers.map((id) => {
return new Api.TopPeer({ return new Api.TopPeer({