Bots: Support similar bots (#5476)

This commit is contained in:
Alexander Zinchuk 2025-01-21 18:21:36 +01:00
parent 36639f1c2a
commit ab5742bbc9
17 changed files with 180 additions and 8 deletions

View File

@ -4,7 +4,7 @@ src/lib/fasttextweb/fasttext-wasm.js
src/lib/gramjs/tl/types-generator/template.js
src/lib/gramjs/tl/api.d.ts
src/lib/gramjs/tl/apiTl.js
src/lib/gramjs/tl/apiTl.ts
src/lib/gramjs/tl/schemaTl.js
src/lib/lovely-chart

View File

@ -693,3 +693,21 @@ export async function fetchPopularAppBots({
nextOffset: result.nextOffset,
};
}
export async function fetchBotsRecommendations({ user }: { user: ApiChat }) {
if (!user) return undefined;
const inputUser = buildInputEntity(user.id, user.accessHash) as GramJs.InputUser;
const result = await invokeRequest(new GramJs.bots.GetBotRecommendations({
bot: inputUser,
}));
if (!result) {
return undefined;
}
const similarBots = result?.users.map(buildApiUser).filter(Boolean);
return {
similarBots,
count: result instanceof GramJs.users.UsersSlice ? result.count : similarBots.length,
};
}

View File

@ -1491,9 +1491,12 @@
"ProfileTabVoice" = "Voice";
"ProfileTabSharedGroups" = "Groups";
"ProfileTabSimilarChannels" = "Similar Channels";
"ProfileTabSimilarBots" = "Similar Bots";
"ActionUnsupportedTitle" = "Action not supported yet";
"ActionUnsupportedDescription" = "Please, use one of our apps to complete this action.";
"LocationPermissionText" = "**{name}** requests access to set your **location**. You will be able to revoke this access in the profile page of **{name}**.";
"UnlockMoreSimilarBots" = "Show More Apps";
"MoreSimilarBotsText" = "Subscribe to **Telegram Premium** to unlock up to {count} similar apps."
"GiftWasNotFound" = "Gift was not found";
"ViewButtonRequestJoin" = "REQUEST TO JOIN";
"ViewButtonMessage" = "VIEW MESSAGE";
@ -1508,5 +1511,4 @@
"ViewButtonStory" = "VIEW STORY";
"ViewButtonBoost" = "BOOST";
"ViewButtonStickerset" = "VIEW STICKERS";
"ViewButtonGiftUnique" = "VIEW COLLECTIBLE";
"ViewButtonGiftUnique" = "VIEW COLLECTIBLE";

View File

@ -122,6 +122,7 @@
}
&.similarChannels-list,
&.similarBots-list,
&.commonChats-list,
&.members-list,
&.gifts-list {
@ -134,11 +135,13 @@
}
}
&.similarBots-list,
&.similarChannels-list {
.ListItem.blured {
filter: opacity(0.8);
}
.show-more-bots,
.show-more-channels {
width: calc(100% - 1rem);
margin: 0 auto;

View File

@ -39,6 +39,7 @@ import {
isChatChannel,
isChatGroup,
isUserBot,
isUserId,
isUserRightBanned,
} from '../../global/helpers';
import {
@ -50,6 +51,7 @@ import {
selectIsCurrentUserPremium,
selectIsRightColumnShown,
selectPeerStories,
selectSimilarBotsIds,
selectSimilarChannelIds,
selectTabState,
selectTheme,
@ -112,6 +114,7 @@ type OwnProps = {
type StateProps = {
theme: ISettings['theme'];
isChannel?: boolean;
isBot?: boolean;
currentUserId?: string;
messagesById?: Record<number, ApiMessage>;
foundIds?: number[];
@ -142,9 +145,10 @@ type StateProps = {
nextProfileTab?: ProfileTabType;
shouldWarnAboutSvg?: boolean;
similarChannels?: string[];
similarBots?: string[];
botPreviewMedia? : ApiBotPreviewMedia[];
isCurrentUserPremium?: boolean;
limitSimilarChannels: number;
limitSimilarPeers: number;
isTopicInfo?: boolean;
isSavedDialog?: boolean;
forceScrollProfileTab?: boolean;
@ -172,6 +176,7 @@ const Profile: FC<OwnProps & StateProps> = ({
profileState,
theme,
isChannel,
isBot,
currentUserId,
messagesById,
foundIds,
@ -203,8 +208,9 @@ const Profile: FC<OwnProps & StateProps> = ({
nextProfileTab,
shouldWarnAboutSvg,
similarChannels,
similarBots,
isCurrentUserPremium,
limitSimilarChannels,
limitSimilarPeers,
isTopicInfo,
isSavedDialog,
forceScrollProfileTab,
@ -225,6 +231,7 @@ const Profile: FC<OwnProps & StateProps> = ({
loadStoriesArchive,
openPremiumModal,
loadChannelRecommendations,
loadBotRecommendations,
loadPreviewMedias,
loadUserGifts,
} = getActions();
@ -283,13 +290,17 @@ const Profile: FC<OwnProps & StateProps> = ({
arr.push({ type: 'similarChannels', key: 'ProfileTabSimilarChannels' });
}
if (isBot && similarBots?.length) {
arr.push({ type: 'similarBots', key: 'ProfileTabSimilarBots' });
}
return arr.map((tab) => ({
type: tab.type,
title: lang(tab.key),
}));
}, [
isSavedMessages, isSavedDialog, hasStoriesTab, hasGiftsTab, hasMembersTab, hasPreviewMediaTab, isTopicInfo,
hasCommonChatsTab, isChannel, similarChannels?.length, lang,
hasCommonChatsTab, isChannel, isBot, similarChannels?.length, similarBots?.length, lang,
]);
const initialTab = useMemo(() => {
@ -330,6 +341,12 @@ const Profile: FC<OwnProps & StateProps> = ({
}
}, [chatId, isChannel, similarChannels, isSynced]);
useEffect(() => {
if (isBot && !similarBots && isSynced) {
loadBotRecommendations({ userId: chatId });
}
}, [chatId, isBot, similarBots, isSynced]);
const giftIds = useMemo(() => {
return gifts?.map(({ date, gift, fromId }) => `${date}-${fromId}-${gift.id}`);
}, [gifts]);
@ -371,6 +388,7 @@ const Profile: FC<OwnProps & StateProps> = ({
pinnedStoryIds,
archiveStoryIds,
similarChannels,
similarBots,
});
const isFirstTab = (isSavedMessages && resultType === 'dialogs')
|| (hasStoriesTab && resultType === 'stories')
@ -706,7 +724,49 @@ const Profile: FC<OwnProps & StateProps> = ({
<i className="icon icon-unlock-badge" />
</Button>
<div className="more-similar">
{renderText(oldLang('MoreSimilarText', limitSimilarChannels), ['simple_markdown'])}
{renderText(oldLang('MoreSimilarText', limitSimilarPeers), ['simple_markdown'])}
</div>
</>
)}
</div>
) : resultType === 'similarBots' ? (
<div key={resultType}>
{(viewportIds as string[])!.map((userId, i) => (
<ListItem
key={userId}
teactOrderKey={i}
className={buildClassName(
'chat-item-clickable search-result',
!isCurrentUserPremium && i === similarBots!.length - 1 && 'blured',
)}
// eslint-disable-next-line react/jsx-no-bind
onClick={() => openChat({ id: userId })}
>
{isUserId(userId) ? (
<PrivateChatInfo
userId={userId}
avatarSize="medium"
/>
) : (
<GroupChatInfo
chatId={userId}
avatarSize="medium"
/>
)}
</ListItem>
))}
{!isCurrentUserPremium && (
<>
{/* eslint-disable-next-line react/jsx-no-bind */}
<Button className="show-more-bots" size="smaller" onClick={() => openPremiumModal()}>
{lang('UnlockMoreSimilarBots')}
<i className="icon icon-unlock-badge" />
</Button>
<div className="more-similar">
{renderText(lang('MoreSimilarBotsText', { count: limitSimilarPeers }, {
withNodes: true,
withMarkdown: true,
}))}
</div>
</>
)}
@ -814,6 +874,7 @@ export default memo(withGlobal<OwnProps>(
const isGroup = chat && isChatGroup(chat);
const isChannel = chat && isChatChannel(chat);
const isBot = user && isUserBot(user);
const hasMembersTab = !isTopicInfo && !isSavedDialog && (isGroup || (isChannel && isChatAdmin(chat!)));
const members = chatFullInfo?.members;
const adminMembersById = chatFullInfo?.adminMembersById;
@ -825,6 +886,7 @@ export default memo(withGlobal<OwnProps>(
const canDeleteMembers = hasMembersTab && chat && (getHasAdminRight(chat, 'banUsers') || chat.isCreator);
const activeDownloads = selectActiveDownloads(global);
const { similarChannelIds } = selectSimilarChannelIds(global, chatId) || {};
const { similarBotsIds } = selectSimilarBotsIds(global, chatId) || {};
const isCurrentUserPremium = selectIsCurrentUserPremium(global);
const peer = user || chat;
@ -851,6 +913,7 @@ export default memo(withGlobal<OwnProps>(
return {
theme: selectTheme(global),
isChannel,
isBot,
messagesById,
foundIds,
mediaSearchType,
@ -879,12 +942,13 @@ export default memo(withGlobal<OwnProps>(
forceScrollProfileTab: selectTabState(global).forceScrollProfileTab,
shouldWarnAboutSvg: global.settings.byKey.shouldWarnAboutSvg,
similarChannels: similarChannelIds,
similarBots: similarBotsIds,
botPreviewMedia,
isCurrentUserPremium,
isTopicInfo,
isSavedDialog,
isSynced: global.isSynced,
limitSimilarChannels: selectPremiumLimit(global, 'recommendedChannels'),
limitSimilarPeers: selectPremiumLimit(global, 'recommendedChannels'),
...(hasMembersTab && members && { members, adminMembersById }),
...(hasCommonChatsTab && user && { commonChatIds: commonChats?.ids }),
};

View File

@ -34,6 +34,7 @@ export default function useProfileViewportIds({
pinnedStoryIds,
archiveStoryIds,
similarChannels,
similarBots,
} : {
loadMoreMembers: AnyToVoidFunction;
loadCommonChats: AnyToVoidFunction;
@ -56,6 +57,7 @@ export default function useProfileViewportIds({
pinnedStoryIds?: number[];
archiveStoryIds?: number[];
similarChannels?: string[];
similarBots?: string[];
}) {
const resultType = tabType === 'members' || !mediaSearchType ? tabType : mediaSearchType;
@ -184,6 +186,9 @@ export default function useProfileViewportIds({
case 'similarChannels':
viewportIds = similarChannels;
break;
case 'similarBots':
viewportIds = similarBots;
break;
case 'gifts':
viewportIds = giftIds;
getMore = loadMoreGifts;

View File

@ -60,6 +60,7 @@ import {
addChatMembers,
addChats,
addMessages,
addSimilarBots,
addSimilarChannels,
addUsers,
addUserStatuses,
@ -2694,6 +2695,32 @@ addActionHandler('loadChannelRecommendations', async (global, actions, payload):
setGlobal(global);
});
addActionHandler('loadBotRecommendations', async (global, actions, payload): Promise<void> => {
const { userId } = payload;
const user = selectChat(global, userId);
if (!user) {
return;
}
const result = await callApi('fetchBotsRecommendations', {
user,
});
if (!result) {
return;
}
const { similarBots, count } = result;
const users = buildCollectionByKey(similarBots, 'id');
global = getGlobal();
global = addUsers(global, users);
global = addSimilarBots(global, userId, Object.keys(users), count);
setGlobal(global);
});
addActionHandler('toggleChannelRecommendations', (global, actions, payload): ActionReturnType => {
const { chatId } = payload;
const chat = selectChat(global, chatId);

View File

@ -220,6 +220,10 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) {
cached.chats.similarChannelsById = initialState.chats.similarChannelsById;
}
if (!cached.chats.similarBotsById) {
cached.chats.similarBotsById = initialState.chats.similarBotsById;
}
if (!cached.chats.lastMessageIds) {
cached.chats.lastMessageIds = initialState.chats.lastMessageIds;
}
@ -472,6 +476,7 @@ function reduceChats<T extends GlobalState>(global: T): GlobalState['chats'] {
return {
...global.chats,
similarChannelsById: {},
similarBotsById: {},
isFullyLoaded: {},
loadingParameters: INITIAL_GLOBAL_STATE.chats.loadingParameters,
byId: pickTruthy(global.chats.byId, idsToSave),

View File

@ -115,6 +115,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = {
byId: {},
fullInfoById: {},
similarChannelsById: {},
similarBotsById: {},
topicsInfoById: {},
loadingParameters: {
active: {},

View File

@ -463,3 +463,24 @@ export function toggleSimilarChannels<T extends GlobalState>(
},
};
}
export function addSimilarBots<T extends GlobalState>(
global: T,
chatId: string,
similarBotsIds: string[],
count?: number,
) {
return {
...global,
chats: {
...global.chats,
similarBotsById: {
...global.chats.similarBotsById,
[chatId]: {
similarBotsIds,
count,
},
},
},
};
}

View File

@ -323,6 +323,13 @@ export function selectSimilarChannelIds<T extends GlobalState>(
return global.chats.similarChannelsById[chatId];
}
export function selectSimilarBotsIds<T extends GlobalState>(
global: T,
chatId: string,
) {
return global.chats.similarBotsById[chatId];
}
export function selectChatLastMessageId<T extends GlobalState>(
global: T, chatId: string, listType: 'all' | 'saved' = 'all',
) {

View File

@ -1008,6 +1008,9 @@ export interface ActionPayloads {
loadChannelRecommendations: {
chatId?: string;
};
loadBotRecommendations: {
userId: string;
};
toggleChannelRecommendations: {
chatId: string;
};

View File

@ -57,6 +57,7 @@ import type {
PerformanceType,
Point,
ServiceNotification,
SimilarBotsInfo,
Size,
StarGiftCategory,
StarsSubscriptions,
@ -223,6 +224,8 @@ export type GlobalState = {
count: number;
}
>;
similarBotsById: Record<string, SimilarBotsInfo>;
};
messages: {

View File

@ -1682,6 +1682,7 @@ channels.getChannelRecommendations#25a71742 flags:# channel:flags.0?InputChannel
channels.searchPosts#d19f987b hashtag:string offset_rate:int offset_peer:InputPeer offset_id:int limit:int = messages.Messages;
bots.setBotInfo#10cf3123 flags:# bot:flags.2?InputUser lang_code:string name:flags.3?string about:flags.0?string description:flags.1?string = Bool;
bots.canSendMessage#1359f4e6 bot:InputUser = Bool;
bots.getBotRecommendations#a1b70815 bot:InputUser = users.Users;
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;

View File

@ -271,6 +271,7 @@
"channels.getChannelRecommendations",
"channels.searchPosts",
"channels.reportSpam",
"bots.getBotRecommendations",
"bots.canSendMessage",
"bots.allowSendMessage",
"bots.invokeWebViewCustomMethod",

View File

@ -375,6 +375,7 @@ export type ProfileTabType =
| 'stories'
| 'storiesArchive'
| 'similarChannels'
| 'similarBots'
| 'dialogs'
| 'gifts';
export type SharedMediaType = 'media' | 'documents' | 'links' | 'audio' | 'voice';
@ -619,6 +620,11 @@ export type ChatRequestedTranslations = {
manualMessages?: Record<number, string>;
};
export type SimilarBotsInfo = {
similarBotsIds?: string[];
count: number;
};
export type ConfettiParams = OptionalCombine<{
style?: ConfettiStyle;
withStars?: boolean;

View File

@ -1220,8 +1220,10 @@ export interface LangPair {
'ProfileTabVoice': undefined;
'ProfileTabSharedGroups': undefined;
'ProfileTabSimilarChannels': undefined;
'ProfileTabSimilarBots': undefined;
'ActionUnsupportedTitle': undefined;
'ActionUnsupportedDescription': undefined;
'UnlockMoreSimilarBots': undefined;
'GiftWasNotFound': undefined;
'ViewButtonRequestJoin': undefined;
'ViewButtonMessage': undefined;
@ -1705,6 +1707,9 @@ export interface LangPairWithVariables<V extends unknown = LangVariable> {
'LocationPermissionText': {
'name': V;
};
'MoreSimilarBotsText': {
'count': V;
};
}
export interface LangPairPlural {