From f66cb55dacf0d3f73977ae828d8165df1de84804 Mon Sep 17 00:00:00 2001 From: zubiden <19638254+zubiden@users.noreply.github.com> Date: Fri, 6 Dec 2024 19:44:20 +0400 Subject: [PATCH] Mini Apps: Implement suggesting file downloads (#5269) --- src/api/gramjs/methods/bots.ts | 18 +++ src/assets/localization/fallback.strings | 3 + .../modals/webApp/WebAppModalTabContent.tsx | 124 ++++++++++++++---- src/lib/gramjs/tl/apiTl.js | 1 + src/lib/gramjs/tl/static/api.json | 1 + src/types/language.d.ts | 6 + src/types/webapp.ts | 7 + src/util/download.ts | 2 + 8 files changed, 138 insertions(+), 24 deletions(-) diff --git a/src/api/gramjs/methods/bots.ts b/src/api/gramjs/methods/bots.ts index 0da2cca98..8a9e2ebe1 100644 --- a/src/api/gramjs/methods/bots.ts +++ b/src/api/gramjs/methods/bots.ts @@ -593,6 +593,24 @@ export async function fetchPreviewMedias({ bot } : { bot: ApiUser }) { return previews; } +export function checkBotDownloadFileParams({ + bot, + fileName, + url, +}: { + bot: ApiUser; + fileName: string; + url: string; +}) { + return invokeRequest(new GramJs.bots.CheckDownloadFileParams({ + bot: buildInputPeer(bot.id, bot.accessHash), + fileName, + url, + }), { + shouldReturnTrue: true, + }); +} + function processInlineBotResult(queryId: string, results: GramJs.TypeBotInlineResult[]) { return results.map((result) => { if (result instanceof GramJs.BotInlineMediaResult) { diff --git a/src/assets/localization/fallback.strings b/src/assets/localization/fallback.strings index 8dd719ea2..de3c9a9ef 100644 --- a/src/assets/localization/fallback.strings +++ b/src/assets/localization/fallback.strings @@ -1393,6 +1393,9 @@ "BotSuggestedStatus" = "Do you want to set this emoji status suggested by **{bot}**?"; "BotSuggestedStatusTitle" = "Set Emoji Status"; "BotSuggestedStatusUpdated" = "Your emoji status is updated."; +"BotDownloadFileTitle" = "Download File"; +"BotDownloadFileDescription" = "**{bot}** suggests you to download **{filename}**"; +"BotDownloadFileButton" = "Download"; "PrivacyGifts" = "Gifts"; "PrivacyGiftsTitle" = "Who can display gifts on my profile?" "PrivacyGiftsInfo" = "Choose whether gifts from specific senders need your approval before they're visible to others on your profile." diff --git a/src/components/modals/webApp/WebAppModalTabContent.tsx b/src/components/modals/webApp/WebAppModalTabContent.tsx index 8632fcbfc..9326548f5 100644 --- a/src/components/modals/webApp/WebAppModalTabContent.tsx +++ b/src/components/modals/webApp/WebAppModalTabContent.tsx @@ -19,6 +19,7 @@ import { } from '../../../global/selectors'; import buildClassName from '../../../util/buildClassName'; import buildStyle from '../../../util/buildStyle'; +import download from '../../../util/download'; import { extractCurrentThemeParams, validateHexColor } from '../../../util/themeStyle'; import { callApi } from '../../../api/gramjs'; import { REM } from '../../common/helpers/mediaDimensions'; @@ -27,6 +28,7 @@ import renderText from '../../common/helpers/renderText'; import { getIsWebAppsFullscreenSupported } from '../../../hooks/useAppLayout'; import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev'; import useFlag from '../../../hooks/useFlag'; +import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; import useOldLang from '../../../hooks/useOldLang'; import useSyncEffect from '../../../hooks/useSyncEffect'; @@ -133,6 +135,7 @@ const WebAppModalTabContent: FC = ({ const [popupParameters, setPopupParameters] = useState(); const [isRequestingPhone, setIsRequestingPhone] = useState(false); const [isRequestingWriteAccess, setIsRequestingWriteAccess] = useState(false); + const [requestedFileDownload, setRequestedFileDownload] = useState<{ url: string; fileName: string } | undefined>(); const [bottomBarColor, setBottomBarColor] = useState(); const { unlockPopupsAt, handlePopupOpened, handlePopupClosed, @@ -190,7 +193,8 @@ const WebAppModalTabContent: FC = ({ // eslint-disable-next-line no-null/no-null const frameRef = useRef(null); - const lang = useOldLang(); + const oldLang = useOldLang(); + const lang = useLang(); const isOpen = modal?.isModalOpen || false; const isSimple = Boolean(buttonText); @@ -359,6 +363,20 @@ const WebAppModalTabContent: FC = ({ }); }); + const handleRejectFileDownload = useLastCallback((shouldCloseActive?: boolean) => { + if (shouldCloseActive) { + setRequestedFileDownload(undefined); + handlePopupClosed(); + } + + sendEvent({ + eventType: 'file_download_requested', + eventData: { + status: 'cancelled', + }, + }); + }); + const handleRejectWriteAccess = useLastCallback(() => { sendEvent({ eventType: 'write_access_requested', @@ -404,6 +422,41 @@ const WebAppModalTabContent: FC = ({ setIsRequestingWriteAccess(!canWrite); } + async function handleCheckDownloadFile(fileUrl: string, fileName: string) { + const canDownload = await callApi('checkBotDownloadFileParams', { + bot: bot!, + url: fileUrl, + fileName, + }); + + if (!canDownload) { + sendEvent({ + eventType: 'file_download_requested', + eventData: { + status: 'cancelled', + }, + }); + return; + } + + setRequestedFileDownload({ url: fileUrl, fileName }); + handlePopupOpened(); + } + + const handleDownloadFile = useLastCallback(() => { + if (!requestedFileDownload) return; + setRequestedFileDownload(undefined); + handlePopupClosed(); + + download(requestedFileDownload.url, requestedFileDownload.fileName); + sendEvent({ + eventType: 'file_download_requested', + eventData: { + status: 'downloading', + }, + }); + }); + async function handleInvokeCustomMethod(requestId: string, method: string, parameters: string) { const result = await callApi('invokeWebViewCustomMethod', { bot: bot!, @@ -576,6 +629,14 @@ const WebAppModalTabContent: FC = ({ const { method, params, req_id: requestId } = eventData; handleInvokeCustomMethod(requestId, method, JSON.stringify(params)); } + + if (eventType === 'web_app_request_file_download') { + if (requestedFileDownload || unlockPopupsAt > Date.now()) { + handleRejectFileDownload(); + return; + } + handleCheckDownloadFile(eventData.url, eventData.file_name); + } } const mainButtonCurrentColor = useCurrentOrPrev(mainButton?.color, true); @@ -739,7 +800,7 @@ const WebAppModalTabContent: FC = ({ isBackButtonVisible && styles.stateBack, ); const backButtonCaption = shouldShowAppNameInFullscreen ? activeWebAppName - : lang(isBackButtonVisible ? 'Back' : 'Close'); + : oldLang(isBackButtonVisible ? 'Back' : 'Close'); const hasHeaderElement = headerButtonCaptionRef?.current; @@ -895,22 +956,6 @@ const WebAppModalTabContent: FC = ({ ) } - - {popupParameters && ( = ({ // eslint-disable-next-line react/jsx-no-bind onClick={() => handleAppPopupClose(button.id)} > - {button.text || lang(DEFAULT_BUTTON_TEXT[button.type])} + {button.text || oldLang(DEFAULT_BUTTON_TEXT[button.type])} ))} )} + + + + diff --git a/src/lib/gramjs/tl/apiTl.js b/src/lib/gramjs/tl/apiTl.js index 7461a3408..8e9a1f329 100644 --- a/src/lib/gramjs/tl/apiTl.js +++ b/src/lib/gramjs/tl/apiTl.js @@ -1661,6 +1661,7 @@ 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; bots.getPreviewMedias#a2a5594d bot:InputUser = Vector; +bots.checkDownloadFileParams#50077589 bot:InputUser file_name:string url:string = Bool; payments.getPaymentForm#37148dbb flags:# invoice:InputInvoice theme_params:flags.0?DataJSON = payments.PaymentForm; payments.getPaymentReceipt#2478d1cc peer:InputPeer msg_id:int = payments.PaymentReceipt; payments.validateRequestedInfo#b6c8f12b flags:# save:flags.0?true invoice:InputInvoice info:PaymentRequestedInfo = payments.ValidatedRequestedInfo; diff --git a/src/lib/gramjs/tl/static/api.json b/src/lib/gramjs/tl/static/api.json index 84888a836..46321a41d 100644 --- a/src/lib/gramjs/tl/static/api.json +++ b/src/lib/gramjs/tl/static/api.json @@ -276,6 +276,7 @@ "bots.getPopularAppBots", "bots.setBotInfo", "bots.getPreviewMedias", + "bots.checkDownloadFileParams", "payments.getPaymentForm", "payments.getPaymentReceipt", "payments.validateRequestedInfo", diff --git a/src/types/language.d.ts b/src/types/language.d.ts index 902d9aaab..06442914f 100644 --- a/src/types/language.d.ts +++ b/src/types/language.d.ts @@ -1159,6 +1159,8 @@ export interface LangPair { 'VideoConversionView': undefined; 'BotSuggestedStatusTitle': undefined; 'BotSuggestedStatusUpdated': undefined; + 'BotDownloadFileTitle': undefined; + 'BotDownloadFileButton': undefined; 'PrivacyGifts': undefined; 'PrivacyGiftsTitle': undefined; 'PrivacyGiftsInfo': undefined; @@ -1563,6 +1565,10 @@ export interface LangPairWithVariables { 'BotSuggestedStatus': { 'bot': V; }; + 'BotDownloadFileDescription': { + 'bot': V; + 'filename': V; + }; 'StarsSubscribeBotButtonMonth': { 'amount': V; }; diff --git a/src/types/webapp.ts b/src/types/webapp.ts index 866e20790..5627964ac 100644 --- a/src/types/webapp.ts +++ b/src/types/webapp.ts @@ -106,6 +106,10 @@ export type WebAppInboundEvent = custom_emoji_id: string; duration?: number; }> | + WebAppEvent<'web_app_request_file_download', { + url: string; + file_name: string; + }> | WebAppEvent<'web_app_request_viewport' | 'web_app_request_theme' | 'web_app_ready' | 'web_app_expand' | 'web_app_request_phone' | 'web_app_close' | 'web_app_close_scan_qr_popup' | 'web_app_request_write_access' | 'web_app_request_phone' | 'iframe_will_reload' @@ -194,6 +198,9 @@ export type WebAppOutboundEvent = error: 'UNSUPPORTED' | 'USER_DECLINED' | 'SUGGESTED_EMOJI_INVALID' | 'DURATION_INVALID' | 'SERVER_ERROR' | 'UNKNOWN_ERROR'; }> | + WebAppEvent<'file_download_requested', { + status: 'cancelled' | 'downloading'; + }> | WebAppEvent<'main_button_pressed' | 'secondary_button_pressed' | 'back_button_pressed' | 'settings_button_pressed' | 'scan_qr_popup_closed' | 'reload_iframe' | 'emoji_status_set', null>; diff --git a/src/util/download.ts b/src/util/download.ts index b3166d188..6a69b1732 100644 --- a/src/util/download.ts +++ b/src/util/download.ts @@ -39,6 +39,8 @@ async function processQueue() { function downloadOne({ url, filename }: PendingDownload) { const link = document.createElement('a'); link.href = url; + link.target = '_blank'; + link.rel = 'noopener noreferrer'; link.download = filename; try { link.click();