From 354910fc945f4cda8866a0bc867a2e9c8a0bef21 Mon Sep 17 00:00:00 2001 From: Michael Weimann Date: Mon, 26 Aug 2024 11:07:15 +0200 Subject: [PATCH] Implement the download_file widget action Signed-off-by: Michael Weimann --- .changeset/sweet-jeans-smile.md | 5 ++ .../src/UploadImagePage/Image.tsx | 66 +++++++++++++++++++ .../src/UploadImagePage/ImageListView.tsx | 9 +-- .../src/UploadImagePage/UploadImagePage.tsx | 3 +- packages/api/api-report.api.md | 3 + packages/api/src/api/WidgetApiImpl.ts | 8 +++ packages/api/src/api/types.ts | 10 +++ packages/testing/src/api/mockWidgetApi.ts | 1 + 8 files changed, 98 insertions(+), 7 deletions(-) create mode 100644 .changeset/sweet-jeans-smile.md create mode 100644 example-widget-mui/src/UploadImagePage/Image.tsx diff --git a/.changeset/sweet-jeans-smile.md b/.changeset/sweet-jeans-smile.md new file mode 100644 index 00000000..13b2ee08 --- /dev/null +++ b/.changeset/sweet-jeans-smile.md @@ -0,0 +1,5 @@ +--- +'@matrix-widget-toolkit/api': minor +--- + +Add support for the download_file widget action diff --git a/example-widget-mui/src/UploadImagePage/Image.tsx b/example-widget-mui/src/UploadImagePage/Image.tsx new file mode 100644 index 00000000..a3e5f8ea --- /dev/null +++ b/example-widget-mui/src/UploadImagePage/Image.tsx @@ -0,0 +1,66 @@ +/* + * Copyright 2024 Nordeck IT + Consulting GmbH + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useWidgetApi } from '@matrix-widget-toolkit/react'; +import React, { useCallback, useEffect, useState } from 'react'; + +type ImageProps = { + alt?: string; + /** + * MXC URI of the image that should be shown + */ + contentUrl: string; +}; + +/** + * Component that loads the image from the content repository and displays it. + */ +export const Image: React.FC = function ({ + contentUrl, + ...imageProps +}) { + const [dataUrl, setDataUrl] = useState(); + const widgetApi = useWidgetApi(); + + const handleLoad = useCallback(() => { + if (dataUrl) { + URL.revokeObjectURL(dataUrl); + } + }, [dataUrl]); + + useEffect(() => { + (async () => { + try { + const result = await widgetApi.downloadFile(contentUrl); + + if (!(result.file instanceof Blob)) { + throw new Error('Got non Blob file response'); + } + + const downloadedFileDataUrl = URL.createObjectURL(result.file); + setDataUrl(downloadedFileDataUrl); + } catch (error) { + console.log('Error downloading file', error); + } + })(); + }, [contentUrl]); + + if (dataUrl === undefined) { + return null; + } + + return ; +}; diff --git a/example-widget-mui/src/UploadImagePage/ImageListView.tsx b/example-widget-mui/src/UploadImagePage/ImageListView.tsx index 0904c154..91a8e318 100644 --- a/example-widget-mui/src/UploadImagePage/ImageListView.tsx +++ b/example-widget-mui/src/UploadImagePage/ImageListView.tsx @@ -33,6 +33,7 @@ import { UploadedImageEvent, isValidUploadedImage, } from '../events'; +import { Image } from './Image'; export const ImageListView = (): ReactElement => { const widgetApi = useWidgetApi(); @@ -75,13 +76,9 @@ export const ImageListView = (): ReactElement => { {imageNames.length > 0 && imageNames.map((image) => ( - {image.content.name} { ROOM_EVENT_UPLOADED_IMAGE, ), WidgetApiFromWidgetAction.MSC4039UploadFileAction, + WidgetApiFromWidgetAction.MSC4039DownloadFileAction, WidgetApiFromWidgetAction.MSC4039GetMediaConfigAction, ]} > - {/* + {/* The StoreProvider is located here to keep the example small. Normal applications would locate it outside of the router to establish a single, global store. diff --git a/packages/api/api-report.api.md b/packages/api/api-report.api.md index 0c9a95af..164ecc46 100644 --- a/packages/api/api-report.api.md +++ b/packages/api/api-report.api.md @@ -5,6 +5,7 @@ ```ts import { Capability } from 'matrix-widget-api'; +import { IDownloadFileActionFromWidgetResponseData } from 'matrix-widget-api'; import { IGetMediaConfigActionFromWidgetResponseData } from 'matrix-widget-api'; import { IModalWidgetCreateData } from 'matrix-widget-api'; import { IModalWidgetOpenRequestDataButton } from 'matrix-widget-api'; @@ -271,6 +272,7 @@ export type WidgetApi = { }>; getMediaConfig(): Promise; uploadFile(file: XMLHttpRequestBodyInit): Promise; + downloadFile(contentUrl: string): Promise; }; // @public @@ -281,6 +283,7 @@ export class WidgetApiImpl implements WidgetApi { widgetParameters: WidgetParameters, { capabilities, supportStandalone }?: WidgetApiOptions); closeModal(data?: T): Promise; static create({ capabilities, supportStandalone, }?: WidgetApiOptions): Promise; + downloadFile(contentUrl: string): Promise; getMediaConfig(): Promise; getWidgetConfig(): Readonly | undefined>; hasCapabilities(capabilities: Array): boolean; diff --git a/packages/api/src/api/WidgetApiImpl.ts b/packages/api/src/api/WidgetApiImpl.ts index b0eafab0..f12fb2ff 100644 --- a/packages/api/src/api/WidgetApiImpl.ts +++ b/packages/api/src/api/WidgetApiImpl.ts @@ -16,6 +16,7 @@ import { Capability, + IDownloadFileActionFromWidgetResponseData, IGetMediaConfigActionFromWidgetResponseData, IModalWidgetCreateData, IModalWidgetOpenRequestDataButton, @@ -799,4 +800,11 @@ export class WidgetApiImpl implements WidgetApi { ): Promise { return await this.matrixWidgetApi.uploadFile(file); } + + /** {@inheritdoc WidgetApi.downloadFile} */ + async downloadFile( + contentUrl: string, + ): Promise { + return await this.matrixWidgetApi.downloadFile(contentUrl); + } } diff --git a/packages/api/src/api/types.ts b/packages/api/src/api/types.ts index 039b7d92..e21c3384 100644 --- a/packages/api/src/api/types.ts +++ b/packages/api/src/api/types.ts @@ -16,6 +16,7 @@ import { Capability, + IDownloadFileActionFromWidgetResponseData, IGetMediaConfigActionFromWidgetResponseData, IModalWidgetCreateData, IModalWidgetOpenRequestDataButton, @@ -561,5 +562,14 @@ export type WidgetApi = { file: XMLHttpRequestBodyInit, ): Promise; + /** + * Download a file to the media repository on the homeserver. + * @param contentUrl - MXC URI of the file to download + * @returns resolves to an object with: file - the file contents + */ + downloadFile( + contentUrl: string, + ): Promise; + // TODO: sendSticker, setAlwaysOnScreen }; diff --git a/packages/testing/src/api/mockWidgetApi.ts b/packages/testing/src/api/mockWidgetApi.ts index ef2f4bac..3b4d766a 100644 --- a/packages/testing/src/api/mockWidgetApi.ts +++ b/packages/testing/src/api/mockWidgetApi.ts @@ -235,6 +235,7 @@ export function mockWidgetApi(opts?: { uploadFile: jest.fn().mockResolvedValue({ content_uri: 'mxc://...', }), + downloadFile: jest.fn(), }; widgetApi.receiveRoomEvents.mockImplementation(async (type, options) => {