52 | >
53 | );
54 | };
55 |
56 | StickerPicker.propTypes = {
57 | handleStickerSend: PropTypes.func,
58 | };
59 |
60 | export default StickerPicker;
61 |
--------------------------------------------------------------------------------
/web-ui/src/components/chat/Avatars.jsx:
--------------------------------------------------------------------------------
1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: MIT-0
3 |
4 | import React from "react";
5 | import PropTypes from "prop-types";
6 | import { AVATARS } from "../../constants";
7 |
8 | const Avatars = ({ handleAvatarClick, currentAvatar }) => {
9 | return (
10 | <>
11 | {AVATARS.map((avatar) => {
12 | const selected = avatar.name === currentAvatar ? " selected" : "";
13 | return (
14 |
52 | );
53 | })}
54 | >
55 | );
56 | };
57 |
58 | Avatars.propTypes = {
59 | currentAvatar: PropTypes.string,
60 | handleAvatarClick: PropTypes.func,
61 | };
62 |
63 | export default Avatars;
64 |
--------------------------------------------------------------------------------
/web-ui/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
15 |
16 |
20 |
21 |
30 |
31 | Amazon IVS Chat Demo - Web
32 |
33 |
34 |
35 |
36 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/web-ui/src/constants.js:
--------------------------------------------------------------------------------
1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: MIT-0
3 |
4 | export const AVATARS = [
5 | {
6 | name: "bear",
7 | src: "https://d39ii5l128t5ul.cloudfront.net/assets/animals_square/bear.png",
8 | },
9 | {
10 | name: "bird",
11 | src: "https://d39ii5l128t5ul.cloudfront.net/assets/animals_square/bird.png",
12 | },
13 | {
14 | name: "bird2",
15 | src: "https://d39ii5l128t5ul.cloudfront.net/assets/animals_square/bird2.png",
16 | },
17 | {
18 | name: "dog",
19 | src: "https://d39ii5l128t5ul.cloudfront.net/assets/animals_square/dog.png",
20 | },
21 | {
22 | name: "giraffe",
23 | src: "https://d39ii5l128t5ul.cloudfront.net/assets/animals_square/giraffe.png",
24 | },
25 | {
26 | name: "hedgehog",
27 | src: "https://d39ii5l128t5ul.cloudfront.net/assets/animals_square/hedgehog.png",
28 | },
29 | {
30 | name: "hippo",
31 | src: "https://d39ii5l128t5ul.cloudfront.net/assets/animals_square/hippo.png",
32 | }
33 | ];
34 |
35 | export const STICKERS = [
36 | {
37 | name: "cute",
38 | src: "https://d39ii5l128t5ul.cloudfront.net/assets/chat/v1/sticker-1.png"
39 | },
40 | {
41 | name: "angry",
42 | src: "https://d39ii5l128t5ul.cloudfront.net/assets/chat/v1/sticker-2.png"
43 | },
44 | {
45 | name: "sad",
46 | src: "https://d39ii5l128t5ul.cloudfront.net/assets/chat/v1/sticker-3.png"
47 | },
48 | {
49 | name: "happy",
50 | src: "https://d39ii5l128t5ul.cloudfront.net/assets/chat/v1/sticker-4.png"
51 | },
52 | {
53 | name: "surprised",
54 | src: "https://d39ii5l128t5ul.cloudfront.net/assets/chat/v1/sticker-5.png"
55 | },
56 | {
57 | name: "cool",
58 | src: "https://d39ii5l128t5ul.cloudfront.net/assets/chat/v1/sticker-6.png"
59 | },
60 | {
61 | name: "love",
62 | src: "https://d39ii5l128t5ul.cloudfront.net/assets/chat/v1/sticker-7.png"
63 | },
64 | {
65 | name: "rocket",
66 | src: "https://d39ii5l128t5ul.cloudfront.net/assets/chat/v1/sticker-8.png"
67 | },
68 | {
69 | name: "confetti",
70 | src: "https://d39ii5l128t5ul.cloudfront.net/assets/chat/v1/sticker-9.png"
71 | },
72 | {
73 | name: "camera",
74 | src: "https://d39ii5l128t5ul.cloudfront.net/assets/chat/v1/sticker-10.png"
75 | }
76 | ]
--------------------------------------------------------------------------------
/serverless/src/chat-auth.js:
--------------------------------------------------------------------------------
1 | const AWS = require("aws-sdk");
2 | const IVSChat = new AWS.Ivschat();
3 |
4 | const response = {
5 | statusCode: 200,
6 | headers: {
7 | "Access-Control-Allow-Headers" : "Content-Type",
8 | "Access-Control-Allow-Origin": "*",
9 | "Access-Control-Allow-Methods": "POST"
10 | },
11 | body: ""
12 | };
13 |
14 | /**
15 | * A function that generates an IVS chat authentication token based on the request parameters.
16 | */
17 | exports.chatAuthHandler = async (event) => {
18 | if (event.httpMethod !== 'POST') {
19 | throw new Error(`chatAuthHandler only accepts POST method, you tried: ${event.httpMethod}`);
20 | }
21 |
22 | console.info('chatAuthHandler received:', event);
23 |
24 | // Parse the incoming request body
25 | const body = JSON.parse(event.body);
26 | const { arn, roomIdentifier, userId } = body;
27 | const roomId = arn || roomIdentifier;
28 | const additionalAttributes = body.attributes || {};
29 | const capabilities = body.capabilities || []; // The permission to view messages is implicit
30 | const durationInMinutes = body.durationInMinutes || 55; // default the expiration to 55 mintues
31 |
32 | if (!roomId || !userId) {
33 | response.statusCode = 400;
34 | response.body = { error: 'Missing parameters: `arn or roomIdentifier`, `userId`' };
35 | return response;
36 | }
37 |
38 | // Construct parameters.
39 | // Documentation is available at https://docs.aws.amazon.com/ivs/latest/ChatAPIReference/Welcome.html
40 | const params = {
41 | roomIdentifier: `${roomId}`,
42 | userId: `${userId}`,
43 | attributes: { ...additionalAttributes },
44 | capabilities: capabilities,
45 | sessionDurationInMinutes: durationInMinutes,
46 | };
47 |
48 | try {
49 | const data = await IVSChat.createChatToken(params).promise();
50 | console.info("Got data:", data);
51 | response.statusCode = 200;
52 | response.body = JSON.stringify(data);
53 | } catch (err) {
54 | console.error('ERROR: chatAuthHandler > IVSChat.createChatToken:', err);
55 | response.statusCode = 500;
56 | response.body = err.stack;
57 | }
58 |
59 | console.info(`response from: ${event.path} statusCode: ${response.statusCode} body: ${response.body}`);
60 | return response;
61 | }
62 |
--------------------------------------------------------------------------------
/serverless/template.yaml:
--------------------------------------------------------------------------------
1 | AWSTemplateFormatVersion: "2010-09-09"
2 | Transform: "AWS::Serverless-2016-10-31"
3 | Description: Amazon IVS Simple Chat Backend
4 |
5 | Globals:
6 | Api:
7 | Cors:
8 | AllowMethods: "'GET,POST,OPTIONS'"
9 | AllowHeaders: "'*'"
10 | AllowOrigin: "'*'"
11 | Function:
12 | Runtime: nodejs18.x
13 | Timeout: 30
14 | MemorySize: 128
15 |
16 | Resources:
17 | IvsChatLambdaRefLayer:
18 | Type: AWS::Serverless::LayerVersion
19 | Properties:
20 | LayerName: sam-app-dependencies
21 | Description: Dependencies for sam app [ivs-simple-chat-backend]
22 | ContentUri: dependencies/
23 | CompatibleRuntimes:
24 | - nodejs18.x
25 | LicenseInfo: "MIT"
26 | RetentionPolicy: Retain
27 | # This is a Lambda function config associated with the source code: chat-auth.js
28 | chatAuthFunction:
29 | Type: AWS::Serverless::Function
30 | Properties:
31 | Handler: src/chat-auth.chatAuthHandler
32 | Description: A function that generates an IVS chat authentication token based on the request parameters.
33 | Layers:
34 | - !Ref IvsChatLambdaRefLayer
35 | Policies:
36 | - Statement:
37 | Effect: Allow
38 | Action:
39 | - ivschat:*
40 | Resource: "*"
41 | Events:
42 | Api:
43 | Type: Api
44 | Properties:
45 | Path: /auth
46 | Method: POST
47 |
48 | # This is a Lambda function config associated with the source code: chat-event.js
49 | chatEventFunction:
50 | Type: AWS::Serverless::Function
51 | Properties:
52 | Handler: src/chat-event.chatEventHandler
53 | Description: A function that sends an event to a specified IVS chat room
54 | Layers:
55 | - !Ref IvsChatLambdaRefLayer
56 | Policies:
57 | - Statement:
58 | Effect: Allow
59 | Action:
60 | - ivschat:*
61 | Resource: "*"
62 | Events:
63 | Api:
64 | Type: Api
65 | Properties:
66 | Path: /event
67 | Method: POST
68 |
69 | # This is a Lambda function config associated with the source code: chat-event.js
70 | chatListFunction:
71 | Type: AWS::Serverless::Function
72 | Properties:
73 | Handler: src/chat-list.chatListHandler
74 | Description: A function that returns a list of available chat rooms
75 | Layers:
76 | - !Ref IvsChatLambdaRefLayer
77 | Policies:
78 | - Statement:
79 | Effect: Allow
80 | Action:
81 | - ivschat:*
82 | Resource: "*"
83 | Events:
84 | Api:
85 | Type: Api
86 | Properties:
87 | Path: /list
88 | Method: GET
89 |
90 | Outputs:
91 | ApiURL:
92 | Description: "API endpoint URL for Prod environment"
93 | Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/"
94 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Amazon IVS Chat Web Demo
2 |
3 | A demo web application intended as an educational tool to demonstrate how you can build a simple live video and chat application with [Amazon IVS](https://aws.amazon.com/ivs/).
4 |
5 |
6 |
7 | **This project is intended for education purposes only and not for production usage.**
8 |
9 | This is a serverless web application, leveraging [Amazon IVS](https://aws.amazon.com/ivs/) for video streaming and chat, [AWS Lambda](https://aws.amazon.com/lambda/), and [Amazon API Gateway](https://aws.amazon.com/api-gateway/). The web user interface is written in Javascript and built on React.
10 |
11 | The demo showcases how you can implement a simple live streaming application with video and chat using Amazon IVS. Viewers are asked to enter their name the first time they begin chatting. Chat users can send plain text messages, text links, emojis, and stickers. Chat moderators can delete messages and kick users.
12 |
13 | ## Getting Started
14 |
15 | This demo is comprised of two parts: `serverless` (the demo backend) and `web-ui` (the demo frontend).
16 |
17 | 1. If you do not have an AWS account, you can create one by following this guide: [How do I create and activate a new Amazon Web Services account?](https://aws.amazon.com/premiumsupport/knowledge-center/create-and-activate-aws-account/)
18 | 2. Log into the [AWS console](https://console.aws.amazon.com/) if you are not already. Note: If you are logged in as an IAM user, ensure your account has permissions to create and manage the necessary resources and components for this application.
19 | 3. Deploy the [serverless backend](./serverless/README.md) to your AWS account. The CloudFormation template will automate the deployment process.
20 |
21 | ## Known issues and limitations
22 |
23 | * The application is meant for demonstration purposes and **not** for production use.
24 | * This application is only tested in the us-west-2 (Oregon) region. Additional regions may be supported depending on service availability.
25 |
26 | ## About Amazon IVS
27 | Amazon Interactive Video Service (Amazon IVS) is a managed live streaming and stream chat solution that is quick and easy to set up, and ideal for creating interactive video experiences. [Learn more](https://aws.amazon.com/ivs/).
28 |
29 | * [Amazon IVS docs](https://docs.aws.amazon.com/ivs/)
30 | * [User Guide](https://docs.aws.amazon.com/ivs/latest/userguide/)
31 | * [API Reference](https://docs.aws.amazon.com/ivs/latest/APIReference/)
32 | * [Setting Up for Streaming with Amazon Interactive Video Service](https://aws.amazon.com/blogs/media/setting-up-for-streaming-with-amazon-ivs/)
33 | * [Learn more about Amazon IVS on IVS.rocks](https://ivs.rocks/)
34 | * [View more demos like this](https://ivs.rocks/examples)
35 |
36 | ## Security
37 |
38 | See [CONTRIBUTING](CONTRIBUTING.md#security-issue-notifications) for more information.
39 |
40 | ## License
41 |
42 | This library is licensed under the MIT-0 License. See the LICENSE file.
--------------------------------------------------------------------------------
/web-ui/src/components/chat/SignIn.jsx:
--------------------------------------------------------------------------------
1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: MIT-0
3 |
4 | import React, { useState, createRef, useEffect } from "react";
5 | import Avatars from "./Avatars";
6 |
7 | const SignIn = ({ handleSignIn }) => {
8 | const [username, setUsername] = useState("");
9 | const [moderator, setModerator] = useState(false);
10 | const [avatar, setAvatar] = useState({});
11 | const [loaded, setLoaded] = useState(false);
12 | const inputRef = createRef();
13 |
14 | useEffect(() => {
15 | setLoaded(true);
16 | inputRef.current.focus();
17 | }, [loaded]); // eslint-disable-line
18 |
19 | return (
20 |
21 |
22 |
Join the chat room
23 |
80 |
81 |
82 |
83 | );
84 | };
85 |
86 | export default SignIn;
87 |
--------------------------------------------------------------------------------
/CONTRIBUTING.md:
--------------------------------------------------------------------------------
1 | # Contributing Guidelines
2 |
3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional
4 | documentation, we greatly value feedback and contributions from our community.
5 |
6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary
7 | information to effectively respond to your bug report or contribution.
8 |
9 |
10 | ## Reporting Bugs/Feature Requests
11 |
12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features.
13 |
14 | When filing an issue, please check existing open, or recently closed, issues to make sure somebody else hasn't already
15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful:
16 |
17 | * A reproducible test case or series of steps
18 | * The version of our code being used
19 | * Any modifications you've made relevant to the bug
20 | * Anything unusual about your environment or deployment
21 |
22 |
23 | ## Contributing via Pull Requests
24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that:
25 |
26 | 1. You are working against the latest source on the *master* branch.
27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already.
28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted.
29 |
30 | To send us a pull request, please:
31 |
32 | 1. Fork the repository.
33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change.
34 | 3. Ensure local tests pass.
35 | 4. Commit to your fork using clear commit messages.
36 | 5. Send us a pull request, answering any default questions in the pull request interface.
37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation.
38 |
39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and
40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/).
41 |
42 |
43 | ## Finding contributions to work on
44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any 'help wanted' issues is a great place to start.
45 |
46 |
47 | ## Code of Conduct
48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct).
49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact
50 | opensource-codeofconduct@amazon.com with any additional questions or comments.
51 |
52 |
53 | ## Security issue notifications
54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue.
55 |
56 |
57 | ## Licensing
58 |
59 | See the [LICENSE](LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution.
60 |
61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes.
62 |
--------------------------------------------------------------------------------
/web-ui/src/components/videoPlayer/VideoPlayer.jsx:
--------------------------------------------------------------------------------
1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: MIT-0
3 |
4 | import React, { useEffect } from 'react';
5 |
6 | // Styles
7 | import './VideoPlayer.css';
8 |
9 | const VideoPlayer = ({
10 | usernameRaisedHand,
11 | showRaiseHandPopup,
12 | playbackUrl,
13 | }) => {
14 | useEffect(() => {
15 | const MediaPlayerPackage = window.IVSPlayer;
16 |
17 | // First, check if the browser supports the Amazon IVS player.
18 | if (!MediaPlayerPackage.isPlayerSupported) {
19 | console.warn(
20 | 'The current browser does not support the Amazon IVS player.'
21 | );
22 | return;
23 | }
24 |
25 | const PlayerState = MediaPlayerPackage.PlayerState;
26 | const PlayerEventType = MediaPlayerPackage.PlayerEventType;
27 |
28 | // Initialize player
29 | const player = MediaPlayerPackage.create();
30 | player.attachHTMLVideoElement(document.getElementById('video-player'));
31 |
32 | // Attach event listeners
33 | player.addEventListener(PlayerState.PLAYING, () => {
34 | console.info('Player State - PLAYING');
35 | });
36 | player.addEventListener(PlayerState.ENDED, () => {
37 | console.info('Player State - ENDED');
38 | });
39 | player.addEventListener(PlayerState.READY, () => {
40 | console.info('Player State - READY');
41 | });
42 | player.addEventListener(PlayerEventType.ERROR, (err) => {
43 | console.warn('Player Event - ERROR:', err);
44 | });
45 |
46 | // Setup stream and play
47 | player.setAutoplay(true);
48 | player.load(playbackUrl);
49 | player.setVolume(0.5);
50 | }, []); // eslint-disable-line
51 |
52 | return (
53 | <>
54 |
55 |
56 |
62 |
63 | {showRaiseHandPopup ? (
64 |
65 |
66 |
73 |
74 | {usernameRaisedHand} raised their hand
75 |
76 | ) : (
77 | <>>
78 | )}
79 |
80 |
81 |
82 | >
83 | );
84 | };
85 |
86 | export default VideoPlayer;
87 |
--------------------------------------------------------------------------------
/web-ui/README.md:
--------------------------------------------------------------------------------
1 | # Amazon IVS Chat Web Demo Frontend
2 |
3 | ## Prerequisites
4 |
5 | * [NodeJS](https://nodejs.org/)
6 | * Npm is installed with Node.js
7 | * Amazon IVS Chat Demo backend (see [README.md](../serverless) in the `serverless` folder for details on the configuration)
8 |
9 | ## Configuration
10 |
11 | The following entries in `src/config.js` (inside the web-ui project directory) are used to configure the live video player and the chat websocket address. Both values will be made available to you when setting up the serverless backend using AWS CloudFormation. [Show me how](../serverless).
12 |
13 | * `PLAYBACK_URL`
14 | * Amazon IVS live video stream to play inside the video player
15 | * `CHAT_REGION`
16 | * The AWS region of the chat room. For example, `us-west-2`.
17 | * `API_URL`
18 | * Endpoint for the [Amazon IVS Chat Demo](../serverless) Backend
19 | * `CHAT_ROOM_ID`
20 | * The ID (or ARN) of the Amazon IVS Chat Room that the app should use.
21 | * You must create an Amazon IVS Chat Room to get a chat room ID/ARN. Refer to [Getting Started with Amazon IVS Chat](https://docs.aws.amazon.com/ivs/latest/userguide/getting-started-chat.html) for a detailed guide.
22 |
23 | ## Running the demo
24 |
25 | After you are done configuring the app, follow these instructions to run the demo:
26 |
27 | 1. [Install NodeJS](https://nodejs.org/). Download latest LTS version ("Recommended for Most Users")
28 | 2. Navigate to the web-ui project directory on your local computer
29 | 3. Run: npm install
30 | 4. Run: npm start
31 |
32 | ## Limitations
33 |
34 | * Message and user data for this demo is not saved/persistent (ie. reloading the page would go back to initial state).
35 |
36 | --------------------------------------------------
37 |
38 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app).
39 |
40 | ## Available Scripts
41 |
42 | In the project directory, you can run:
43 |
44 | ### `npm start`
45 |
46 | Runs the app in the development mode.
47 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser.
48 |
49 | The page will reload if you make edits.
50 | You will also see any lint errors in the console.
51 |
52 | ### `npm test`
53 |
54 | Launches the test runner in the interactive watch mode.
55 | See the section about [running tests](https://create-react-app.dev/docs/running-tests/) for more information.
56 |
57 | ### `npm run build`
58 |
59 | Builds the app for production to the `build` folder.
60 | It correctly bundles React in production mode and optimizes the build for the best performance.
61 |
62 | The build is minified and the filenames include the hashes.
63 | Your app is ready to be deployed!
64 |
65 | See the section about [deployment](https://create-react-app.dev/docs/deployment/) for more information.
66 |
67 | ## Learn More
68 |
69 | You can learn more in the [Create React App documentation](https://create-react-app.dev/docs/getting-started/).
70 | To learn React, check out the [React documentation](https://reactjs.org/).
71 |
72 | ### Code Splitting
73 |
74 | https://create-react-app.dev/docs/code-splitting/
75 |
76 | ### Analyzing the Bundle Size
77 |
78 | https://create-react-app.dev/docs/analyzing-the-bundle-size/
79 |
80 | ### Making a Progressive Web App
81 |
82 | https://create-react-app.dev/docs/making-a-progressive-web-app/
83 |
84 | ### Advanced Configuration
85 |
86 | https://create-react-app.dev/docs/advanced-configuration/
87 |
88 | ### Deployment
89 |
90 | https://create-react-app.dev/docs/deployment/
91 |
92 | ### `npm run build` fails to minify
93 |
94 | https://create-react-app.dev/docs/troubleshooting/#npm-run-build-fails-to-minify
95 |
--------------------------------------------------------------------------------
/web-ui/src/serviceWorker.js:
--------------------------------------------------------------------------------
1 | // This optional code is used to register a service worker.
2 | // register() is not called by default.
3 |
4 | // This lets the app load faster on subsequent visits in production, and gives
5 | // it offline capabilities. However, it also means that developers (and users)
6 | // will only see deployed updates on subsequent visits to a page, after all the
7 | // existing tabs open on the page have been closed, since previously cached
8 | // resources are updated in the background.
9 |
10 | // To learn more about the benefits of this model and instructions on how to
11 | // opt-in, read https://bit.ly/CRA-PWA
12 |
13 | const isLocalhost = Boolean(
14 | window.location.hostname === 'localhost' ||
15 | // [::1] is the IPv6 localhost address.
16 | window.location.hostname === '[::1]' ||
17 | // 127.0.0.0/8 are considered localhost for IPv4.
18 | window.location.hostname.match(
19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
20 | )
21 | );
22 |
23 | export function register(config) {
24 | if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
25 | // The URL constructor is available in all browsers that support SW.
26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href);
27 | if (publicUrl.origin !== window.location.origin) {
28 | // Our service worker won't work if PUBLIC_URL is on a different origin
29 | // from what our page is served on. This might happen if a CDN is used to
30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374
31 | return;
32 | }
33 |
34 | window.addEventListener('load', () => {
35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
36 |
37 | if (isLocalhost) {
38 | // This is running on localhost. Let's check if a service worker still exists or not.
39 | checkValidServiceWorker(swUrl, config);
40 |
41 | // Add some additional logging to localhost, pointing developers to the
42 | // service worker/PWA documentation.
43 | navigator.serviceWorker.ready.then(() => {
44 | console.log(
45 | 'This web app is being served cache-first by a service ' +
46 | 'worker. To learn more, visit https://bit.ly/CRA-PWA'
47 | );
48 | });
49 | } else {
50 | // Is not localhost. Just register service worker
51 | registerValidSW(swUrl, config);
52 | }
53 | });
54 | }
55 | }
56 |
57 | function registerValidSW(swUrl, config) {
58 | navigator.serviceWorker
59 | .register(swUrl)
60 | .then(registration => {
61 | registration.onupdatefound = () => {
62 | const installingWorker = registration.installing;
63 | if (installingWorker == null) {
64 | return;
65 | }
66 | installingWorker.onstatechange = () => {
67 | if (installingWorker.state === 'installed') {
68 | if (navigator.serviceWorker.controller) {
69 | // At this point, the updated precached content has been fetched,
70 | // but the previous service worker will still serve the older
71 | // content until all client tabs are closed.
72 | console.log(
73 | 'New content is available and will be used when all ' +
74 | 'tabs for this page are closed. See https://bit.ly/CRA-PWA.'
75 | );
76 |
77 | // Execute callback
78 | if (config && config.onUpdate) {
79 | config.onUpdate(registration);
80 | }
81 | } else {
82 | // At this point, everything has been precached.
83 | // It's the perfect time to display a
84 | // "Content is cached for offline use." message.
85 | console.log('Content is cached for offline use.');
86 |
87 | // Execute callback
88 | if (config && config.onSuccess) {
89 | config.onSuccess(registration);
90 | }
91 | }
92 | }
93 | };
94 | };
95 | })
96 | .catch(error => {
97 | console.error('Error during service worker registration:', error);
98 | });
99 | }
100 |
101 | function checkValidServiceWorker(swUrl, config) {
102 | // Check if the service worker can be found. If it can't reload the page.
103 | fetch(swUrl, {
104 | headers: { 'Service-Worker': 'script' },
105 | })
106 | .then(response => {
107 | // Ensure service worker exists, and that we really are getting a JS file.
108 | const contentType = response.headers.get('content-type');
109 | if (
110 | response.status === 404 ||
111 | (contentType != null && contentType.indexOf('javascript') === -1)
112 | ) {
113 | // No service worker found. Probably a different app. Reload the page.
114 | navigator.serviceWorker.ready.then(registration => {
115 | registration.unregister().then(() => {
116 | window.location.reload();
117 | });
118 | });
119 | } else {
120 | // Service worker found. Proceed as normal.
121 | registerValidSW(swUrl, config);
122 | }
123 | })
124 | .catch(() => {
125 | console.log(
126 | 'No internet connection found. App is running in offline mode.'
127 | );
128 | });
129 | }
130 |
131 | export function unregister() {
132 | if ('serviceWorker' in navigator) {
133 | navigator.serviceWorker.ready
134 | .then(registration => {
135 | registration.unregister();
136 | })
137 | .catch(error => {
138 | console.error(error.message);
139 | });
140 | }
141 | }
142 |
--------------------------------------------------------------------------------
/.github/workflows/update-ivs-sdks.yml:
--------------------------------------------------------------------------------
1 | name: Update Amazon IVS SDKs
2 |
3 | on:
4 | schedule:
5 | - cron: '0 0 * * *' # Run daily at midnight UTC
6 | workflow_dispatch: # Allow manual triggering
7 |
8 | jobs:
9 | update-ivs-sdks:
10 | runs-on: ubuntu-latest
11 | permissions:
12 | contents: write
13 | pull-requests: write
14 | steps:
15 | - uses: actions/checkout@v4
16 |
17 | - name: Setup Node.js
18 | uses: actions/setup-node@v4
19 | with:
20 | node-version: '20'
21 |
22 | - name: Check for Player SDK updates
23 | id: check-player
24 | run: |
25 | # Extract current version from index.html
26 | CURRENT_VERSION=$(grep -oP 'player\.live-video\.net/\K[0-9]+\.[0-9]+\.[0-9]+' web-ui/public/index.html)
27 | echo "Current Player SDK version: $CURRENT_VERSION"
28 |
29 | # Get latest version from npm
30 | LATEST_VERSION=$(npm view amazon-ivs-player version)
31 | echo "Latest Player SDK version: $LATEST_VERSION"
32 |
33 | # Compare versions
34 | if [ "$CURRENT_VERSION" != "$LATEST_VERSION" ]; then
35 | echo "Player SDK update available: $CURRENT_VERSION -> $LATEST_VERSION"
36 | echo "has_update=true" >> $GITHUB_OUTPUT
37 | echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
38 | echo "new_version=$LATEST_VERSION" >> $GITHUB_OUTPUT
39 |
40 | # Update the script src in index.html
41 | sed -i "s|player\.live-video\.net/$CURRENT_VERSION|player.live-video.net/$LATEST_VERSION|g" web-ui/public/index.html
42 | else
43 | echo "No Player SDK update available"
44 | echo "has_update=false" >> $GITHUB_OUTPUT
45 | fi
46 |
47 | - name: Check for Chat Messaging SDK updates
48 | id: check-chat
49 | run: |
50 | cd web-ui
51 |
52 | # Extract current version from package.json
53 | CURRENT_VERSION=$(node -p "require('./package.json').dependencies['amazon-ivs-chat-messaging']" | sed 's/[\^~]//g')
54 | echo "Current Chat Messaging SDK version: $CURRENT_VERSION"
55 |
56 | # Get latest version from npm
57 | LATEST_VERSION=$(npm view amazon-ivs-chat-messaging version)
58 | echo "Latest Chat Messaging SDK version: $LATEST_VERSION"
59 |
60 | # Compare versions
61 | if [ "$CURRENT_VERSION" != "$LATEST_VERSION" ]; then
62 | echo "Chat Messaging SDK update available: $CURRENT_VERSION -> $LATEST_VERSION"
63 | echo "has_update=true" >> $GITHUB_OUTPUT
64 | echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
65 | echo "new_version=$LATEST_VERSION" >> $GITHUB_OUTPUT
66 |
67 | # Update package.json
68 | npm install amazon-ivs-chat-messaging@$LATEST_VERSION --save-exact
69 | # Change back to caret notation to match existing style
70 | sed -i "s|\"amazon-ivs-chat-messaging\": \"$LATEST_VERSION\"|\"amazon-ivs-chat-messaging\": \"^$LATEST_VERSION\"|g" package.json
71 | else
72 | echo "No Chat Messaging SDK update available"
73 | echo "has_update=false" >> $GITHUB_OUTPUT
74 | fi
75 |
76 | - name: Prepare PR details
77 | id: pr-details
78 | run: |
79 | PLAYER_UPDATE="${{ steps.check-player.outputs.has_update }}"
80 | CHAT_UPDATE="${{ steps.check-chat.outputs.has_update }}"
81 |
82 | if [ "$PLAYER_UPDATE" = "true" ] || [ "$CHAT_UPDATE" = "true" ]; then
83 | echo "has_changes=true" >> $GITHUB_OUTPUT
84 |
85 | # Build commit message and PR title
86 | if [ "$PLAYER_UPDATE" = "true" ] && [ "$CHAT_UPDATE" = "true" ]; then
87 | TITLE="chore: update Amazon IVS Player SDK to ${{ steps.check-player.outputs.new_version }} and Chat Messaging SDK to ${{ steps.check-chat.outputs.new_version }}"
88 | BODY="This PR updates both Amazon IVS SDKs:
89 |
90 | - **Player SDK**: ${{ steps.check-player.outputs.current_version }} → ${{ steps.check-player.outputs.new_version }}
91 | - **Chat Messaging SDK**: ${{ steps.check-chat.outputs.current_version }} → ${{ steps.check-chat.outputs.new_version }}
92 |
93 | ## Changes
94 | - Updated Player SDK CDN URL in \`web-ui/public/index.html\`
95 | - Updated Chat Messaging SDK version in \`web-ui/package.json\`
96 |
97 | This update was automatically generated by the dependency update workflow."
98 | elif [ "$PLAYER_UPDATE" = "true" ]; then
99 | TITLE="chore: update Amazon IVS Player SDK to ${{ steps.check-player.outputs.new_version }}"
100 | BODY="This PR updates the Amazon IVS Player SDK from ${{ steps.check-player.outputs.current_version }} to ${{ steps.check-player.outputs.new_version }}.
101 |
102 | ## Changes
103 | - Updated Player SDK CDN URL in \`web-ui/public/index.html\`
104 |
105 | This update was automatically generated by the dependency update workflow."
106 | else
107 | TITLE="chore: update amazon-ivs-chat-messaging to ${{ steps.check-chat.outputs.new_version }}"
108 | BODY="This PR updates the amazon-ivs-chat-messaging package from ${{ steps.check-chat.outputs.current_version }} to ${{ steps.check-chat.outputs.new_version }}.
109 |
110 | ## Changes
111 | - Updated Chat Messaging SDK version in \`web-ui/package.json\`
112 |
113 | This update was automatically generated by the dependency update workflow."
114 | fi
115 |
116 | # Use environment files for multiline output
117 | echo "PR_TITLE=$TITLE" >> $GITHUB_ENV
118 | echo "PR_BODY<> $GITHUB_ENV
119 | echo "$BODY" >> $GITHUB_ENV
120 | echo "EOF" >> $GITHUB_ENV
121 | else
122 | echo "has_changes=false" >> $GITHUB_OUTPUT
123 | fi
124 |
125 | - name: Create Pull Request
126 | if: steps.pr-details.outputs.has_changes == 'true'
127 | uses: peter-evans/create-pull-request@v6
128 | with:
129 | commit-message: ${{ env.PR_TITLE }}
130 | title: ${{ env.PR_TITLE }}
131 | body: ${{ env.PR_BODY }}
132 | branch: dependency-update/ivs-sdks
133 | delete-branch: true
134 | labels: dependencies
135 |
--------------------------------------------------------------------------------
/serverless/README.md:
--------------------------------------------------------------------------------
1 | # Amazon IVS Chat Demo Backend
2 |
3 | This readme includes instructions for deploying the Amazon IVS Chat Demo backend to an AWS Account. This backend supports the following Amazon IVS Chat Demos:
4 |
5 | * [Amazon IVS Chat Web Demo](https://github.com/aws-samples/amazon-ivs-chat-web-demo)
6 | * [Amazon IVS Chat iOS Demo](https://github.com/aws-samples/amazon-ivs-chat-for-ios-demo)
7 | * [Amazon IVS Chat Android Demo](https://github.com/aws-samples/amazon-ivs-chat-for-android-demo)
8 |
9 | ## Application overview
10 |
11 |
12 |
13 | The chat demo backend emits event messages and handles encrypted chat tokens, which authorize users to perform actions in your chat room. In this demo, the backend simply accepts whatever information the client provides. When you launch the client app, you’ll be able to pick a username, profile picture, and whether or not you want moderator permissions. In a production setting, the backend application would likely interface with your existing user service to determine the capabilities to grant a client. For example, a client that is marked as a default user in your user database might only be authorized view and send messages, but a client marked as a moderator could also be authorized to delete messages and disconnect users.
14 |
15 | ## Prerequisites
16 |
17 | * [AWS CLI Version 2](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html)
18 | * [AWS SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html)
19 | * Access to an AWS Account with at least the following permissions:
20 | * Create IAM roles
21 | * Create Lambda Functions
22 | * Authenticate and send Events in Amazon IVS Chat
23 | * Create Amazon S3 Buckets
24 |
25 | ***IMPORTANT NOTE:** Running this demo application in your AWS account will create and consume AWS resources, which will cost money.* Amazon IVS is eligible for the AWS free tier. Visit the Amazon IVS [pricing page](https://aws.amazon.com/ivs/pricing/) for more details.
26 |
27 | ## Run this app locally
28 |
29 | Before you start, run the following command to make sure you're in the correct AWS account (or configure as needed):
30 |
31 | ```bash
32 | aws configure
33 | ```
34 |
35 | ### 1. Install AWS SDK
36 |
37 | Navigate to `serverless/dependencies/nodejs` and run `npm install` to install the AWS SDK.
38 |
39 | ### 2. Start the local api
40 |
41 | Navigate back to the `serverless` directory and run the following command:
42 |
43 | ```bash
44 | sam local start-api -p 3100
45 | ```
46 |
47 | For a full list of command flags, refer to the [SAM CLI documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-cli-command-reference-sam-local-start-api.html)
48 |
49 |
50 | ## Deployment instructions
51 |
52 | Before you start, run the following command to make sure you're in the correct AWS account (or configure as needed):
53 |
54 | ```bash
55 | aws configure
56 | ```
57 |
58 | For configuration specifics, refer to the [AWS CLI User Guide](https://docs.aws.amazon.com/cli/latest/userguide/cli-chap-configure.html)
59 |
60 | ### 1. Install AWS SDK
61 |
62 | Navigate to `serverless/dependencies/nodejs` and run `npm install` to install the AWS SDK.
63 |
64 | ### 2. Create an S3 bucket
65 |
66 | * Replace `` with a name for your S3 Bucket.
67 | * Replace `` with your AWS region. The following regions are currently supported:
68 | * us-east-1
69 | * us-west-2
70 | * eu-east-1
71 |
72 | ```bash
73 | aws s3api create-bucket --bucket --region \
74 | --create-bucket-configuration LocationConstraint=
75 | ```
76 |
77 | ### 3. Pack template with SAM
78 |
79 | ```bash
80 | sam package --template-file template.yaml \
81 | --s3-bucket \
82 | --output-template-file output.yaml
83 | ```
84 |
85 | DO NOT run the output from above command, proceed to next step.
86 |
87 | ### 4. Deploy the packaged template
88 |
89 | * Replace `` with a name of your choice. The stack name will be used to reference the application.
90 | * Replace `` with the AWS region you entered in Step 1.
91 |
92 | ```bash
93 | sam deploy --template-file ./output.yaml \
94 | --stack-name \
95 | --capabilities CAPABILITY_IAM \
96 | --region
97 | ```
98 |
99 | ### Use your deployed backend in the client applications
100 |
101 | When the deployment successfully completes, copy the output `ApiURL` for use in the various client application demos:
102 |
103 | * [Amazon IVS Chat Web Demo](https://github.com/aws-samples/amazon-ivs-chat-web-demo)
104 | * [Amazon IVS Chat iOS Demo](https://github.com/aws-samples/amazon-ivs-chat-for-ios-demo)
105 | * [Amazon IVS Chat Android Demo](https://github.com/aws-samples/amazon-ivs-chat-for-android-demo)
106 |
107 | ### Accessing the deployed application
108 |
109 | If needed, you can retrieve the Cloudformation stack outputs by running the following command:
110 |
111 | * Replace `` with the name of your stack from Step 3.
112 |
113 | ```bash
114 | aws cloudformation describe-stacks --stack-name \
115 | --query 'Stacks[].Outputs'
116 | ```
117 |
118 | ## Cleanup
119 |
120 | To delete the deployed application, you can use the AWS CLI. You can also visit the [AWS Cloudformation Console](https://us-west-2.console.aws.amazon.com/cloudformation/home) to manage your application.
121 |
122 | Delete Cloudformation stack:
123 |
124 | ```bash
125 | aws cloudformation delete-stack --stack-name
126 | ```
127 |
128 | Remove files in S3 bucket
129 |
130 | ```bash
131 | aws s3 rm s3:// --recursive
132 | ```
133 |
134 | Delete S3 bucket
135 |
136 | ```bash
137 | aws s3api delete-bucket --bucket --region
138 | ```
139 |
140 | ## Resources
141 |
142 | For an introduction to the AWS SAM specification, the AWS SAM CLI, and serverless application concepts, see the [AWS SAM Developer Guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html).
143 |
144 | Next, you can use the AWS Serverless Application Repository to deploy ready-to-use apps that go beyond Hello World samples and learn how authors developed their applications. For more information, see the [AWS Serverless Application Repository main page](https://aws.amazon.com/serverless/serverlessrepo/) and the [AWS Serverless Application Repository Developer Guide](https://docs.aws.amazon.com/serverlessrepo/latest/devguide/what-is-serverlessrepo.html).
145 |
--------------------------------------------------------------------------------
/THIRD-PARTY-LICENSES.txt:
--------------------------------------------------------------------------------
1 | The Amazon IVS Chat for Web Demo includes the following third-party software/licensing:
2 |
3 | ** Material-design-icons - https://github.com/google/material-design-icons
4 |
5 | Apache License
6 | Version 2.0, January 2004
7 | http://www.apache.org/licenses/
8 |
9 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
10 |
11 | 1. Definitions.
12 |
13 | "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
14 |
15 | "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
16 |
17 | "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
18 |
19 | "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
20 |
21 | "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
22 |
23 | "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
24 |
25 | "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
26 |
27 | "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
28 |
29 | "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
30 |
31 | "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
32 |
33 | 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
34 |
35 | 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
36 |
37 | 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
38 |
39 | You must give any other recipients of the Work or Derivative Works a copy of this License; and
40 | You must cause any modified files to carry prominent notices stating that You changed the files; and
41 | You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
42 | If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
43 |
44 | You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
45 | 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
46 |
47 | 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
48 |
49 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
50 |
51 | 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
52 |
53 | 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
54 |
55 | END OF TERMS AND CONDITIONS
56 |
--------------------------------------------------------------------------------
/web-ui/src/components/chat/Chat.css:
--------------------------------------------------------------------------------
1 | /* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. */
2 | /* SPDX-License-Identifier: MIT-0 */
3 |
4 | :root {
5 | --section-max-width: auto;
6 | --color--primary: #000;
7 | --color-bg-modal-overlay: rgba(185, 185, 192, 0.9);
8 | --color-bg-chat-sticker: #fee77f;
9 | --chat-width: 600px;
10 | --sticker-columns: repeat(5, 1fr);
11 | --hand-raise-transform: translateY(-0.4rem);
12 | }
13 |
14 | .main {
15 | height: calc(100vh - var(--header-height));
16 | }
17 |
18 | .content-wrapper {
19 | width: 100%;
20 | height: 100%;
21 | background: var(--color-bg-chat);
22 | display: flex;
23 | flex-direction: row;
24 | position: relative;
25 | align-items: stretch;
26 | margin: 0 auto;
27 | }
28 |
29 | .player-wrapper {
30 | width: 100%;
31 | background: black;
32 | position: relative;
33 | overflow: hidden;
34 | display: flex;
35 | justify-content: center;
36 | align-items: center;
37 | }
38 |
39 | .raise-hand {
40 | width: 100%;
41 | height: 65px;
42 | color: #0080bf;
43 | background: #ccf9ff;
44 | border-radius: 30px;
45 | position: absolute;
46 | display: flex;
47 | justify-content: center;
48 | align-items: center;
49 | bottom: 5px;
50 | }
51 |
52 | .aspect-spacer {
53 | padding-bottom: 56.25%;
54 | }
55 |
56 | .el-player {
57 | width: 100%;
58 | height: 100%;
59 | position: absolute;
60 | top: 0;
61 | background: #000;
62 | }
63 |
64 | .col-wrapper {
65 | width: var(--chat-width);
66 | background: var(--color-bg-chat);
67 | flex-shrink: 0;
68 | align-self: stretch;
69 | position: relative;
70 | }
71 |
72 | .hidden {
73 | display: none !important;
74 | }
75 |
76 | .btn:disabled {
77 | opacity: 0.5;
78 | background: var(--color-bg-button-primary-default);
79 | }
80 |
81 | .chat-line-btn > svg {
82 | fill: currentColor;
83 | }
84 |
85 | .input-line-btn {
86 | padding: 0;
87 | margin: 0;
88 | width: var(--input-height);
89 | height: var(--input-height);
90 | border-radius: var(--input-height);
91 | overflow: hidden;
92 | margin: 0 5px 5px 0;
93 | flex-shrink: 0;
94 | border: 2px solid transparent;
95 | display: inline-flex;
96 | justify-content: center;
97 | align-items: center;
98 | fill: currentColor;
99 | color: var(--color-text-hint);
100 | }
101 |
102 | .raise-hand-btn {
103 | fill: currentColor;
104 | position: relative;
105 | overflow: visible;
106 | }
107 |
108 | .raise-hand-btn:before {
109 | position: absolute;
110 | display: flex;
111 | align-items: center;
112 | justify-content: center;
113 | content: '\2191'; /* up arrow */
114 | top: -0.5rem;
115 | right: -0.5rem;
116 | width: 1.8rem;
117 | height: 1.8rem;
118 | z-index: 9;
119 | border-radius: 1.2rem;
120 | font-size: 1.2rem;
121 | color: inherit;
122 | background: inherit;
123 | border: 2px solid var(--color-bg-chat);
124 | font-weight: bold;
125 | transition: transform 250ms cubic-bezier(0.4, 0, 0.2, 1);
126 | transform: translateY(0rem);
127 | }
128 |
129 | .raise-hand-btn:hover:before {
130 | background: var(--color--positive);
131 | color: var(--color-text-inverted);
132 | transform: translateY(0rem);
133 | }
134 |
135 | .raise-hand-btn:focus:before {
136 | border-color: var(--color-bg-primary);
137 | }
138 |
139 | .raise-hand-btn--raised {
140 | background-color: var(--color-bg-inverted);
141 | color: var(--color-text-inverted);
142 | }
143 |
144 | .raise-hand-btn--raised:before {
145 | content: '\2193'; /* down arrow */
146 | background: var(--color--destructive);
147 | transform: var(--hand-raise-transform);
148 | }
149 |
150 | .raise-hand-btn--raised:hover:before {
151 | background: var(--color--destructive);
152 | color: var(--color-text-inverted);
153 | transform: translateY(0rem);
154 | }
155 |
156 | .raise-hand-btn--raised:focus:before,
157 | .raise-hand-btn--raised:hover:before {
158 | color: var(--color-text-inverted);
159 | }
160 |
161 | .input-line-btn:hover {
162 | color: var(--color-text-base);
163 | background-color: var(--color-bg-button-hover);
164 | }
165 |
166 | .input-line-btn:focus {
167 | color: var(--color-text-base);
168 | border-color: var(--color-bg-primary);
169 | background: var(--color-bg-button-focus);
170 | }
171 |
172 | /* Chat */
173 | .chat-wrapper {
174 | position: absolute;
175 | top: 0;
176 | bottom: 0;
177 | left: 0;
178 | right: 0;
179 | padding-bottom: calc(var(--input-height) + 3rem);
180 | display: flex;
181 | flex-direction: column;
182 | align-items: flex-start;
183 | }
184 |
185 | .chat-wrapper .messages {
186 | height: 100%;
187 | width: 100%;
188 | overflow-y: auto;
189 | display: flex;
190 | flex-direction: column;
191 | align-items: flex-start;
192 | padding: 1rem 1.5rem;
193 | }
194 |
195 | .composer button.btn {
196 | margin-bottom: 0;
197 | }
198 |
199 | .error-line {
200 | padding: 6px 15px;
201 | background: var(--color-bg-destructive);
202 | border-radius: var(--input-height);
203 | display: flex;
204 | margin: 0 0 5px 0;
205 | }
206 |
207 | .error-line p {
208 | font-size: 1.2rem;
209 | display: inline;
210 | font-weight: bold;
211 | color: white;
212 | }
213 |
214 | .success-line {
215 | padding: 6px 15px;
216 | background: var(--color-bg-positive);
217 | border-radius: var(--input-height);
218 | display: flex;
219 | margin: 0 0 5px 0;
220 | }
221 |
222 | .success-line p {
223 | font-size: 1.2rem;
224 | display: inline;
225 | font-weight: bold;
226 | color: white;
227 | }
228 |
229 | .chat-line {
230 | flex-grow: 1;
231 | padding: 1.2rem 1.6rem 1.2rem 1.2rem;
232 | background: var(--color-bg-chat-bubble);
233 | border-radius: 2.4rem;
234 | display: flex;
235 | align-items: center;
236 | margin: 0 0.5rem 0.5rem 0;
237 | }
238 |
239 | .chat-line p {
240 | display: inline;
241 | font-weight: normal;
242 | }
243 |
244 | .chat-line .username {
245 | font-weight: 800;
246 | padding-right: 0.1rem;
247 | }
248 |
249 | .chat-line .username::after {
250 | content: '\00a0 ';
251 | }
252 |
253 | .chat-line--sticker {
254 | background: var(--color-bg-chat-sticker);
255 | will-change: transform;
256 | animation: scaleIn 250ms cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
257 | }
258 |
259 | .chat-line-wrapper {
260 | display: flex;
261 | align-items: flex-start;
262 | }
263 |
264 | .chat-line-sticker-wrapper {
265 | display: flex;
266 | align-items: flex-start;
267 | }
268 |
269 | .chat-line-actions {
270 | flex-shrink: 0;
271 | height: 100%;
272 | display: flex;
273 | align-items: flex-start;
274 | }
275 |
276 | .chat-line-actions button:first-child {
277 | margin-right: 5px;
278 | }
279 |
280 | .chat-line-img {
281 | margin: 0;
282 | padding: 0;
283 | width: 2.4rem;
284 | height: 2.4rem;
285 | border-radius: 1.2rem;
286 | overflow: hidden;
287 | margin-right: 0.5rem;
288 | display: inline;
289 | flex-shrink: 0;
290 | border: 2px solid transparent;
291 | }
292 |
293 | .chat-line-btn {
294 | padding: 0;
295 | margin: 0;
296 | width: 4.8rem;
297 | height: 4.8rem;
298 | border-radius: 2.4rem;
299 | overflow: hidden;
300 | margin: 0 5px 5px 0;
301 | flex-shrink: 0;
302 | border: 2px solid transparent;
303 | display: inline-flex;
304 | justify-content: center;
305 | align-items: center;
306 | color: var(--color-text-hint);
307 | }
308 |
309 | .chat-line-btn:hover {
310 | color: var(--color-text-destructive);
311 | background: var(--color-bg-button-hover);
312 | }
313 |
314 | .chat-line-btn:focus {
315 | color: var(--color-text-destructive);
316 | border-color: var(--color-bg-primary);
317 | background: var(--color-bg-button-focus);
318 | }
319 |
320 | .composer {
321 | position: absolute;
322 | bottom: 0;
323 | left: 0;
324 | right: 0;
325 | padding: 1rem 1.5rem;
326 | background: var(--color-bg-chat);
327 | }
328 |
329 | .composer input {
330 | width: 100%;
331 | }
332 |
333 | .chat-sticker {
334 | width: 10rem;
335 | height: 10rem;
336 | object-fit: contain;
337 | display: inline;
338 | animation: scaleIn 500ms cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
339 | }
340 |
341 | .stickers-container {
342 | position: absolute;
343 | bottom: calc(var(--input-height) + 2rem);
344 | max-height: 18rem;
345 | overflow-x: hidden;
346 | overflow-y: auto;
347 | right: 0;
348 | left: 0;
349 | padding: 1rem;
350 | margin: 1rem;
351 | display: grid;
352 | grid-template-columns: var(--sticker-columns);
353 | background: var(--color-bg-chat);
354 | border-radius: var(--radius-small);
355 | z-index: 9;
356 | box-shadow: 0 6px 30px rgba(0, 0, 0, 0.08);
357 | }
358 |
359 | .sticker-item {
360 | object-fit: contain;
361 | width: 100%;
362 | height: 100%;
363 | transition: transform 250ms cubic-bezier(0.075, 0.82, 0.165, 1);
364 | }
365 |
366 | .sticker-btn {
367 | width: 100%;
368 | height: 9rem;
369 | padding: 1rem;
370 | display: flex;
371 | flex-shrink: 0;
372 | flex-grow: 1;
373 | align-items: center;
374 | justify-content: center;
375 | background: var(--color-bg-chat);
376 | overflow: hidden;
377 | }
378 |
379 | .sticker-btn:focus,
380 | .sticker-btn:hover {
381 | background: var(--color-bg-button-hover);
382 | }
383 |
384 | .sticker-btn:focus > .sticker-item,
385 | .sticker-btn:hover > .sticker-item {
386 | transform: scale(1.5);
387 | }
388 |
389 | .item-select-container {
390 | width: 100%;
391 | background: var(--color-bg-input);
392 | border-radius: var(--radius-small);
393 | }
394 |
395 | .item-select-grid {
396 | display: grid;
397 | grid-gap: 1rem;
398 | grid-template-columns: repeat(7, 1fr);
399 | }
400 |
401 | .item-select-grid--small {
402 | grid-template-columns: repeat(auto-fit, 5.2rem);
403 | }
404 |
405 | .item-container {
406 | position: relative;
407 | display: flex;
408 | justify-content: center;
409 | border: solid 0.2rem transparent;
410 | overflow: hidden;
411 | border-radius: 50%;
412 | }
413 |
414 | button.item-container {
415 | padding: 0;
416 | margin: 0;
417 | width: 4.8rem;
418 | height: 4.8rem;
419 | }
420 |
421 | .item-container:focus {
422 | border: solid 0.2rem var(--color--primary);
423 | }
424 |
425 | .item-container.selected {
426 | border: solid 0.2rem var(--color--primary);
427 | background: var(--color-bg-body);
428 | }
429 |
430 | .item {
431 | width: 100%;
432 | height: 100%;
433 | position: relative;
434 | }
435 |
436 | .item.selected {
437 | opacity: 0.5;
438 | }
439 |
440 | .icon.selected {
441 | width: 2.4rem;
442 | height: 2.4rem;
443 | }
444 |
445 | .item-selected-wrapper {
446 | position: absolute;
447 | width: 100%;
448 | height: 100%;
449 | display: grid;
450 | place-items: center;
451 | align-content: center;
452 | }
453 |
454 | .item--avatar {
455 | width: 4.8rem;
456 | height: 4.8rem;
457 | }
458 |
459 | @media (max-width: 1440px) {
460 | :root {
461 | --chat-width: 400px;
462 | --sticker-columns: repeat(4, 1fr);
463 | }
464 | }
465 |
466 | @media (max-width: 1080px) {
467 | :root {
468 | --chat-width: 100%;
469 | --sticker-columns: repeat(6, 1fr);
470 | }
471 | .content-wrapper {
472 | height: 100%;
473 | flex-direction: column;
474 | top: 0;
475 | }
476 | .col-wrapper {
477 | height: auto;
478 | flex-grow: 1;
479 | }
480 | }
481 |
482 | @keyframes scaleIn {
483 | 0% {
484 | transform: scale3d(0.2, 0.2, 1);
485 | }
486 | 100% {
487 | transform: scale3d(1, 1, 1);
488 | }
489 | }
490 |
--------------------------------------------------------------------------------
/web-ui/src/components/chat/Chat.jsx:
--------------------------------------------------------------------------------
1 | // Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2 | // SPDX-License-Identifier: MIT-0
3 |
4 | import React, { useEffect, useState, useRef, createRef } from 'react';
5 | import Linkify from 'linkify-react';
6 | import axios from 'axios';
7 | import {
8 | ChatRoom,
9 | DeleteMessageRequest,
10 | DisconnectUserRequest,
11 | SendMessageRequest,
12 | } from 'amazon-ivs-chat-messaging';
13 | import { uuidv4 } from '../../helpers';
14 |
15 | import * as config from '../../config';
16 |
17 | // Components
18 | import VideoPlayer from '../videoPlayer/VideoPlayer';
19 | import SignIn from './SignIn';
20 | import StickerPicker from './StickerPicker';
21 | import RaiseHand from './RaiseHand';
22 |
23 | // Styles
24 | import './Chat.css';
25 |
26 | const Chat = () => {
27 | const [showSignIn, setShowSignIn] = useState(true);
28 | const [username, setUsername] = useState('');
29 | const [moderator, setModerator] = useState(false);
30 | const [message, setMessage] = useState('');
31 | const [messages, setMessages] = useState([]);
32 | const [chatRoom, setChatRoom] = useState([]);
33 | const [showRaiseHandPopup, setShowRaiseHandPopup] = useState(false);
34 | const [usernameRaisedHand, setUsernameRaisedHand] = useState(null);
35 | const [handRaised, setHandRaised] = useState(false);
36 | const previousRaiseHandUsername = useRef(null);
37 |
38 | const chatRef = createRef();
39 | const messagesEndRef = createRef();
40 |
41 | // Fetches a chat token
42 | const tokenProvider = async (selectedUsername, isModerator, avatarUrl) => {
43 | const uuid = uuidv4();
44 | const permissions = isModerator
45 | ? ['SEND_MESSAGE', 'DELETE_MESSAGE', 'DISCONNECT_USER']
46 | : ['SEND_MESSAGE'];
47 |
48 | const data = {
49 | arn: config.CHAT_ROOM_ID,
50 | userId: `${selectedUsername}.${uuid}`,
51 | attributes: {
52 | username: `${selectedUsername}`,
53 | avatar: `${avatarUrl.src}`,
54 | },
55 | capabilities: permissions,
56 | };
57 |
58 | var token;
59 | try {
60 | const response = await axios.post(`${config.API_URL}/auth`, data);
61 | token = {
62 | token: response.data.token,
63 | sessionExpirationTime: new Date(response.data.sessionExpirationTime),
64 | tokenExpirationTime: new Date(response.data.tokenExpirationTime),
65 | };
66 | } catch (error) {
67 | console.error('Error:', error);
68 | }
69 |
70 | return token;
71 | };
72 |
73 | const handleSignIn = (selectedUsername, isModerator, avatarUrl) => {
74 | // Set application state
75 | setUsername(selectedUsername);
76 | setModerator(isModerator);
77 |
78 | // Instantiate a chat room
79 | const room = new ChatRoom({
80 | regionOrUrl: config.CHAT_REGION,
81 | tokenProvider: () =>
82 | tokenProvider(selectedUsername, isModerator, avatarUrl),
83 | });
84 | setChatRoom(room);
85 |
86 | // Connect to the chat room
87 | room.connect();
88 | };
89 |
90 | useEffect(() => {
91 | // If chat room listeners are not available, do not continue
92 | if (!chatRoom.addListener) {
93 | return;
94 | }
95 |
96 | // Hide the sign in modal
97 | setShowSignIn(false);
98 |
99 | const unsubscribeOnConnected = chatRoom.addListener('connect', () => {
100 | // Connected to the chat room.
101 | renderConnect();
102 | });
103 |
104 | const unsubscribeOnDisconnected = chatRoom.addListener(
105 | 'disconnect',
106 | (reason) => {
107 | // Disconnected from the chat room.
108 | }
109 | );
110 |
111 | const unsubscribeOnUserDisconnect = chatRoom.addListener(
112 | 'userDisconnect',
113 | (disconnectUserEvent) => {
114 | /* Example event payload:
115 | * {
116 | * id: "AYk6xKitV4On",
117 | * userId": "R1BLTDN84zEO",
118 | * reason": "Spam",
119 | * sendTime": new Date("2022-10-11T12:56:41.113Z"),
120 | * requestId": "b379050a-2324-497b-9604-575cb5a9c5cd",
121 | * attributes": { UserId: "R1BLTDN84zEO", Reason: "Spam" }
122 | * }
123 | */
124 | renderDisconnect(disconnectUserEvent.reason);
125 | }
126 | );
127 |
128 | const unsubscribeOnConnecting = chatRoom.addListener('connecting', () => {
129 | // Connecting to the chat room.
130 | });
131 |
132 | const unsubscribeOnMessageReceived = chatRoom.addListener(
133 | 'message',
134 | (message) => {
135 | // Received a message
136 | const messageType = message.attributes?.message_type || 'MESSAGE';
137 | switch (messageType) {
138 | case 'RAISE_HAND':
139 | handleRaiseHand(message);
140 | break;
141 | case 'STICKER':
142 | handleSticker(message);
143 | break;
144 | default:
145 | handleMessage(message);
146 | break;
147 | }
148 | }
149 | );
150 |
151 | const unsubscribeOnEventReceived = chatRoom.addListener(
152 | 'event',
153 | (event) => {
154 | // Received an event
155 | handleEvent(event);
156 | }
157 | );
158 |
159 | const unsubscribeOnMessageDeleted = chatRoom.addListener(
160 | 'messageDelete',
161 | (deleteEvent) => {
162 | // Received message delete event
163 | const messageIdToDelete = deleteEvent.messageId;
164 | setMessages((prevState) => {
165 | // Remove message that matches the MessageID to delete
166 | const newState = prevState.filter(
167 | (item) => item.messageId !== messageIdToDelete
168 | );
169 | return newState;
170 | });
171 | }
172 | );
173 |
174 | return () => {
175 | unsubscribeOnConnected();
176 | unsubscribeOnDisconnected();
177 | unsubscribeOnUserDisconnect();
178 | unsubscribeOnConnecting();
179 | unsubscribeOnMessageReceived();
180 | unsubscribeOnEventReceived();
181 | unsubscribeOnMessageDeleted();
182 | };
183 | }, [chatRoom]);
184 |
185 | useEffect(() => {
186 | const scrollToBottom = () => {
187 | messagesEndRef.current.scrollIntoView({ behavior: 'smooth' });
188 | };
189 | scrollToBottom();
190 | });
191 |
192 | useEffect(() => {
193 | previousRaiseHandUsername.current = usernameRaisedHand;
194 | }, [usernameRaisedHand]);
195 |
196 | // Handlers
197 | const handleError = (data) => {
198 | const username = '';
199 | const userId = '';
200 | const avatar = '';
201 | const message = `Error ${data.errorCode}: ${data.errorMessage}`;
202 | const messageId = '';
203 | const timestamp = `${Date.now()}`;
204 |
205 | const newMessage = {
206 | type: 'ERROR',
207 | timestamp,
208 | username,
209 | userId,
210 | avatar,
211 | message,
212 | messageId,
213 | };
214 |
215 | setMessages((prevState) => {
216 | return [...prevState, newMessage];
217 | });
218 | };
219 |
220 | const handleMessage = (data) => {
221 | const username = data.sender.attributes.username;
222 | const userId = data.sender.userId;
223 | const avatar = data.sender.attributes.avatar;
224 | const message = data.content;
225 | const messageId = data.id;
226 | const timestamp = data.sendTime;
227 |
228 | const newMessage = {
229 | type: 'MESSAGE',
230 | timestamp,
231 | username,
232 | userId,
233 | avatar,
234 | message,
235 | messageId,
236 | };
237 |
238 | setMessages((prevState) => {
239 | return [...prevState, newMessage];
240 | });
241 | };
242 |
243 | const handleEvent = (event) => {
244 | const eventName = event.eventName;
245 | switch (eventName) {
246 | case 'aws:DELETE_MESSAGE':
247 | // Ignore system delete message events, as they are handled
248 | // by the messageDelete listener on the room.
249 | break;
250 | case 'app:DELETE_BY_USER':
251 | const userIdToDelete = event.attributes.userId;
252 | setMessages((prevState) => {
253 | // Remove message that matches the MessageID to delete
254 | const newState = prevState.filter(
255 | (item) => item.userId !== userIdToDelete
256 | );
257 | return newState;
258 | });
259 | break;
260 | default:
261 | console.info('Unhandled event received:', event);
262 | }
263 | };
264 |
265 | const handleOnClick = () => {
266 | setShowSignIn(true);
267 | };
268 |
269 | const handleChange = (e) => {
270 | setMessage(e.target.value);
271 | };
272 |
273 | const handleKeyDown = (e) => {
274 | if (e.key === 'Enter') {
275 | if (message) {
276 | sendMessage(message);
277 | setMessage('');
278 | }
279 | }
280 | };
281 |
282 | const deleteMessageByUserId = async (userId) => {
283 | // Send a delete event
284 | try {
285 | const response = await sendEvent({
286 | eventName: 'app:DELETE_BY_USER',
287 | eventAttributes: {
288 | userId: userId,
289 | },
290 | });
291 | return response;
292 | } catch (error) {
293 | return error;
294 | }
295 | };
296 |
297 | const handleMessageDelete = async (messageId) => {
298 | const request = new DeleteMessageRequest(messageId, 'Reason for deletion');
299 | try {
300 | await chatRoom.deleteMessage(request);
301 | } catch (error) {
302 | console.error(error);
303 | }
304 | };
305 |
306 | const handleUserKick = async (userId) => {
307 | const request = new DisconnectUserRequest(userId, 'Kicked by moderator');
308 | try {
309 | await chatRoom.disconnectUser(request);
310 | await deleteMessageByUserId(userId);
311 | } catch (error) {
312 | console.error(error);
313 | }
314 | };
315 |
316 | const handleSticker = (data) => {
317 | const username = data.sender.attributes?.username;
318 | const userId = data.sender.userId;
319 | const avatar = data.sender.attributes.avatar;
320 | const message = data.content;
321 | const sticker = data.attributes.sticker_src;
322 | const messageId = data.id;
323 | const timestamp = data.sendTime;
324 |
325 | const newMessage = {
326 | type: 'STICKER',
327 | timestamp,
328 | username,
329 | userId,
330 | avatar,
331 | message,
332 | messageId,
333 | sticker,
334 | };
335 |
336 | setMessages((prevState) => {
337 | return [...prevState, newMessage];
338 | });
339 | };
340 |
341 | const handleRaiseHand = async (data) => {
342 | const username = data.sender.attributes?.username;
343 | setUsernameRaisedHand(username);
344 |
345 | if (previousRaiseHandUsername.current !== username) {
346 | setShowRaiseHandPopup(true);
347 | } else {
348 | setShowRaiseHandPopup((showRaiseHandPopup) => !showRaiseHandPopup);
349 | }
350 | };
351 |
352 | const handleStickerSend = async (sticker) => {
353 | const content = `Sticker: ${sticker.name}`;
354 | const attributes = {
355 | message_type: 'STICKER',
356 | sticker_src: `${sticker.src}`,
357 | };
358 | const request = new SendMessageRequest(content, attributes);
359 | try {
360 | await chatRoom.sendMessage(request);
361 | } catch (error) {
362 | handleError(error);
363 | }
364 | };
365 |
366 | const handleRaiseHandSend = async () => {
367 | const attributes = {
368 | message_type: 'RAISE_HAND',
369 | };
370 |
371 | const request = new SendMessageRequest(`[raise hand event]`, attributes);
372 | try {
373 | await chatRoom.sendMessage(request);
374 | setHandRaised((prevState) => !prevState);
375 | } catch (error) {
376 | handleError(error);
377 | }
378 | };
379 |
380 | const sendMessage = async (message) => {
381 | const content = `${message.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}`;
382 | const request = new SendMessageRequest(content);
383 | try {
384 | await chatRoom.sendMessage(request);
385 | } catch (error) {
386 | handleError(error);
387 | }
388 | };
389 |
390 | const sendEvent = async (data) => {
391 | const formattedData = {
392 | arn: config.CHAT_ROOM_ID,
393 | eventName: `${data.eventName}`,
394 | eventAttributes: data.eventAttributes,
395 | };
396 |
397 | try {
398 | const response = await axios.post(
399 | `${config.API_URL}/event`,
400 | formattedData
401 | );
402 | console.info('SendEvent Success:', response.data);
403 | return response;
404 | } catch (error) {
405 | console.error('SendEvent Error:', error);
406 | return error;
407 | }
408 | };
409 |
410 | // Renderers
411 | const renderErrorMessage = (errorMessage) => {
412 | return (
413 |