Mini Apps: Implement suggesting file downloads (#5269)

This commit is contained in:
zubiden 2024-12-06 19:44:20 +04:00 committed by Alexander Zinchuk
parent f32847eb14
commit f66cb55dac
8 changed files with 138 additions and 24 deletions

View File

@ -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) {

View File

@ -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."

View File

@ -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<OwnProps & StateProps> = ({
const [popupParameters, setPopupParameters] = useState<PopupOptions | undefined>();
const [isRequestingPhone, setIsRequestingPhone] = useState(false);
const [isRequestingWriteAccess, setIsRequestingWriteAccess] = useState(false);
const [requestedFileDownload, setRequestedFileDownload] = useState<{ url: string; fileName: string } | undefined>();
const [bottomBarColor, setBottomBarColor] = useState<string | undefined>();
const {
unlockPopupsAt, handlePopupOpened, handlePopupClosed,
@ -190,7 +193,8 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
// eslint-disable-next-line no-null/no-null
const frameRef = useRef<HTMLIFrameElement>(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<OwnProps & StateProps> = ({
});
});
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<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
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<OwnProps & StateProps> = ({
</Button>
</div>
) }
<ConfirmDialog
isOpen={isRequestingPhone}
onClose={handleRejectPhone}
title={lang('ShareYouPhoneNumberTitle')}
text={lang('AreYouSureShareMyContactInfoBot')}
confirmHandler={handleAcceptPhone}
confirmLabel={lang('ContactShare')}
/>
<ConfirmDialog
isOpen={isRequestingWriteAccess}
onClose={handleRejectWriteAccess}
title={lang('lng_bot_allow_write_title')}
text={lang('lng_bot_allow_write')}
confirmHandler={handleAcceptWriteAccess}
confirmLabel={lang('lng_bot_allow_write_confirm')}
/>
{popupParameters && (
<Modal
isOpen={Boolean(popupParameters)}
@ -933,27 +978,58 @@ const WebAppModalTabContent: FC<OwnProps & StateProps> = ({
// 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])}
</Button>
))}
</div>
</Modal>
)}
<ConfirmDialog
isOpen={isRequestingPhone}
onClose={handleRejectPhone}
title={oldLang('ShareYouPhoneNumberTitle')}
text={oldLang('AreYouSureShareMyContactInfoBot')}
confirmHandler={handleAcceptPhone}
confirmLabel={oldLang('ContactShare')}
/>
<ConfirmDialog
isOpen={isRequestingWriteAccess}
onClose={handleRejectWriteAccess}
title={oldLang('lng_bot_allow_write_title')}
text={oldLang('lng_bot_allow_write')}
confirmHandler={handleAcceptWriteAccess}
confirmLabel={oldLang('lng_bot_allow_write_confirm')}
/>
<ConfirmDialog
isOpen={Boolean(requestedFileDownload)}
title={lang('BotDownloadFileTitle')}
textParts={lang('BotDownloadFileDescription', {
bot: bot?.firstName,
filename: requestedFileDownload?.fileName,
}, {
withNodes: true,
withMarkdown: true,
})}
confirmLabel={lang('BotDownloadFileButton')}
onClose={handleRejectFileDownload}
confirmHandler={handleDownloadFile}
/>
<ConfirmDialog
isOpen={isCloseModalOpen}
onClose={handleHideCloseModal}
title={lang('lng_bot_close_warning_title')}
text={lang('lng_bot_close_warning')}
title={oldLang('lng_bot_close_warning_title')}
text={oldLang('lng_bot_close_warning')}
confirmHandler={handleConfirmCloseModal}
confirmIsDestructive
confirmLabel={lang('lng_bot_close_warning_sure')}
confirmLabel={oldLang('lng_bot_close_warning_sure')}
/>
<ConfirmDialog
isOpen={isRemoveModalOpen}
onClose={handleHideRemoveModal}
title={lang('BotRemoveFromMenuTitle')}
textParts={renderText(lang('BotRemoveFromMenu', bot?.firstName), ['simple_markdown'])}
title={oldLang('BotRemoveFromMenuTitle')}
textParts={renderText(oldLang('BotRemoveFromMenu', bot?.firstName), ['simple_markdown'])}
confirmHandler={handleRemoveAttachBot}
confirmIsDestructive
/>

View File

@ -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<BotPreviewMedia>;
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;

View File

@ -276,6 +276,7 @@
"bots.getPopularAppBots",
"bots.setBotInfo",
"bots.getPreviewMedias",
"bots.checkDownloadFileParams",
"payments.getPaymentForm",
"payments.getPaymentReceipt",
"payments.validateRequestedInfo",

View File

@ -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<V extends unknown = LangVariable> {
'BotSuggestedStatus': {
'bot': V;
};
'BotDownloadFileDescription': {
'bot': V;
'filename': V;
};
'StarsSubscribeBotButtonMonth': {
'amount': V;
};

View File

@ -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>;

View File

@ -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();