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 {
const {
testMode, expires, gifSearchUsername, chatSizeMax, autologinToken, reactionsDefault,
messageLengthMax, editTimeLimit, forwardedCountMax,
messageLengthMax, editTimeLimit, forwardedCountMax, ratingEDecay,
} = config;
const defaultReaction = reactionsDefault && buildApiReaction(reactionsDefault);
return {
@ -224,6 +224,7 @@ export function buildApiConfig(config: GramJs.Config): ApiConfig {
maxMessageLength: messageLengthMax,
editTimeLimit,
maxForwardedCount: forwardedCountMax,
ratingEDecay,
};
}

View File

@ -23,7 +23,6 @@ import {
buildBotSwitchPm,
buildBotSwitchWebview,
} from '../apiBuilders/bots';
import { buildApiChatFromPreview } from '../apiBuilders/chats';
import { omitVirtualClassFields } from '../apiBuilders/helpers';
import { buildMessageMediaContent } from '../apiBuilders/messageContent';
import { buildApiUrlAuthResult } from '../apiBuilders/misc';
@ -39,7 +38,6 @@ import {
import {
addDocumentToLocalDb,
addPhotoToLocalDb,
addUserToLocalDb,
addWebDocumentToLocalDb,
} from '../helpers/localDb';
import { deserializeBytes } from '../helpers/misc';
@ -61,68 +59,6 @@ export async function answerCallbackButton({
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({
bot, chat, query, offset = DEFAULT_PRIMITIVES.STRING,
}: {

View File

@ -21,6 +21,8 @@ export * from './messages';
export * from './users';
export * from './topPeers';
export * from './symbols';
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;
}
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() {
const result = await invokeRequest(new GramJs.contacts.GetContacts({ hash: DEFAULT_PRIMITIVES.BIGINT }));
if (!result || result instanceof GramJs.contacts.ContactsNotModified) {

View File

@ -351,8 +351,26 @@ export interface ApiConfig {
maxMessageLength: number;
editTimeLimit: 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 {
expires: number;
pendingSuggestions: string[];

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -23,12 +23,14 @@ import { formatStarsAsText } from '../../../util/localization/format';
import { oldTranslate } from '../../../util/oldLangProvider';
import requestActionTimeout from '../../../util/requestActionTimeout';
import { debounce } from '../../../util/schedulers';
import { getServerTime } from '../../../util/serverTime';
import { extractCurrentThemeParams } from '../../../util/themeStyle';
import { callApi } from '../../../api/gramjs';
import {
getMainUsername,
getWebAppKey,
isChatAdmin,
isUserBot,
isUserRightBanned,
prepareMessageReplyInfo,
} from '../../helpers';
import {
@ -52,6 +54,7 @@ import { updateTabState } from '../../reducers/tabs';
import {
selectBot,
selectChat,
selectChatFullInfo,
selectChatLastMessageId,
selectChatMessage,
selectCurrentChat,
@ -73,10 +76,13 @@ import { getPeerStarsForMessage } from './messages';
import { getIsWebAppsFullscreenSupported } from '../../../hooks/useAppLayout';
const TOP_PEERS_REQUEST_COOLDOWN = 60; // 1 min
const runDebouncedForSearch = debounce((cb) => cb(), 500, false);
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 => {
const {
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);
});
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> => {
const {
chatId, username, query, offset,
tabId = getCurrentTabId(),
} = payload;
const chat = selectChat(global, chatId);
if (!chat || !canUseInlineBots(global, chat)) {
return;
}
let inlineBotData = selectTabState(global, tabId).inlineBots.byUsername[username];
if (inlineBotData === false) {
return;
}
if (inlineBotData === undefined) {
const { user: inlineBot, chat } = await callApi('fetchInlineBot', { username }) || {};
const { user: inlineBot } = await callApi('getChatByUsername', username) || {};
global = getGlobal();
if (!inlineBot || !chat) {
if (!inlineBot || !isUserBot(inlineBot) || !inlineBot.botPlaceholder) {
global = replaceInlineBotSettings(global, username, false, tabId);
setGlobal(global);
return;
@ -818,6 +779,7 @@ addActionHandler('requestMainWebView', async (global, actions, payload): Promise
};
global = addWebAppToOpenList(global, newActiveApp, true, true, tabId);
setGlobal(global);
actions.bumpTopPeerRating({ category: 'botsApp', peerId: botId });
if (isFullscreen && getIsWebAppsFullscreenSupported()) {
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 * as langProvider from '../../../util/oldLangProvider';
import { throttle } from '../../../util/schedulers';
import { getServerTime } from '../../../util/serverTime';
import { callApi } from '../../../api/gramjs';
import { isUserBot } from '../../helpers';
import { addActionHandler, getGlobal, setGlobal } from '../../index';
@ -43,7 +42,6 @@ import {
} from '../../selectors';
const PROFILE_PHOTOS_FIRST_LOAD_LIMIT = 10;
const TOP_PEERS_REQUEST_COOLDOWN = 60; // 1 min
const runThrottledForSearch = throttle((cb) => cb(), 500, false);
addActionHandler('loadFullUser', async (global, actions, payload): Promise<void> => {
@ -105,32 +103,6 @@ addActionHandler('loadUser', async (global, actions, payload): Promise<void> =>
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> => {
const contactList = await callApi('fetchContactList');
if (!contactList) {

View File

@ -30,7 +30,9 @@ import {
getMessageText,
groupMessageIdsByThreadId,
isActionMessage,
isDeletedUser,
isMessageLocal,
isUserBot,
pickMatchingTypingDraftMessage,
} from '../../helpers';
import { getMessageReplyInfo, getStoryReplyInfo } from '../../helpers/replies';
@ -99,6 +101,7 @@ import {
selectTabState,
selectTopic,
selectTopicFromMessage,
selectUser,
selectViewportIds,
} from '../../selectors';
import {
@ -179,6 +182,25 @@ function removeTypingDraftEntries<T extends GlobalState>(
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 => {
switch (update['@type']) {
case 'newMessage': {
@ -345,6 +367,22 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
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
if (!isLocal && !chat?.isNotJoined && !selectIsChatListed(global, chatId)) {
actions.loadTopChats();
@ -690,6 +728,10 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
setGlobal(global);
if (shouldBumpCorrespondentTopPeer(global, chatId)) {
actions.bumpTopPeerRating({ category: 'correspondents', peerId: chatId });
}
break;
}
@ -725,6 +767,9 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
}
setGlobal(global);
if (shouldBumpCorrespondentTopPeer(global, chatId)) {
actions.bumpTopPeerRating({ category: 'correspondents', peerId: chatId });
}
break;
}

View File

@ -252,8 +252,8 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
if (!cached.chats.loadingParameters) {
cached.chats.loadingParameters = initialState.chats.loadingParameters;
}
if (!cached.topBotApps) {
cached.topBotApps = initialState.topBotApps;
if (!cached.topPeerCategories) {
cached.topPeerCategories = initialState.topPeerCategories;
}
if (!cached.reactions.defaultTags?.[0]?.type) {
@ -443,9 +443,7 @@ function reduceGlobal<T extends GlobalState>(global: T) {
'attachMenu',
'currentUserId',
'contactList',
'topPeers',
'topInlineBots',
'topBotApps',
'topPeerCategories',
'recentEmojis',
'recentCustomEmojis',
'push',
@ -560,6 +558,7 @@ function reduceUsers<T extends GlobalState>(global: T): GlobalState['users'] {
.filter((id): id is string => Boolean(id) && isUserId(id));
const attachBotIds = Object.keys(global.attachMenu?.bots || {});
const topPeerIds = getTopPeerIds(global);
const idsToSave = unique([
...currentUserId ? [currentUserId] : [],
@ -567,7 +566,7 @@ function reduceUsers<T extends GlobalState>(global: T): GlobalState['users'] {
...chatStoriesUserIds,
...visibleUserIds || [],
...attachBotIds,
...global.topPeers.userIds || [],
...topPeerIds.filter(isUserId),
...global.recentlyFoundChatIds?.filter(isUserId) || [],
...getOrderedIds(ARCHIVED_FOLDER_ID)?.slice(0, GLOBAL_STATE_CACHE_ARCHIVED_CHAT_LIST_LIMIT).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;
});
}));
const topPeerIds = getTopPeerIds(global);
const unlinkedIdsToSave = [
...currentUserId ? [currentUserId] : [],
...currentChatIds,
...messagesChatIds,
...topPeerIds,
...global.recentlyFoundChatIds || [],
...getOrderedIds(ARCHIVED_FOLDER_ID)?.slice(0, GLOBAL_STATE_CACHE_ARCHIVED_CHAT_LIST_LIMIT) || [],
...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'] {
const { currentUserId } = global;
const byChatId: GlobalState['messages']['byChatId'] = {};

View File

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

View File

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

View File

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

View File

@ -145,7 +145,13 @@ class TelegramClient {
if (request instanceof Api.contacts.GetTopPeers) {
return new Api.contacts.TopPeers({
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,
peers: this.mockData.topPeers.map((id) => {
return new Api.TopPeer({