|
| 1 | +--- |
| 2 | +title: Enable file sharing using UI Library and Azure Blob Storage |
| 3 | +titleSuffix: An Azure Communication Services tutorial |
| 4 | +description: Learn how to use Azure Communication Services with the UI Library to enable file sharing through chat leveraging Azure Blob Storage. |
| 5 | +author: anjulgarg |
| 6 | +manager: alkwa |
| 7 | +services: azure-communication-services |
| 8 | + |
| 9 | +ms.author: anjulgarg |
| 10 | +ms.date: 04/04/2022 |
| 11 | +ms.topic: tutorial |
| 12 | +ms.service: azure-communication-services |
| 13 | +ms.subservice: chat |
| 14 | +--- |
| 15 | + |
| 16 | +# Enable file sharing using UI Library and Azure Blob Storage |
| 17 | + |
| 18 | +[!INCLUDE [Public Preview Notice](../includes/public-preview-include.md)] |
| 19 | + |
| 20 | +In this tutorial, we are configuring the Azure Communication Services UI Library Chat Composite to enable file sharing. The UI Library Chat Composite provides a set of rich components and UI controls that can be used to enable file sharing. We are using Azure Blob Storage to enable the storage of the files that are shared through the chat thread. |
| 21 | + |
| 22 | +>[!IMPORTANT] |
| 23 | +>Azure Communication Services doesn't provide a file storage service. You need to use your own file storage service for sharing files. For the pupose of this tutorial, we are be using Azure Blob Storage.** |
| 24 | +> |
| 25 | +> This tutorial is about file sharing between Azure Communication Services end user in an Azure Communication Services Chat. For file sharing in a Teams interoperability chat, see the documentation in our storybook for [Adding file sharing in Teams Interop Chat](https://azure.github.io/communication-ui-library/?path=/docs/composites-call-with-chat-basicexample--basic-example#in-teams-interop-meeting-chat-thread). Note that for Teams Interoperability chat, we only support Azure Communication Services end user to receive file attachments from Teams users at this time. See [Web UI library use cases](../concepts/ui-library/ui-library-use-cases.md) for more information. |
| 26 | +
|
| 27 | +## Download code |
| 28 | + |
| 29 | +Access the full code for this tutorial on [GitHub](https://github.com/Azure-Samples/communication-services-javascript-quickstarts/tree/main/ui-library-filesharing-chat-composite). If you want to leverage file sharing using UI Components, reference [this sample](https://github.com/Azure-Samples/communication-services-javascript-quickstarts/tree/main/ui-library-filesharing-ui-components). |
| 30 | + |
| 31 | +## Prerequisites |
| 32 | + |
| 33 | +- An Azure account with an active subscription. For details, see [Create an account for free](https://azure.microsoft.com/free/?WT.mc_id=A261C142F). |
| 34 | +- [Visual Studio Code](https://code.visualstudio.com/) on one of the [supported platforms](https://code.visualstudio.com/docs/supporting/requirements#_platforms). |
| 35 | +- [Node.js](https://nodejs.org/), Active LTS and Maintenance LTS versions (10.14.1 recommended). Use the `node --version` command to check your version. |
| 36 | +- An active Communication Services resource and connection string. [Create a Communication Services resource](../quickstarts/create-communication-resource.md). |
| 37 | + |
| 38 | +This tutorial assumes that you already know how to setup and run a Chat Composite. You can follow the [Chat Composite tutorial](https://azure.github.io/communication-ui-library/?path=/docs/quickstarts-composites--page) to learn how to set up and run a Chat Composite. |
| 39 | + |
| 40 | +## Overview |
| 41 | + |
| 42 | +The UI Library Chat Composite supports file sharing by enabling developers to pass the URL to a hosted file that is sent through the Azure Communication Services chat service. The UI Library renders the attached file and supports multiple extensions to configure the look and feel of the file sent. More specifically, it supports the following features: |
| 43 | + |
| 44 | +1. Attach file button for picking files through the OS File Picker |
| 45 | +2. Configure allowed file extensions. |
| 46 | +3. Enable/disable multiple uploads. |
| 47 | +4. File Icons for a wide variety of file types. |
| 48 | +5. File upload/download cards with progress indicators. |
| 49 | +6. Ability to dynamically validate each file upload and display errors on the UI. |
| 50 | +7. Ability to cancel an upload and remove an uploaded file before it is sent. |
| 51 | +8. View uploaded files in MessageThread, download them. Allows asynchronous downloads. |
| 52 | + |
| 53 | +This diagram shows a typical flow of a file sharing scenario for both upload and download. The section marked as `Client Managed` shows the building blocks that need to be implemented by developers. |
| 54 | + |
| 55 | + |
| 56 | + |
| 57 | +## Setup File Storage using Azure Blob |
| 58 | + |
| 59 | +You can follow the tutorial [Upload file to Azure Blob Storage with an Azure Function](/azure/developer/javascript/how-to/with-web-app/azure-function-file-upload) to write the backend code required for file sharing. |
| 60 | + |
| 61 | +Once implemented, you can call this Azure Function inside the `uploadHandler` function to upload files to Azure Blob Storage. For the remaining of the tutorial, we assume you have generated the function using the tutorial for Azure Blob Storage linked above. |
| 62 | + |
| 63 | +### Securing your Azure Blob Storage Container |
| 64 | + |
| 65 | +Note that the tutorial above assumes that your Azure blob storage container allows public access to the files you upload. Making your Azure storage containers public isn't recommended for real world production applications. |
| 66 | + |
| 67 | +For downloading the files you upload to Azure blob storage, you can use shared access signatures (SAS). A shared access signature (SAS) provides secure delegated access to resources in your storage account. With a SAS, you have granular control over how a client can access your data. |
| 68 | + |
| 69 | +The downloadable [GitHub sample](https://github.com/Azure-Samples/communication-services-javascript-quickstarts/tree/main/ui-library-filesharing-chat-composite) showcases the use of SAS for creating SAS URLs to Azure Storage contents. Additionally, you can [read more about SAS](../../storage/common/storage-sas-overview.md). |
| 70 | + |
| 71 | +UI Library requires a React environment to be setup. Next we do that. If you already have a React App, you can skip this section. |
| 72 | + |
| 73 | +### Set Up React App |
| 74 | + |
| 75 | +We use the create-react-app template for this quickstart. For more information, see: [Get Started with React](https://reactjs.org/docs/create-a-new-react-app.html) |
| 76 | + |
| 77 | +```bash |
| 78 | + |
| 79 | +npx create-react-app ui-library-quickstart-composites --template typescript |
| 80 | + |
| 81 | +cd ui-library-quickstart-composites |
| 82 | + |
| 83 | +``` |
| 84 | + |
| 85 | +At the end of this process, you should have a full application inside of the folder `ui-library-quickstart-composites`. |
| 86 | +For this quickstart, we are modifying files inside of the `src` folder. |
| 87 | + |
| 88 | +### Install the Package |
| 89 | + |
| 90 | +Use the `npm install` command to install the beta Azure Communication Services UI Library for JavaScript. |
| 91 | + |
| 92 | +```bash |
| 93 | + |
| 94 | +npm install @azure/ [email protected] |
| 95 | + |
| 96 | +``` |
| 97 | + |
| 98 | +`@azure/communication-react` specifies core Azure Communication Services as `peerDependencies` so that |
| 99 | +you can most consistently use the API from the core libraries in your application. You need to install those libraries as well: |
| 100 | + |
| 101 | +```bash |
| 102 | + |
| 103 | +npm install @azure/ [email protected] |
| 104 | +npm install @azure/ [email protected] |
| 105 | + |
| 106 | +``` |
| 107 | + |
| 108 | +### Run Create React App |
| 109 | + |
| 110 | +Let's test the Create React App installation by running: |
| 111 | + |
| 112 | +```bash |
| 113 | + |
| 114 | +npm run start |
| 115 | + |
| 116 | +``` |
| 117 | + |
| 118 | +## Configuring Chat Composite to enable File Sharing |
| 119 | + |
| 120 | +You need to replace the variable values for both common variable required to initialize the chat composite. |
| 121 | + |
| 122 | +`App.tsx` |
| 123 | + |
| 124 | +```javascript |
| 125 | +import { FileUploadHandler, FileUploadManager } from '@azure/communication-react'; |
| 126 | +import { initializeFileTypeIcons } from '@fluentui/react-file-type-icons'; |
| 127 | +import { |
| 128 | + ChatComposite, |
| 129 | + fromFlatCommunicationIdentifier, |
| 130 | + useAzureCommunicationChatAdapter |
| 131 | +} from '@azure/communication-react'; |
| 132 | +import React, { useMemo } from 'react'; |
| 133 | + |
| 134 | +initializeFileTypeIcons(); |
| 135 | + |
| 136 | +function App(): JSX.Element { |
| 137 | + // Common variables |
| 138 | + const endpointUrl = 'INSERT_ENDPOINT_URL'; |
| 139 | + const userId = ' INSERT_USER_ID'; |
| 140 | + const displayName = 'INSERT_DISPLAY_NAME'; |
| 141 | + const token = 'INSERT_ACCESS_TOKEN'; |
| 142 | + const threadId = 'INSERT_THREAD_ID'; |
| 143 | + |
| 144 | + // We can't even initialize the Chat and Call adapters without a well-formed token. |
| 145 | + const credential = useMemo(() => { |
| 146 | + try { |
| 147 | + return new AzureCommunicationTokenCredential(token); |
| 148 | + } catch { |
| 149 | + console.error('Failed to construct token credential'); |
| 150 | + return undefined; |
| 151 | + } |
| 152 | + }, [token]); |
| 153 | + |
| 154 | + // Memoize arguments to `useAzureCommunicationChatAdapter` so that |
| 155 | + // a new adapter is only created when an argument changes. |
| 156 | + const chatAdapterArgs = useMemo( |
| 157 | + () => ({ |
| 158 | + endpoint: endpointUrl, |
| 159 | + userId: fromFlatCommunicationIdentifier(userId) as CommunicationUserIdentifier, |
| 160 | + displayName, |
| 161 | + credential, |
| 162 | + threadId |
| 163 | + }), |
| 164 | + [userId, displayName, credential, threadId] |
| 165 | + ); |
| 166 | + const chatAdapter = useAzureCommunicationChatAdapter(chatAdapterArgs); |
| 167 | + |
| 168 | + if (!!chatAdapter) { |
| 169 | + return ( |
| 170 | + <> |
| 171 | + <div style={containerStyle}> |
| 172 | + <ChatComposite |
| 173 | + adapter={chatAdapter} |
| 174 | + options={{ |
| 175 | + fileSharing: { |
| 176 | + uploadHandler: fileUploadHandler, |
| 177 | + // If `fileDownloadHandler` is not provided. The file URL is opened in a new tab. |
| 178 | + downloadHandler: fileDownloadHandler, |
| 179 | + accept: 'image/png, image/jpeg, text/plain, .docx', |
| 180 | + multiple: true |
| 181 | + } |
| 182 | + }} /> |
| 183 | + </div> |
| 184 | + </> |
| 185 | + ); |
| 186 | + } |
| 187 | + if (credential === undefined) { |
| 188 | + return <h3>Failed to construct credential. Provided token is malformed.</h3>; |
| 189 | + } |
| 190 | + return <h3>Initializing...</h3>; |
| 191 | +} |
| 192 | + |
| 193 | +const fileUploadHandler: FileUploadHandler = async (userId, fileUploads) => { |
| 194 | + for (const fileUpload of fileUploads) { |
| 195 | + try { |
| 196 | + const { name, url, extension } = await uploadFileToAzureBlob(fileUpload); |
| 197 | + fileUpload.notifyUploadCompleted({ name, extension, url }); |
| 198 | + } catch (error) { |
| 199 | + if (error instanceof Error) { |
| 200 | + fileUpload.notifyUploadFailed(error.message); |
| 201 | + } |
| 202 | + } |
| 203 | + } |
| 204 | +} |
| 205 | + |
| 206 | +const uploadFileToAzureBlob = async (fileUpload: FileUploadManager) => { |
| 207 | + // You need to handle the file upload here and upload it to Azure Blob Storage. |
| 208 | + // This is a code snippet for how to configure the upload |
| 209 | + // Optionally, you can also update the file upload progress. |
| 210 | + fileUpload.notifyUploadProgressChanged(0.2); |
| 211 | + return { |
| 212 | + name: 'SampleFile.jpg', // File name displayed during download |
| 213 | + url: 'https://sample.com/sample.jpg', // Download URL of the file. |
| 214 | + extension: 'jpeg' // File extension used for file icon during download. |
| 215 | + }; |
| 216 | + |
| 217 | +const fileDownloadHandler: FileDownloadHandler = async (userId, fileData) => { |
| 218 | + return new URL(fileData.url); |
| 219 | + } |
| 220 | + }; |
| 221 | +} |
| 222 | + |
| 223 | +``` |
| 224 | + |
| 225 | +## Configure upload method to use Azure Blob Storage |
| 226 | + |
| 227 | +To enable Azure Blob Storage upload, we need to modify the `uploadFileToAzureBlob` method we declared above with the following code. You need to replace the Azure Function information below to enable to upload. |
| 228 | + |
| 229 | +`App.tsx` |
| 230 | + |
| 231 | +```javascript |
| 232 | + |
| 233 | +const uploadFileToAzureBlob = async (fileUpload: FileUploadManager) => { |
| 234 | + const file = fileUpload.file; |
| 235 | + if (!file) { |
| 236 | + throw new Error('fileUpload.file is undefined'); |
| 237 | + } |
| 238 | + |
| 239 | + const filename = file.name; |
| 240 | + const fileExtension = file.name.split('.').pop(); |
| 241 | + |
| 242 | + // Following is an example of calling an Azure Function to handle file upload |
| 243 | + // The https://learn.microsoft.com/azure/developer/javascript/how-to/with-web-app/azure-function-file-upload |
| 244 | + // tutorial uses 'username' parameter to specify the storage container name. |
| 245 | + // Note that the container in the tutorial is private by default. To get default downloads working in |
| 246 | + // this sample, you need to change the container's access level to Public via Azure Portal. |
| 247 | + const username = 'ui-library'; |
| 248 | + |
| 249 | + // You can get function url from the Azure Portal: |
| 250 | + const azFunctionBaseUri='<YOUR_AZURE_FUNCTION_URL>'; |
| 251 | + const uri = `${azFunctionBaseUri}&username=${username}&filename=${filename}`; |
| 252 | + |
| 253 | + const formData = new FormData(); |
| 254 | + formData.append(file.name, file); |
| 255 | + |
| 256 | + const response = await axios.request({ |
| 257 | + method: "post", |
| 258 | + url: uri, |
| 259 | + data: formData, |
| 260 | + onUploadProgress: (p) => { |
| 261 | + // Optionally, you can update the file upload progess. |
| 262 | + fileUpload.notifyUploadProgressChanged(p.loaded / p.total); |
| 263 | + }, |
| 264 | + }); |
| 265 | + |
| 266 | + const storageBaseUrl = 'https://<YOUR_STORAGE_ACCOUNT>.blob.core.windows.net'; |
| 267 | + |
| 268 | + return { |
| 269 | + name: filename, |
| 270 | + url: `${storageBaseUrl}/${username}/${filename}`, |
| 271 | + extension: fileExtension |
| 272 | + }; |
| 273 | +} |
| 274 | + |
| 275 | +``` |
| 276 | + |
| 277 | +## Error Handling |
| 278 | + |
| 279 | +When an upload fails, the UI Library Chat Composite would display an error message. |
| 280 | + |
| 281 | + |
| 282 | + |
| 283 | +Here is sample code showcasing how you can fail an upload due to a size validation error by changing the `fileUploadHandler` above. |
| 284 | + |
| 285 | +`App.tsx` |
| 286 | + |
| 287 | +```javascript |
| 288 | +import { FileUploadHandler } from from '@azure/communication-react'; |
| 289 | + |
| 290 | +const fileUploadHandler: FileUploadHandler = async (userId, fileUploads) => { |
| 291 | + for (const fileUpload of fileUploads) { |
| 292 | + if (fileUpload.file && fileUpload.file.size > 99 * 1024 * 1024) { |
| 293 | + // Notify ChatComposite about upload failure. |
| 294 | + // Allows you to provide a custom error message. |
| 295 | + fileUpload.notifyUploadFailed('File too big. Select a file under 99 MB.'); |
| 296 | + } |
| 297 | + } |
| 298 | +} |
| 299 | +``` |
| 300 | + |
| 301 | +## File Downloads - Advanced Usage |
| 302 | + |
| 303 | +By default, the file `url` provided through `notifyUploadCompleted` method need be used to trigger a file download. However, if you need to handle a download in a different way, you can provide a custom `downloadHandler` to ChatComposite. Below we need to modify the `fileDownloadHandler` that we declared above to check for an authorized user before allowing to download the file. |
| 304 | + |
| 305 | +`App.tsx` |
| 306 | + |
| 307 | +```javascript |
| 308 | +import { FileDownloadHandler } from "communication-react"; |
| 309 | + |
| 310 | +const isUnauthorizedUser = (userId: string): boolean => { |
| 311 | + // You need to write your own logic here for this example. |
| 312 | +} |
| 313 | + |
| 314 | +const fileDownloadHandler: FileDownloadHandler = async (userId, fileData) => { |
| 315 | + if (isUnauthorizedUser(userId)) { |
| 316 | + // Error message to be displayed to the user. |
| 317 | + return { errorMessage: 'You don’t have permission to download this file.' }; |
| 318 | + } else { |
| 319 | + // If this function returns a Promise that resolves a URL string, |
| 320 | + // the URL is opened in a new tab. |
| 321 | + return new URL(fileData.url); |
| 322 | + } |
| 323 | +} |
| 324 | +``` |
| 325 | + |
| 326 | +Download error message shown in an error bar on top of the Chat Composite. |
| 327 | + |
| 328 | + |
| 329 | + |
| 330 | + |
| 331 | +## Clean up resources |
| 332 | + |
| 333 | +If you want to clean up and remove a Communication Services subscription, you can delete the resource or resource group. Deleting the resource group also deletes any other resources associated with it. You can find out more about [cleaning up Azure Communication Service resources](../quickstarts/create-communication-resource.md#clean-up-resources) and [cleaning Azure Function Resources](../../azure-functions/create-first-function-vs-code-csharp.md#clean-up-resources). |
| 334 | + |
| 335 | +## Next steps |
| 336 | + |
| 337 | +> [!div class="nextstepaction"] |
| 338 | +> [Check the rest of the UI Library](https://azure.github.io/communication-ui-library/) |
| 339 | +
|
| 340 | +You may also want to: |
| 341 | + |
| 342 | +- [Add chat to your app](../quickstarts/chat/get-started.md) |
| 343 | +- [Creating user access tokens](../quickstarts/identity/access-tokens.md) |
| 344 | +- [Learn about client and server architecture](../concepts/client-and-server-architecture.md) |
| 345 | +- [Learn about authentication](../concepts/authentication.md) |
0 commit comments