diff --git a/.eslintignore b/.eslintignore index de4fb1180..d82cb90fb 100644 --- a/.eslintignore +++ b/.eslintignore @@ -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 diff --git a/src/api/gramjs/methods/bots.ts b/src/api/gramjs/methods/bots.ts index ce1c28788..d348ff957 100644 --- a/src/api/gramjs/methods/bots.ts +++ b/src/api/gramjs/methods/bots.ts @@ -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, + }; +} diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index 9e4a5b2a0..529a9b899 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -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"; \ No newline at end of file diff --git a/src/components/right/Profile.scss b/src/components/right/Profile.scss index d42c51f0c..036d537e2 100644 --- a/src/components/right/Profile.scss +++ b/src/components/right/Profile.scss @@ -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; diff --git a/src/components/right/Profile.tsx b/src/components/right/Profile.tsx index e002fbf71..a0e56eeba 100644 --- a/src/components/right/Profile.tsx +++ b/src/components/right/Profile.tsx @@ -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; 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 = ({ profileState, theme, isChannel, + isBot, currentUserId, messagesById, foundIds, @@ -203,8 +208,9 @@ const Profile: FC = ({ nextProfileTab, shouldWarnAboutSvg, similarChannels, + similarBots, isCurrentUserPremium, - limitSimilarChannels, + limitSimilarPeers, isTopicInfo, isSavedDialog, forceScrollProfileTab, @@ -225,6 +231,7 @@ const Profile: FC = ({ loadStoriesArchive, openPremiumModal, loadChannelRecommendations, + loadBotRecommendations, loadPreviewMedias, loadUserGifts, } = getActions(); @@ -283,13 +290,17 @@ const Profile: FC = ({ 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 = ({ } }, [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 = ({ pinnedStoryIds, archiveStoryIds, similarChannels, + similarBots, }); const isFirstTab = (isSavedMessages && resultType === 'dialogs') || (hasStoriesTab && resultType === 'stories') @@ -706,7 +724,49 @@ const Profile: FC = ({
- {renderText(oldLang('MoreSimilarText', limitSimilarChannels), ['simple_markdown'])} + {renderText(oldLang('MoreSimilarText', limitSimilarPeers), ['simple_markdown'])} +
+ + )} + + ) : resultType === 'similarBots' ? ( +
+ {(viewportIds as string[])!.map((userId, i) => ( + openChat({ id: userId })} + > + {isUserId(userId) ? ( + + ) : ( + + )} + + ))} + {!isCurrentUserPremium && ( + <> + {/* eslint-disable-next-line react/jsx-no-bind */} + +
+ {renderText(lang('MoreSimilarBotsText', { count: limitSimilarPeers }, { + withNodes: true, + withMarkdown: true, + }))}
)} @@ -814,6 +874,7 @@ export default memo(withGlobal( 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( 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( return { theme: selectTheme(global), isChannel, + isBot, messagesById, foundIds, mediaSearchType, @@ -879,12 +942,13 @@ export default memo(withGlobal( 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 }), }; diff --git a/src/components/right/hooks/useProfileViewportIds.ts b/src/components/right/hooks/useProfileViewportIds.ts index 2cf1b16e8..3f5122792 100644 --- a/src/components/right/hooks/useProfileViewportIds.ts +++ b/src/components/right/hooks/useProfileViewportIds.ts @@ -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; diff --git a/src/global/actions/api/chats.ts b/src/global/actions/api/chats.ts index 96959bf1f..800ea28a2 100644 --- a/src/global/actions/api/chats.ts +++ b/src/global/actions/api/chats.ts @@ -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 => { + 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); diff --git a/src/global/cache.ts b/src/global/cache.ts index 8fffbc248..6fabcc232 100644 --- a/src/global/cache.ts +++ b/src/global/cache.ts @@ -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(global: T): GlobalState['chats'] { return { ...global.chats, similarChannelsById: {}, + similarBotsById: {}, isFullyLoaded: {}, loadingParameters: INITIAL_GLOBAL_STATE.chats.loadingParameters, byId: pickTruthy(global.chats.byId, idsToSave), diff --git a/src/global/initialState.ts b/src/global/initialState.ts index 6755ef20e..b4a93ac1a 100644 --- a/src/global/initialState.ts +++ b/src/global/initialState.ts @@ -115,6 +115,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = { byId: {}, fullInfoById: {}, similarChannelsById: {}, + similarBotsById: {}, topicsInfoById: {}, loadingParameters: { active: {}, diff --git a/src/global/reducers/chats.ts b/src/global/reducers/chats.ts index 255ff02ba..63a5baf93 100644 --- a/src/global/reducers/chats.ts +++ b/src/global/reducers/chats.ts @@ -463,3 +463,24 @@ export function toggleSimilarChannels( }, }; } + +export function addSimilarBots( + global: T, + chatId: string, + similarBotsIds: string[], + count?: number, +) { + return { + ...global, + chats: { + ...global.chats, + similarBotsById: { + ...global.chats.similarBotsById, + [chatId]: { + similarBotsIds, + count, + }, + }, + }, + }; +} diff --git a/src/global/selectors/chats.ts b/src/global/selectors/chats.ts index 0ee2beac9..03dd4c6c9 100644 --- a/src/global/selectors/chats.ts +++ b/src/global/selectors/chats.ts @@ -323,6 +323,13 @@ export function selectSimilarChannelIds( return global.chats.similarChannelsById[chatId]; } +export function selectSimilarBotsIds( + global: T, + chatId: string, +) { + return global.chats.similarBotsById[chatId]; +} + export function selectChatLastMessageId( global: T, chatId: string, listType: 'all' | 'saved' = 'all', ) { diff --git a/src/global/types/actions.ts b/src/global/types/actions.ts index 19991ecf5..0833444be 100644 --- a/src/global/types/actions.ts +++ b/src/global/types/actions.ts @@ -1008,6 +1008,9 @@ export interface ActionPayloads { loadChannelRecommendations: { chatId?: string; }; + loadBotRecommendations: { + userId: string; + }; toggleChannelRecommendations: { chatId: string; }; diff --git a/src/global/types/globalState.ts b/src/global/types/globalState.ts index 6b8dfce89..570c3a53c 100644 --- a/src/global/types/globalState.ts +++ b/src/global/types/globalState.ts @@ -57,6 +57,7 @@ import type { PerformanceType, Point, ServiceNotification, + SimilarBotsInfo, Size, StarGiftCategory, StarsSubscriptions, @@ -223,6 +224,8 @@ export type GlobalState = { count: number; } >; + + similarBotsById: Record; }; messages: { diff --git a/src/lib/gramjs/tl/apiTl.ts b/src/lib/gramjs/tl/apiTl.ts index 2ff5e083f..cea5e053e 100644 --- a/src/lib/gramjs/tl/apiTl.ts +++ b/src/lib/gramjs/tl/apiTl.ts @@ -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; diff --git a/src/lib/gramjs/tl/static/api.json b/src/lib/gramjs/tl/static/api.json index 43de60066..f07f2114b 100644 --- a/src/lib/gramjs/tl/static/api.json +++ b/src/lib/gramjs/tl/static/api.json @@ -271,6 +271,7 @@ "channels.getChannelRecommendations", "channels.searchPosts", "channels.reportSpam", + "bots.getBotRecommendations", "bots.canSendMessage", "bots.allowSendMessage", "bots.invokeWebViewCustomMethod", diff --git a/src/types/index.ts b/src/types/index.ts index cba7be714..2d618160d 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -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; }; +export type SimilarBotsInfo = { + similarBotsIds?: string[]; + count: number; +}; + export type ConfettiParams = OptionalCombine<{ style?: ConfettiStyle; withStars?: boolean; diff --git a/src/types/language.d.ts b/src/types/language.d.ts index c7a1c6f6c..ddb05bce1 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -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 { 'LocationPermissionText': { 'name': V; }; + 'MoreSimilarBotsText': { + 'count': V; + }; } export interface LangPairPlural {