// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. // SPDX-License-Identifier: MIT-0 import React, { useEffect, useState, useRef, createRef } from 'react'; import Linkify from 'linkify-react'; import axios from 'axios'; import { ChatRoom, DeleteMessageRequest, DisconnectUserRequest, SendMessageRequest, } from 'amazon-ivs-chat-messaging'; import { uuidv4 } from '../../helpers'; import * as config from '../../config'; // Components import VideoPlayer from '../videoPlayer/VideoPlayer'; import SignIn from './SignIn'; import StickerPicker from './StickerPicker'; import RaiseHand from './RaiseHand'; // Styles import './Chat.css'; const Chat = () => { const [showSignIn, setShowSignIn] = useState(true); const [username, setUsername] = useState(''); const [moderator, setModerator] = useState(false); const [message, setMessage] = useState(''); const [messages, setMessages] = useState([]); const [chatRoom, setChatRoom] = useState([]); const [showRaiseHandPopup, setShowRaiseHandPopup] = useState(false); const [usernameRaisedHand, setUsernameRaisedHand] = useState(null); const [handRaised, setHandRaised] = useState(false); const previousRaiseHandUsername = useRef(null); const chatRef = createRef(); const messagesEndRef = createRef(); // Fetches a chat token const tokenProvider = async (selectedUsername, isModerator, avatarUrl) => { const uuid = uuidv4(); const permissions = isModerator ? ['SEND_MESSAGE', 'DELETE_MESSAGE', 'DISCONNECT_USER'] : ['SEND_MESSAGE']; const data = { arn: config.CHAT_ROOM_ID, userId: `${selectedUsername}.${uuid}`, attributes: { username: `${selectedUsername}`, avatar: `${avatarUrl.src}`, }, capabilities: permissions, }; var token; try { const response = await axios.post(`${config.API_URL}/auth`, data); token = { token: response.data.token, sessionExpirationTime: new Date(response.data.sessionExpirationTime), tokenExpirationTime: new Date(response.data.tokenExpirationTime), }; } catch (error) { console.error('Error:', error); } return token; }; const handleSignIn = (selectedUsername, isModerator, avatarUrl) => { // Set application state setUsername(selectedUsername); setModerator(isModerator); // Instantiate a chat room const room = new ChatRoom({ regionOrUrl: config.CHAT_REGION, tokenProvider: () => tokenProvider(selectedUsername, isModerator, avatarUrl), }); setChatRoom(room); // Connect to the chat room room.connect(); }; useEffect(() => { // If chat room listeners are not available, do not continue if (!chatRoom.addListener) { return; } // Hide the sign in modal setShowSignIn(false); const unsubscribeOnConnected = chatRoom.addListener('connect', () => { // Connected to the chat room. renderConnect(); }); const unsubscribeOnDisconnected = chatRoom.addListener( 'disconnect', (reason) => { // Disconnected from the chat room. } ); const unsubscribeOnUserDisconnect = chatRoom.addListener( 'userDisconnect', (disconnectUserEvent) => { /* Example event payload: * { * id: "AYk6xKitV4On", * userId": "R1BLTDN84zEO", * reason": "Spam", * sendTime": new Date("2022-10-11T12:56:41.113Z"), * requestId": "b379050a-2324-497b-9604-575cb5a9c5cd", * attributes": { UserId: "R1BLTDN84zEO", Reason: "Spam" } * } */ renderDisconnect(disconnectUserEvent.reason); } ); const unsubscribeOnConnecting = chatRoom.addListener('connecting', () => { // Connecting to the chat room. }); const unsubscribeOnMessageReceived = chatRoom.addListener( 'message', (message) => { // Received a message const messageType = message.attributes?.message_type || 'MESSAGE'; switch (messageType) { case 'RAISE_HAND': handleRaiseHand(message); break; case 'STICKER': handleSticker(message); break; default: handleMessage(message); break; } } ); const unsubscribeOnEventReceived = chatRoom.addListener( 'event', (event) => { // Received an event handleEvent(event); } ); const unsubscribeOnMessageDeleted = chatRoom.addListener( 'messageDelete', (deleteEvent) => { // Received message delete event const messageIdToDelete = deleteEvent.messageId; setMessages((prevState) => { // Remove message that matches the MessageID to delete const newState = prevState.filter( (item) => item.messageId !== messageIdToDelete ); return newState; }); } ); return () => { unsubscribeOnConnected(); unsubscribeOnDisconnected(); unsubscribeOnUserDisconnect(); unsubscribeOnConnecting(); unsubscribeOnMessageReceived(); unsubscribeOnEventReceived(); unsubscribeOnMessageDeleted(); }; }, [chatRoom]); useEffect(() => { const scrollToBottom = () => { messagesEndRef.current.scrollIntoView({ behavior: 'smooth' }); }; scrollToBottom(); }); useEffect(() => { previousRaiseHandUsername.current = usernameRaisedHand; }, [usernameRaisedHand]); // Handlers const handleError = (data) => { const username = ''; const userId = ''; const avatar = ''; const message = `Error ${data.errorCode}: ${data.errorMessage}`; const messageId = ''; const timestamp = `${Date.now()}`; const newMessage = { type: 'ERROR', timestamp, username, userId, avatar, message, messageId, }; setMessages((prevState) => { return [...prevState, newMessage]; }); }; const handleMessage = (data) => { const username = data.sender.attributes.username; const userId = data.sender.userId; const avatar = data.sender.attributes.avatar; const message = data.content; const messageId = data.id; const timestamp = data.sendTime; const newMessage = { type: 'MESSAGE', timestamp, username, userId, avatar, message, messageId, }; setMessages((prevState) => { return [...prevState, newMessage]; }); }; const handleEvent = (event) => { const eventName = event.eventName; switch (eventName) { case 'aws:DELETE_MESSAGE': // Ignore system delete message events, as they are handled // by the messageDelete listener on the room. break; case 'app:DELETE_BY_USER': const userIdToDelete = event.attributes.userId; setMessages((prevState) => { // Remove message that matches the MessageID to delete const newState = prevState.filter( (item) => item.userId !== userIdToDelete ); return newState; }); break; default: console.info('Unhandled event received:', event); } }; const handleOnClick = () => { setShowSignIn(true); }; const handleChange = (e) => { setMessage(e.target.value); }; const handleKeyDown = (e) => { if (e.key === 'Enter') { if (message) { sendMessage(message); setMessage(''); } } }; const deleteMessageByUserId = async (userId) => { // Send a delete event try { const response = await sendEvent({ eventName: 'app:DELETE_BY_USER', eventAttributes: { userId: userId, }, }); return response; } catch (error) { return error; } }; const handleMessageDelete = async (messageId) => { const request = new DeleteMessageRequest(messageId, 'Reason for deletion'); try { await chatRoom.deleteMessage(request); } catch (error) { console.error(error); } }; const handleUserKick = async (userId) => { const request = new DisconnectUserRequest(userId, 'Kicked by moderator'); try { await chatRoom.disconnectUser(request); await deleteMessageByUserId(userId); } catch (error) { console.error(error); } }; const handleSticker = (data) => { const username = data.sender.attributes?.username; const userId = data.sender.userId; const avatar = data.sender.attributes.avatar; const message = data.content; const sticker = data.attributes.sticker_src; const messageId = data.id; const timestamp = data.sendTime; const newMessage = { type: 'STICKER', timestamp, username, userId, avatar, message, messageId, sticker, }; setMessages((prevState) => { return [...prevState, newMessage]; }); }; const handleRaiseHand = async (data) => { const username = data.sender.attributes?.username; setUsernameRaisedHand(username); if (previousRaiseHandUsername.current !== username) { setShowRaiseHandPopup(true); } else { setShowRaiseHandPopup((showRaiseHandPopup) => !showRaiseHandPopup); } }; const handleStickerSend = async (sticker) => { const content = `Sticker: ${sticker.name}`; const attributes = { message_type: 'STICKER', sticker_src: `${sticker.src}`, }; const request = new SendMessageRequest(content, attributes); try { await chatRoom.sendMessage(request); } catch (error) { handleError(error); } }; const handleRaiseHandSend = async () => { const attributes = { message_type: 'RAISE_HAND', }; const request = new SendMessageRequest(`[raise hand event]`, attributes); try { await chatRoom.sendMessage(request); setHandRaised((prevState) => !prevState); } catch (error) { handleError(error); } }; const sendMessage = async (message) => { const content = `${message.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}`; const request = new SendMessageRequest(content); try { await chatRoom.sendMessage(request); } catch (error) { handleError(error); } }; const sendEvent = async (data) => { const formattedData = { arn: config.CHAT_ROOM_ID, eventName: `${data.eventName}`, eventAttributes: data.eventAttributes, }; try { const response = await axios.post( `${config.API_URL}/event`, formattedData ); console.info('SendEvent Success:', response.data); return response; } catch (error) { console.error('SendEvent Error:', error); return error; } }; // Renderers const renderErrorMessage = (errorMessage) => { return (

{errorMessage.message}

); }; const renderSuccessMessage = (successMessage) => { return (

{successMessage.message}

); }; const renderChatLineActions = (message) => { return ( <> ); }; const renderStickerMessage = (message) => (
{`Avatar

{message.username}

{`sticker`}
{moderator ? renderChatLineActions(message) : ''}
); const renderMessage = (message) => { return (
{`Avatar

{message.username} {message.message}

{moderator ? renderChatLineActions(message) : ''}
); }; const renderMessages = () => { return messages.map((message) => { switch (message.type) { case 'ERROR': const errorMessage = renderErrorMessage(message); return errorMessage; case 'SUCCESS': const successMessage = renderSuccessMessage(message); return successMessage; case 'STICKER': const stickerMessage = renderStickerMessage(message); return stickerMessage; case 'MESSAGE': const textMessage = renderMessage(message); return textMessage; default: console.info('Received unsupported message:', message); return <>; } }); }; const renderDisconnect = (reason) => { const error = { type: 'ERROR', timestamp: `${Date.now()}`, username: '', userId: '', avatar: '', message: `Connection closed. Reason: ${reason}`, }; setMessages((prevState) => { return [...prevState, error]; }); }; const renderConnect = () => { const status = { type: 'SUCCESS', timestamp: `${Date.now()}`, username: '', userId: '', avatar: '', message: `Connected to the chat room.`, }; setMessages((prevState) => { return [...prevState, status]; }); }; const isChatConnected = () => { const chatState = chatRoom.state; return chatState === 'connected'; }; return ( <>

Amazon IVS Chat Web Demo

{renderMessages()}
{isChatConnected() && ( )} {isChatConnected() && ( )} {!username && (
)}
{showSignIn && }
); }; export default Chat;