diff --git a/src/api/gramjs/apiBuilders/bots.ts b/src/api/gramjs/apiBuilders/bots.ts index 385000ef4..77775f7f5 100644 --- a/src/api/gramjs/apiBuilders/bots.ts +++ b/src/api/gramjs/apiBuilders/bots.ts @@ -110,7 +110,7 @@ function buildApiAttachMenuIcon(icon: GramJs.AttachMenuBotIcon): ApiAttachBotIco export function buildApiBotInfo(botInfo: GramJs.BotInfo, chatId: string): ApiBotInfo { const { - description, descriptionPhoto, descriptionDocument, userId, commands, menuButton, + description, descriptionPhoto, descriptionDocument, userId, commands, menuButton, hasPreviewMedias, } = botInfo; const botId = userId && buildApiPeerId(userId, 'user'); @@ -126,6 +126,7 @@ export function buildApiBotInfo(botInfo: GramJs.BotInfo, chatId: string): ApiBot photo, menuButton: buildApiBotMenuButton(menuButton), commands: commandsArray?.length ? commandsArray : undefined, + hasPreviewMedia: hasPreviewMedias, }; } diff --git a/src/api/gramjs/apiBuilders/users.ts b/src/api/gramjs/apiBuilders/users.ts index bb4a11b19..950aa30bd 100644 --- a/src/api/gramjs/apiBuilders/users.ts +++ b/src/api/gramjs/apiBuilders/users.ts @@ -81,6 +81,7 @@ export function buildApiUser(mtpUser: GramJs.TypeUser): ApiUser | undefined { type: userType, firstName, lastName, + hasMainMiniApp: Boolean(mtpUser.botHasMainApp), canEditBot: botCanEdit, ...(userType === 'userTypeBot' && { canBeInvitedToGroup: !mtpUser.botNochats }), ...(usernames && { usernames }), diff --git a/src/api/gramjs/methods/bots.ts b/src/api/gramjs/methods/bots.ts index 42c73e192..1166e85f9 100644 --- a/src/api/gramjs/methods/bots.ts +++ b/src/api/gramjs/methods/bots.ts @@ -3,6 +3,7 @@ import { Api as GramJs } from '../../../lib/gramjs'; import type { ApiBotApp, + ApiBotPreviewMedia, ApiChat, ApiInputMessageReplyInfo, ApiPeer, @@ -23,6 +24,7 @@ import { } from '../apiBuilders/bots'; import { buildApiChatFromPreview } from '../apiBuilders/chats'; import { omitVirtualClassFields } from '../apiBuilders/helpers'; +import { buildMessageMediaContent } from '../apiBuilders/messageContent'; import { buildApiUrlAuthResult } from '../apiBuilders/misc'; import { buildApiUser } from '../apiBuilders/users'; import { @@ -238,6 +240,35 @@ export async function requestWebView({ return undefined; } +export async function requestMainWebView({ + peer, + bot, + startParam, + theme, +}: { + peer: ApiPeer; + bot: ApiUser; + startParam?: string; + theme?: ApiThemeParameters; +}) { + const result = await invokeRequest(new GramJs.messages.RequestMainWebView({ + peer: buildInputPeer(peer.id, peer.accessHash), + bot: buildInputPeer(bot.id, bot.accessHash), + startParam, + themeParams: theme ? buildInputThemeParams(theme) : undefined, + platform: WEB_APP_PLATFORM, + })); + + if (!(result instanceof GramJs.WebViewResultUrl)) { + return undefined; + } + + return { + url: result.url, + queryId: result.queryId?.toString(), + }; +} + export async function requestSimpleWebView({ bot, url, @@ -551,6 +582,22 @@ export async function invokeWebViewCustomMethod({ } } +export async function fetchPreviewMedias({ bot } : { bot: ApiUser }) { + const result = await invokeRequest(new GramJs.bots.GetPreviewMedias({ + bot: buildInputPeer(bot.id, bot.accessHash), + })); + + if (!result) return undefined; + + const previews: ApiBotPreviewMedia[] = result.map((preview) => { + return { + content: buildMessageMediaContent(preview.media)!, + date: preview.date, + }; + }); + return previews; +} + function processInlineBotResult(queryId: string, results: GramJs.TypeBotInlineResult[]) { return results.map((result) => { if (result instanceof GramJs.BotInlineMediaResult) { diff --git a/src/api/gramjs/methods/index.ts b/src/api/gramjs/methods/index.ts index 56badbd71..cc3ed5899 100644 --- a/src/api/gramjs/methods/index.ts +++ b/src/api/gramjs/methods/index.ts @@ -65,8 +65,8 @@ export { } from './twoFaSettings'; export { - answerCallbackButton, setBotInfo, fetchTopInlineBots, fetchInlineBot, fetchInlineBotResults, - sendInlineBotResult, startBot, fetchPopularAppBots, fetchTopBotApps, + answerCallbackButton, setBotInfo, fetchTopInlineBots, fetchPreviewMedias, fetchInlineBot, fetchInlineBotResults, + sendInlineBotResult, startBot, requestMainWebView, fetchPopularAppBots, fetchTopBotApps, requestWebView, requestSimpleWebView, sendWebViewData, prolongWebView, loadAttachBots, toggleAttachBot, fetchBotApp, requestBotUrlAuth, requestLinkUrlAuth, acceptBotUrlAuth, acceptLinkUrlAuth, loadAttachBot, requestAppWebView, allowBotSendMessages, fetchBotCanSendMessage, invokeWebViewCustomMethod, diff --git a/src/api/types/bots.ts b/src/api/types/bots.ts index 127aa56bf..15af7c798 100644 --- a/src/api/types/bots.ts +++ b/src/api/types/bots.ts @@ -1,6 +1,6 @@ import type { ApiDimensions, - ApiPhoto, ApiSticker, ApiThumbnail, ApiVideo, + ApiPhoto, ApiSticker, ApiThumbnail, ApiVideo, MediaContainer, } from './messages'; export type ApiInlineResultType = ( @@ -74,4 +74,9 @@ export interface ApiBotInfo { photo?: ApiPhoto; gif?: ApiVideo; menuButton: ApiBotMenuButton; + hasPreviewMedia?: true; +} + +export interface ApiBotPreviewMedia extends MediaContainer { + date: number; } diff --git a/src/api/types/users.ts b/src/api/types/users.ts index ad71d5fe1..e305ec30d 100644 --- a/src/api/types/users.ts +++ b/src/api/types/users.ts @@ -40,6 +40,7 @@ export interface ApiUser { maxStoryId?: number; color?: ApiPeerColor; canEditBot?: boolean; + hasMainMiniApp?: boolean; botActiveUsers?: number; } diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index 211f114d7..3a1a049b3 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -1271,6 +1271,9 @@ "MenuInstallApp" = "Install App"; "RemoveEffect" = "Remove effect"; "ReplyInPrivateMessage" = "Reply In Private Message"; +"ProfileOpenAppAbout" = "By launching this mini app, you agree to the {terms}."; +"ProfileOpenAppTerms" = "Terms of Service for Mini Apps"; +"ProfileBotOpenAppInfoLink" = "https://telegram.org/tos/mini-apps"; "MonetizationInfoTONTitle" = "What is 💎 TON?"; "ChannelEarnLearnCoinAbout" = "TON is a blockchain platform and cryptocurrency that Telegram uses for its high speed and low commissions on transactions. {link}"; "MonetizationBalanceZeroInfo" = "You will be able to collect rewards using Fragment, a third-party platform used by advertisers to pay for ads. {link}"; diff --git a/src/components/common/PreviewMedia.tsx b/src/components/common/PreviewMedia.tsx new file mode 100644 index 000000000..4d9227a54 --- /dev/null +++ b/src/components/common/PreviewMedia.tsx @@ -0,0 +1,82 @@ +import type { FC } from '../../lib/teact/teact'; +import React, { memo, useRef } from '../../lib/teact/teact'; + +import type { ApiBotPreviewMedia } from '../../api/types'; +import type { ObserveFn } from '../../hooks/useIntersectionObserver'; + +import { + getMessageMediaHash, getMessageMediaThumbDataUri, +} from '../../global/helpers'; +import buildClassName from '../../util/buildClassName'; +import { formatMediaDuration } from '../../util/dates/dateFormat'; +import stopEvent from '../../util/stopEvent'; + +import { useIsIntersecting } from '../../hooks/useIntersectionObserver'; +import useLastCallback from '../../hooks/useLastCallback'; +import useMedia from '../../hooks/useMedia'; +import useMediaTransition from '../../hooks/useMediaTransition'; + +import './Media.scss'; + +type OwnProps = { + media: ApiBotPreviewMedia; + idPrefix?: string; + isProtected?: boolean; + observeIntersection?: ObserveFn; + onClick: (index: number) => void; + index: number; +}; + +const PreviewMedia: FC = ({ + media, + idPrefix = 'preview-media', + isProtected, + observeIntersection, + onClick, + index, +}) => { + // eslint-disable-next-line no-null/no-null + const ref = useRef(null); + + const isIntersecting = useIsIntersecting(ref, observeIntersection); + const thumbDataUri = getMessageMediaThumbDataUri(media); + + const mediaBlobUrl = useMedia(getMessageMediaHash(media, 'preview'), !isIntersecting); + const transitionClassNames = useMediaTransition(mediaBlobUrl); + + const video = media.content.video; + + const handleClick = useLastCallback(() => { + onClick(index); + }); + + return ( +
+ + + {video && {video.isGif ? 'GIF' : formatMediaDuration(video.duration)}} + {isProtected && } +
+ ); +}; + +export default memo(PreviewMedia); diff --git a/src/components/common/profile/ChatExtra.module.scss b/src/components/common/profile/ChatExtra.module.scss index fbb856757..11e360459 100644 --- a/src/components/common/profile/ChatExtra.module.scss +++ b/src/components/common/profile/ChatExtra.module.scss @@ -24,6 +24,11 @@ margin-bottom: 0; } +.sectionInfo { + color: var(--color-text-secondary); + font-size: 0.875rem; +} + .personalChannelSubscribers { grid-column: 2; grid-row: 1; @@ -36,3 +41,8 @@ grid-column: 1 / span 2; grid-row: 2; } + +.openAppButton { + margin-top: 0.5rem; + margin-bottom: 0.5rem; +} diff --git a/src/components/common/profile/ChatExtra.tsx b/src/components/common/profile/ChatExtra.tsx index 14e7ea5e4..dff87ab2a 100644 --- a/src/components/common/profile/ChatExtra.tsx +++ b/src/components/common/profile/ChatExtra.tsx @@ -32,20 +32,24 @@ import { copyTextToClipboard } from '../../../util/clipboard'; import { formatPhoneNumberWithCode } from '../../../util/phoneNumber'; import { debounce } from '../../../util/schedulers'; import stopEvent from '../../../util/stopEvent'; +import { extractCurrentThemeParams } from '../../../util/themeStyle'; import { ChatAnimationTypes } from '../../left/main/hooks'; import formatUsername from '../helpers/formatUsername'; import renderText from '../helpers/renderText'; import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps'; +import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; import useMedia from '../../../hooks/useMedia'; import useOldLang from '../../../hooks/useOldLang'; import useDevicePixelRatio from '../../../hooks/window/useDevicePixelRatio'; import Chat from '../../left/main/Chat'; +import Button from '../../ui/Button'; import ListItem from '../../ui/ListItem'; import Skeleton from '../../ui/placeholder/Skeleton'; import Switcher from '../../ui/Switcher'; +import SafeLink from '../SafeLink'; import BusinessHours from './BusinessHours'; import UserBirthday from './UserBirthday'; @@ -70,6 +74,7 @@ type StateProps = { topicLink?: string; hasSavedMessages?: boolean; personalChannel?: ApiChat; + hasMainMiniApp?: boolean; }; const DEFAULT_MAP_CONFIG = { @@ -95,6 +100,7 @@ const ChatExtra: FC = ({ topicLink, hasSavedMessages, personalChannel, + hasMainMiniApp, }) => { const { showNotification, @@ -104,6 +110,7 @@ const ChatExtra: FC = ({ openSavedDialog, openMapModal, requestCollectibleInfo, + requestMainWebView, } = getActions(); const { @@ -120,7 +127,8 @@ const ChatExtra: FC = ({ personalChannelMessageId, birthday, } = userFullInfo || {}; - const lang = useOldLang(); + const oldLang = useOldLang(); + const lang = useLang(); const [areNotificationsEnabled, setAreNotificationsEnabled] = useState(!isMuted); @@ -177,7 +185,7 @@ const ChatExtra: FC = ({ const { address, geo } = businessLocation!; if (!geo) { copyTextToClipboard(address); - showNotification({ message: lang('BusinessLocationCopied') }); + showNotification({ message: oldLang('BusinessLocationCopied') }); return; } @@ -220,7 +228,7 @@ const ChatExtra: FC = ({ requestCollectibleInfo({ collectible: phoneNumber, peerId: peerId!, type: 'phone' }); return; } - copy(formattedNumber!, lang('Phone')); + copy(formattedNumber!, oldLang('Phone')); }); const handleUsernameClick = useLastCallback((username: ApiUsername, isChat?: boolean) => { @@ -228,9 +236,35 @@ const ChatExtra: FC = ({ requestCollectibleInfo({ collectible: username.username, peerId: peerId!, type: 'username' }); return; } - copy(formatUsername(username.username, isChat), lang(isChat ? 'Link' : 'Username')); + copy(formatUsername(username.username, isChat), oldLang(isChat ? 'Link' : 'Username')); }); + const handleOpenApp = useLastCallback(() => { + if (!chat) { + return; + } + const botId = user?.id; + if (!botId) { + return; + } + const theme = extractCurrentThemeParams(); + requestMainWebView({ + botId, + peerId: botId, + theme, + shouldMarkBotTrusted: true, + }); + }); + + const appTermsInfo = lang('ProfileOpenAppAbout', { + terms: ( + + ), + }, { withNodes: true }); + if (!chat || chat.isRestricted || (isSelf && !isInSettings)) { return undefined; } @@ -239,7 +273,7 @@ const ChatExtra: FC = ({ const [mainUsername, ...otherUsernames] = usernameList; const usernameLinks = otherUsernames.length - ? (lang('UsernameAlso', '%USERNAMES%') as string) + ? (oldLang('UsernameAlso', '%USERNAMES%') as string) .split('%') .map((s) => { return (s === 'USERNAMES' ? ( @@ -282,7 +316,7 @@ const ChatExtra: FC = ({ {formatUsername(mainUsername.username, isChat)} {usernameLinks && {usernameLinks}} - {lang(isChat ? 'Link' : 'Username')} + {oldLang(isChat ? 'Link' : 'Username')} ); @@ -292,9 +326,9 @@ const ChatExtra: FC = ({
{personalChannel && (
-

{lang('ProfileChannel')}

+

{oldLang('ProfileChannel')}

- {lang('Subscribers', personalChannel.membersCount, 'i')} + {oldLang('Subscribers', personalChannel.membersCount, 'i')} = ({ // eslint-disable-next-line react/jsx-no-bind {formattedNumber} - {lang('Phone')} + {oldLang('Phone')} )} {activeUsernames && renderUsernames(activeUsernames)} @@ -331,7 +365,7 @@ const ChatExtra: FC = ({ ]) } - {lang(userId ? 'UserBio' : 'Info')} + {oldLang(userId ? 'UserBio' : 'Info')} )} {activeChatUsernames && !isTopicInfo && renderUsernames(activeChatUsernames, true)} @@ -342,18 +376,36 @@ const ChatExtra: FC = ({ narrow ripple // eslint-disable-next-line react/jsx-no-bind - onClick={() => copy(link, lang('SetUrlPlaceholder'))} + onClick={() => copy(link, oldLang('SetUrlPlaceholder'))} >
{link}
- {lang('SetUrlPlaceholder')} + {oldLang('SetUrlPlaceholder')} )} {birthday && ( )} + { hasMainMiniApp && ( + + +
+ {appTermsInfo} +
+
+ )} {!isInSettings && ( - {lang('Notifications')} + {oldLang('Notifications')} = ({ onClick={handleClickLocation} >
{businessLocation.address}
- {lang('BusinessProfileLocation')} + {oldLang('BusinessProfileLocation')}
)} {hasSavedMessages && !isInSettings && ( - {lang('SavedMessagesTab')} + {oldLang('SavedMessagesTab')} )}
@@ -418,6 +470,8 @@ export default memo(withGlobal( ? selectChat(global, userFullInfo.personalChannelId) : undefined; + const hasMainMiniApp = user?.hasMainMiniApp; + return { phoneCodeList, chat, @@ -431,6 +485,7 @@ export default memo(withGlobal( topicLink, hasSavedMessages, personalChannel, + hasMainMiniApp, }; }, )(ChatExtra)); diff --git a/src/components/mediaViewer/helpers/ghostAnimation.ts b/src/components/mediaViewer/helpers/ghostAnimation.ts index e3c62b963..9e11960cb 100644 --- a/src/components/mediaViewer/helpers/ghostAnimation.ts +++ b/src/components/mediaViewer/helpers/ghostAnimation.ts @@ -299,6 +299,11 @@ function getNodes(origin: MediaViewerOrigin, message?: ApiMessage, index?: numbe mediaSelector = '.full-media'; break; + case MediaViewerOrigin.PreviewMedia: + containerSelector = `#preview-media${index}`; + mediaSelector = 'img'; + break; + case MediaViewerOrigin.SharedMedia: containerSelector = `#shared-media${getMessageHtmlId(message!.id, index)}`; mediaSelector = 'img'; @@ -358,6 +363,7 @@ function applyShape(ghost: HTMLDivElement, origin: MediaViewerOrigin) { case MediaViewerOrigin.Inline: case MediaViewerOrigin.ScheduledInline: case MediaViewerOrigin.StarsTransaction: + case MediaViewerOrigin.PreviewMedia: ghost.classList.add('rounded-corners'); break; diff --git a/src/components/right/Profile.scss b/src/components/right/Profile.scss index df100652c..860b8b105 100644 --- a/src/components/right/Profile.scss +++ b/src/components/right/Profile.scss @@ -71,7 +71,8 @@ &.storiesArchive-list, &.stories-list, - &.media-list { + &.media-list, + &.previewMedia-list { display: grid; grid-template-columns: repeat(3, 1fr); grid-auto-rows: 1fr; diff --git a/src/components/right/Profile.tsx b/src/components/right/Profile.tsx index 1f7c17003..7dff2d262 100644 --- a/src/components/right/Profile.tsx +++ b/src/components/right/Profile.tsx @@ -6,6 +6,7 @@ import React, { import { getActions, withGlobal } from '../../global'; import type { + ApiBotPreviewMedia, ApiChat, ApiChatMember, ApiMessage, @@ -52,6 +53,7 @@ import { selectTabState, selectTheme, selectUser, + selectUserFullInfo, } from '../../global/selectors'; import { selectPremiumLimit } from '../../global/selectors/limits'; import buildClassName from '../../util/buildClassName'; @@ -77,6 +79,7 @@ import Document from '../common/Document'; import GroupChatInfo from '../common/GroupChatInfo'; import Media from '../common/Media'; import NothingFound from '../common/NothingFound'; +import PreviewMedia from '../common/PreviewMedia'; import PrivateChatInfo from '../common/PrivateChatInfo'; import ChatExtra from '../common/profile/ChatExtra'; import ProfileInfo from '../common/ProfileInfo'; @@ -113,6 +116,7 @@ type StateProps = { hasCommonChatsTab?: boolean; hasStoriesTab?: boolean; hasMembersTab?: boolean; + hasPreviewMediaTab?: boolean; areMembersHidden?: boolean; canAddMembers?: boolean; canDeleteMembers?: boolean; @@ -133,6 +137,7 @@ type StateProps = { nextProfileTab?: ProfileTabType; shouldWarnAboutSvg?: boolean; similarChannels?: string[]; + botPreviewMedia? : ApiBotPreviewMedia[]; isCurrentUserPremium?: boolean; limitSimilarChannels: number; isTopicInfo?: boolean; @@ -174,6 +179,8 @@ const Profile: FC = ({ hasCommonChatsTab, hasStoriesTab, hasMembersTab, + hasPreviewMediaTab, + botPreviewMedia, areMembersHidden, canAddMembers, canDeleteMembers, @@ -210,6 +217,7 @@ const Profile: FC = ({ loadStoriesArchive, openPremiumModal, loadChannelRecommendations, + loadPreviewMedias, } = getActions(); // eslint-disable-next-line no-null/no-null @@ -229,6 +237,9 @@ const Profile: FC = ({ ...(hasMembersTab ? [{ type: 'members' as const, title: isChannel ? 'ChannelSubscribers' : 'GroupMembers', }] : []), + ...(hasPreviewMediaTab ? [{ + type: 'previewMedia' as const, title: 'ProfileBotPreviewTab', + }] : []), ...TABS, // TODO The filter for voice messages currently does not work // in forum topics. Return it when it's fixed on the server side. @@ -240,6 +251,7 @@ const Profile: FC = ({ ]), [ hasCommonChatsTab, hasMembersTab, + hasPreviewMediaTab, hasStoriesTab, isChannel, isTopicInfo, @@ -274,6 +286,12 @@ const Profile: FC = ({ setActiveTab(index); }, []); + useEffect(() => { + if (hasPreviewMediaTab && !botPreviewMedia) { + loadPreviewMedias({ botId: chatId }); + } + }, [chatId, botPreviewMedia, hasPreviewMediaTab]); + useEffect(() => { if (isChannel && !similarChannels) { loadChannelRecommendations({ chatId }); @@ -364,6 +382,15 @@ const Profile: FC = ({ }); }); + const handleSelectPreviewMedia = useLastCallback((index: number) => { + openMediaViewer({ + standaloneMedia: botPreviewMedia?.flatMap((item) => item?.content.photo + || item?.content.video).filter(Boolean), + origin: MediaViewerOrigin.PreviewMedia, + mediaIndex: index, + }); + }); + const handlePlayAudio = useLastCallback((messageId: number) => { openAudioPlayer({ chatId: profileId, messageId }); }); @@ -416,7 +443,7 @@ const Profile: FC = ({ if (isFirstTab) { renderingDelay = !isRightColumnShown ? HIDDEN_RENDER_DELAY : 0; // @optimization Used to delay first render of secondary tabs while animating - } else if (!viewportIds) { + } else if (!viewportIds && !botPreviewMedia) { renderingDelay = SLIDE_TRANSITION_DURATION; } const canRenderContent = useAsyncRendering([chatId, threadId, resultType, renderingActiveTab], renderingDelay); @@ -438,7 +465,7 @@ const Profile: FC = ({ ); } - if (!viewportIds || !canRenderContent || !messagesById) { + if ((!viewportIds && !botPreviewMedia) || !canRenderContent || !messagesById) { const noSpinner = isFirstTab && !canRenderContent; const forceRenderHiddenMembers = Boolean(resultType === 'members' && areMembersHidden); @@ -450,7 +477,7 @@ const Profile: FC = ({ ); } - if (!viewportIds.length) { + if (viewportIds && !viewportIds?.length) { let text: string; switch (resultType) { @@ -595,6 +622,17 @@ const Profile: FC = ({ )) + ) : resultType === 'previewMedia' ? ( + botPreviewMedia!.map((media, i) => ( + + )) ) : resultType === 'similarChannels' ? (
{(viewportIds as string[])!.map((channelId, i) => ( @@ -739,6 +777,11 @@ export default memo(withGlobal( const peer = user || chat; const peerFullInfo = selectPeerFullInfo(global, chatId); + + const userFullInfo = selectUserFullInfo(global, chatId); + const hasPreviewMediaTab = userFullInfo?.botInfo?.hasPreviewMedia; + const botPreviewMedia = global.users.previewMediaByBotId[chatId]; + const hasStoriesTab = peer && (user?.isSelf || (!peer.areStoriesHidden && peerFullInfo?.hasPinnedStories)) && !isSavedDialog; const peerStories = hasStoriesTab ? selectPeerStories(global, peer.id) : undefined; @@ -757,6 +800,7 @@ export default memo(withGlobal( hasCommonChatsTab, hasStoriesTab, hasMembersTab, + hasPreviewMediaTab, areMembersHidden, canAddMembers, canDeleteMembers, @@ -776,6 +820,7 @@ export default memo(withGlobal( forceScrollProfileTab: selectTabState(global).forceScrollProfileTab, shouldWarnAboutSvg: global.settings.byKey.shouldWarnAboutSvg, similarChannels: similarChannelIds, + botPreviewMedia, isCurrentUserPremium, isTopicInfo, isSavedDialog, diff --git a/src/global/actions/api/bots.ts b/src/global/actions/api/bots.ts index cfcc3714e..46b9f73d1 100644 --- a/src/global/actions/api/bots.ts +++ b/src/global/actions/api/bots.ts @@ -612,6 +612,88 @@ addActionHandler('requestWebView', async (global, actions, payload): Promise => { + const { + botId, peerId, theme, startParam, shouldMarkBotTrusted, + tabId = getCurrentTabId(), + } = payload; + + const bot = selectUser(global, botId); + if (!bot) return; + const peer = selectPeer(global, peerId); + if (!peer) return; + + if (!selectIsTrustedBot(global, botId)) { + if (shouldMarkBotTrusted) { + actions.markBotTrusted({ botId, isWriteAllowed: true, tabId }); + } else { + global = updateTabState(global, { + botTrustRequest: { + botId, + type: 'webApp', + onConfirm: { + action: 'requestMainWebView', + payload, + }, + }, + }, tabId); + setGlobal(global); + return; + } + } + + const result = await callApi('requestMainWebView', { + bot, + peer, + theme, + startParam, + }); + if (!result) { + return; + } + + const { url: webViewUrl, queryId } = result; + + global = getGlobal(); + global = updateTabState(global, { + webApp: { + url: webViewUrl, + botId, + queryId, + buttonText: '', + }, + }, tabId); + setGlobal(global); +}); + +addActionHandler('loadPreviewMedias', async (global, actions, payload): Promise => { + const { + botId, + } = payload; + const bot = selectUser(global, botId); + if (!bot) return; + + const medias = await callApi('fetchPreviewMedias', { + bot, + }); + + global = getGlobal(); + if (medias) { + global = { + ...global, + users: { + ...global.users, + previewMediaByBotId: { + ...global.users.previewMediaByBotId, + [botId]: medias, + }, + }, + }; + + setGlobal(global); + } +}); + addActionHandler('requestAppWebView', async (global, actions, payload): Promise => { const { botId, appName, startApp, theme, isWriteAllowed, isFromConfirm, diff --git a/src/global/actions/api/chats.ts b/src/global/actions/api/chats.ts index 985336e9b..35e045533 100644 --- a/src/global/actions/api/chats.ts +++ b/src/global/actions/api/chats.ts @@ -1485,6 +1485,20 @@ addActionHandler('openChatByUsername', async (global, actions, payload): Promise }); return; } + if (startApp !== undefined && !webAppName) { + const theme = extractCurrentThemeParams(); + const chatByUsername = await fetchChatByUsername(global, username); + global = getGlobal(); + const user = chatByUsername && selectUser(global, chatByUsername.id); + if (!chatByUsername || !chat || !user?.hasMainMiniApp) return; + actions.requestMainWebView({ + botId: chatByUsername.id, + peerId: chat.id, + theme, + tabId, + }); + return; + } if (!isWebApp) { await openChatByUsername( global, actions, { diff --git a/src/global/cache.ts b/src/global/cache.ts index 7a6afd6f9..8b2ee87ce 100644 --- a/src/global/cache.ts +++ b/src/global/cache.ts @@ -253,6 +253,9 @@ function unsafeMigrateCache(cached: GlobalState, initialState: GlobalState) { cached.quickReplies = initialState.quickReplies; } + if (!cached.users.previewMediaByBotId) { + cached.users.previewMediaByBotId = initialState.users.previewMediaByBotId; + } if (!cached.chats.loadingParameters) { cached.chats.loadingParameters = initialState.chats.loadingParameters; } @@ -366,7 +369,11 @@ function reduceCustomEmojis(global: T): GlobalState['cust } function reduceUsers(global: T): GlobalState['users'] { - const { users: { byId, statusesById, fullInfoById }, currentUserId } = global; + const { + users: { + byId, statusesById, fullInfoById, + }, currentUserId, + } = global; const currentChatIds = compact( Object.values(global.byTabId) .map(({ id: tabId }) => selectCurrentMessageList(global, tabId)), @@ -400,6 +407,7 @@ function reduceUsers(global: T): GlobalState['users'] { byId: pick(byId, idsToSave), statusesById: pick(statusesById, idsToSave), fullInfoById: pick(fullInfoById, idsToSave), + previewMediaByBotId: {}, }; } diff --git a/src/global/initialState.ts b/src/global/initialState.ts index 56ef09e0e..268cdad47 100644 --- a/src/global/initialState.ts +++ b/src/global/initialState.ts @@ -97,6 +97,7 @@ export const INITIAL_GLOBAL_STATE: GlobalState = { byId: {}, statusesById: {}, fullInfoById: {}, + previewMediaByBotId: {}, }, chats: { diff --git a/src/global/types.ts b/src/global/types.ts index 440552c59..b4286319f 100644 --- a/src/global/types.ts +++ b/src/global/types.ts @@ -6,6 +6,7 @@ import type { ApiAvailableReaction, ApiBoost, ApiBoostsStatus, + ApiBotPreviewMedia, ApiChannelMonetizationStatistics, ApiChannelStatistics, ApiChat, @@ -925,6 +926,7 @@ export type GlobalState = { statusesById: Record; // Obtained from GetFullUser / UserFullInfo fullInfoById: Record; + previewMediaByBotId: Record; }; chats: { @@ -2873,6 +2875,13 @@ export interface ActionPayloads { isFromBotMenu?: boolean; startParam?: string; } & WithTabId; + requestMainWebView: { + botId: string; + peerId: string; + theme?: ApiThemeParameters; + startParam?: string; + shouldMarkBotTrusted?: boolean; + } & WithTabId; prolongWebView: { botId: string; peerId: string; @@ -2898,6 +2907,9 @@ export interface ActionPayloads { isWriteAllowed?: boolean; isFromConfirm?: boolean; } & WithTabId; + loadPreviewMedias: { + botId: string; + }; setWebAppPaymentSlug: { slug?: string; } & WithTabId; diff --git a/src/lib/gramjs/tl/apiTl.js b/src/lib/gramjs/tl/apiTl.js index 62dcf76f0..08fe6eaa0 100644 --- a/src/lib/gramjs/tl/apiTl.js +++ b/src/lib/gramjs/tl/apiTl.js @@ -1547,6 +1547,7 @@ messages.getQuickReplyMessages#94a495c3 flags:# shortcut_id:int id:flags.0?Vecto messages.sendQuickReplyMessages#6c750de1 peer:InputPeer shortcut_id:int id:Vector random_id:Vector = Updates; messages.getAvailableEffects#dea20a39 hash:int = messages.AvailableEffects; messages.getFactCheck#b9cdc5ee peer:InputPeer msg_id:Vector = Vector; +messages.requestMainWebView#c9e01e7b flags:# compact:flags.7?true peer:InputPeer bot:InputUser start_param:flags.1?string theme_params:flags.0?DataJSON platform:string = WebViewResult; updates.getState#edd4882a = updates.State; updates.getDifference#19c2f763 flags:# pts:int pts_limit:flags.1?int pts_total_limit:flags.0?int date:int qts:int qts_limit:flags.2?int = updates.Difference; updates.getChannelDifference#3173d78 flags:# force:flags.0?true channel:InputChannel filter:ChannelMessagesFilter pts:int limit:int = updates.ChannelDifference; @@ -1617,6 +1618,7 @@ bots.setBotInfo#10cf3123 flags:# bot:flags.2?InputUser lang_code:string name:fla bots.canSendMessage#1359f4e6 bot:InputUser = Bool; bots.allowSendMessage#f132e3ef bot:InputUser = Updates; bots.invokeWebViewCustomMethod#87fc5e7 bot:InputUser custom_method:string params:DataJSON = DataJSON; +bots.getPreviewMedias#a2a5594d bot:InputUser = Vector; bots.getPopularAppBots#c2510192 offset:string limit:int = bots.PopularAppBots; payments.getPaymentForm#37148dbb flags:# invoice:InputInvoice theme_params:flags.0?DataJSON = payments.PaymentForm; payments.getPaymentReceipt#2478d1cc peer:InputPeer msg_id:int = payments.PaymentReceipt; diff --git a/src/lib/gramjs/tl/static/api.json b/src/lib/gramjs/tl/static/api.json index c6847a07a..9ff83f133 100644 --- a/src/lib/gramjs/tl/static/api.json +++ b/src/lib/gramjs/tl/static/api.json @@ -175,6 +175,7 @@ "messages.getQuickReplyMessages", "messages.sendQuickReplyMessages", "messages.getFactCheck", + "messages.requestMainWebView", "updates.getState", "updates.getDifference", "updates.getChannelDifference", @@ -230,6 +231,7 @@ "bots.invokeWebViewCustomMethod", "bots.getPopularAppBots", "bots.setBotInfo", + "bots.getPreviewMedias", "payments.getPaymentForm", "payments.getPaymentReceipt", "payments.validateRequestedInfo", diff --git a/src/types/index.ts b/src/types/index.ts index d40879240..d286104e1 100644 --- a/src/types/index.ts +++ b/src/types/index.ts @@ -329,6 +329,7 @@ export enum MediaViewerOrigin { SearchResult, SuggestedAvatar, StarsTransaction, + PreviewMedia, } export enum StoryViewerOrigin { @@ -392,6 +393,7 @@ export type ProfileTabType = | 'members' | 'commonChats' | 'media' + | 'previewMedia' | 'documents' | 'links' | 'audio' diff --git a/src/types/language.d.ts b/src/types/language.d.ts index 8d666e12e..b6c3d679e 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -1512,6 +1512,11 @@ export interface LangPair { 'MenuInstallApp': undefined; 'RemoveEffect': undefined; 'ReplyInPrivateMessage': undefined; + 'ProfileOpenAppAbout': { + 'terms': string; + }; + 'ProfileOpenAppTerms': undefined; + 'ProfileBotOpenAppInfoLink': undefined; 'MonetizationInfoTONTitle': undefined; 'ChannelEarnLearnCoinAbout': { 'link': string | number; diff --git a/src/util/fallbackLangPack.ts b/src/util/fallbackLangPack.ts index dac6de213..32c3e3d56 100644 --- a/src/util/fallbackLangPack.ts +++ b/src/util/fallbackLangPack.ts @@ -511,4 +511,6 @@ export default { SlowModeWait: 'Slow Mode — %d', OpenMapWith: 'Open map with...', FullDateTimeFormat: '%@, %@', + ProfileOpenAppTerms: 'Terms of Service for Mini Apps', + ProfileBotOpenAppInfoLink: 'https://telegram.org/tos/mini-apps', } as ApiOldLangPack;