Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/audience chat #147

Merged
merged 44 commits into from
Jul 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
9926682
add conversation logic to join-stream-as-viewer endpoint
olipyskoty May 6, 2022
f97129b
Merge pull request #118 from twilio/VIDEO-9744-backend-conversations-…
olipyskoty May 6, 2022
2872c28
add room_sid to backend
olipyskoty May 6, 2022
b52c2a0
add chatfeature to audience
olipyskoty May 6, 2022
9a9069c
get rid of unnecesary appState updates
olipyskoty May 10, 2022
1ab4a81
remve unused variables and keywords from chatProvider
olipyskoty May 10, 2022
54ee9bb
formatting fix
olipyskoty May 10, 2022
fc01ded
Merge branch 'main' into feature/audience-chat
olipyskoty May 12, 2022
4ccf84d
add feature flag to backend
olipyskoty May 13, 2022
0776797
support feature flag on frontend
olipyskoty May 13, 2022
bfec0e2
fix linter error
olipyskoty May 13, 2022
bdc678e
destructure DISABLE_CHAT from serverless context
olipyskoty May 13, 2022
fa982aa
remove console.log
olipyskoty May 13, 2022
44ac27f
dont add chatGrant to token if chat is disabled
olipyskoty May 16, 2022
8eae506
make hasSpeakerInvite consistent with other prejoinscreen cases
olipyskoty May 18, 2022
4d24b31
Merge pull request #122 from twilio/VIDEO-9792-conversations-feature-…
olipyskoty May 18, 2022
4c3ccf6
Merge branch 'feature/audience-chat' into VIDEO-9638-web-implement-chat
olipyskoty Jun 21, 2022
4a777de
add conversation feature flag check for viewers
olipyskoty Jun 22, 2022
65bc53c
hide chat button for viewers if disabled
olipyskoty Jun 22, 2022
daca12b
Merge pull request #120 from twilio/VIDEO-9638-web-implement-chat
olipyskoty Jun 22, 2022
dcc60c0
fix bug to add chat button for viewers
olipyskoty Jun 27, 2022
25c7e72
Make chat window open by default for desktop
olipyskoty Jun 27, 2022
7a3b017
Add mobile styling for PlayerMenuBar
olipyskoty Jun 27, 2022
edcc455
Add MobileBottomMenuBar component
olipyskoty Jun 27, 2022
36b1bec
add optional prop to exclude labels for menubar buttons
olipyskoty Jun 27, 2022
e6c93a9
remove mobileBottomMenuBar component
olipyskoty Jun 28, 2022
ce6309e
remove use of isMobile variable and replace with media queries
olipyskoty Jun 28, 2022
da3e7c5
remove ability to send files via chat
olipyskoty Jun 28, 2022
1aa9e71
Update mobile menu bar buttons to be consistent with iOS
olipyskoty Jun 29, 2022
8ac0b5b
update PlayerMenuBar buttons to be consistent with iOS
olipyskoty Jun 29, 2022
5f141d3
add <Hidden /> to chat and participant buttons
olipyskoty Jul 1, 2022
7e89e57
update margins to center buttons in menubar
olipyskoty Jul 1, 2022
27cd89d
Merge pull request #136 from twilio/VIDEO-10105-finalize-audience-chat
olipyskoty Jul 1, 2022
6171742
update readme with info about chat feature flag
olipyskoty Jul 7, 2022
8459709
format variable to code
olipyskoty Jul 7, 2022
96392f4
formatting
olipyskoty Jul 7, 2022
321f076
Merge pull request #143 from twilio/readme-conversation-flag-info
olipyskoty Jul 7, 2022
6443d49
Merge branch 'main' into feature/audience-chat
olipyskoty Jul 7, 2022
53eb80b
ensure chat window is closed when feature flag is enabled
olipyskoty Jul 7, 2022
7dcde87
Merge pull request #144 from twilio/fix-chat-window-open-bug
olipyskoty Jul 7, 2022
e0905f8
fix chat window open by default bug
olipyskoty Jul 11, 2022
cfa2576
prep for release
olipyskoty Jul 11, 2022
884524e
fix wording of bug fix 116
olipyskoty Jul 11, 2022
f793002
Merge pull request #146 from twilio/prep-release
olipyskoty Jul 11, 2022
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -11,4 +11,7 @@ AUTH_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
# Uncomment to run in stage environment.
# Must also use correct credentials for stage.
# REACT_APP_TWILIO_ENVIRONMENT=stage
# TWILIO_REGION=stage
# TWILIO_REGION=stage

# Un-comment the following line to disable the Twilio Conversations functionality in the app.
# DISABLE_CHAT=true
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
# 1.1.0 (July 11, 2022)

### New Feature

- This release adds a chat feature for the host, speakers, and viewers. This feature allows all users to send and receive textual messages to each other while connected to a stream. This feature is powered by the [Twilio Conversations API](https://www.twilio.com/conversations-api) and is optional. See the [README.md](https://github.com/twilio/twilio-live-interactive-video/blob/feature/audience-chat/README.md#set-your-account-sid-and-auth-token) for more information on how to opt out.

### Bug Fixes

- Fixes an issue where the host could not create more than one stream. [#116](https://github.com/twilio/twilio-live-interactive-video/pull/116)

# 1.0.0 (February 28, 2022)

This is the initial release of the Twilio Live Interactive Video iOS and web Apps. This project demonstrates an interactive live video streaming app that uses [Twilio Live](https://www.twilio.com/docs/live), [Twilio Video](https://www.twilio.com/docs/video) and [Twilio Sync](https://www.twilio.com/docs/sync).
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@ Copy the `.env.example` file to `.env` and perform the following one-time steps

Update the ACCOUNT_SID and AUTH_TOKEN `.env` entries with the Account SID and Auth Token found on the [Twilio Console home page](https://twilio.com/console).

**NOTE**: the use of Twilio Conversations is optional. If you wish to opt out, set the `DISABLE_CHAT` environment variable to `true`.

#### Install Dependencies

Once you have setup all your environment variables, run `npm install` to install all dependencies from NPM.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import React from 'react';
import clsx from 'clsx';
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles';

import { Button } from '@material-ui/core';
import { Button, Hidden } from '@material-ui/core';
import ExitToAppIcon from '@material-ui/icons/ExitToApp';

import { useAppState } from '../../../state';
import { deleteStream } from '../../../state/api/api';
Expand Down Expand Up @@ -40,7 +41,10 @@ export default function EndCallButton(props: { className?: string }) {

return (
<Button onClick={disconnect} className={clsx(classes.button, props.className)} data-cy-disconnect>
End Event
<Hidden smDown>End Event</Hidden>
<Hidden mdUp>
<ExitToAppIcon />
</Hidden>
</Button>
);
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
import React, { useState, useRef } from 'react';
import { Button, Menu as MenuContainer, MenuItem, Typography } from '@material-ui/core';
import clsx from 'clsx';
import { Button, Menu as MenuContainer, MenuItem, Typography, Hidden } from '@material-ui/core';
import { createStyles, makeStyles, Theme } from '@material-ui/core/styles';
import { joinStreamAsViewer, connectViewerToPlayer } from '../../../state/api/api';
import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
import ExitToAppIcon from '@material-ui/icons/ExitToApp';
import { useAppState } from '../../../state';
import useChatContext from '../../../hooks/useChatContext/useChatContext';
import usePlayerContext from '../../../hooks/usePlayerContext/usePlayerContext';
Expand All @@ -21,7 +23,7 @@ const useStyles = makeStyles((theme: Theme) =>
})
);

export default function LeaveEventButton(props: { buttonClassName?: string }) {
export default function LeaveEventButton() {
const classes = useStyles();
const [menuOpen, setMenuOpen] = useState(false);
const { room } = useVideoContext();
Expand Down Expand Up @@ -53,9 +55,17 @@ export default function LeaveEventButton(props: { buttonClassName?: string }) {

return (
<>
<Button onClick={() => setMenuOpen(isOpen => !isOpen)} ref={anchorRef} className={classes.button}>
Leave Event
<ExpandMoreIcon />
<Button
onClick={() => setMenuOpen(isOpen => !isOpen)}
ref={anchorRef}
className={clsx(classes.button, 'MuiButton-mobileBackground')}
>
<Hidden smDown>
Leave Event <ExpandMoreIcon />
</Hidden>
<Hidden mdUp>
<ExitToAppIcon />
</Hidden>
</Button>
<MenuContainer
open={menuOpen}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export default function ToggleAudioButton(props: { disabled?: boolean; className
startIcon={isAudioEnabled ? <MicIcon /> : <MicOffIcon />}
data-cy-audio-toggle
>
{!hasAudioTrack ? 'No Audio' : isAudioEnabled ? 'Mute' : 'Unmute'}
<span className="MuiButton-textLabel">{!hasAudioTrack ? 'No Audio' : isAudioEnabled ? 'Mute' : 'Unmute'}</span>
</Button>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';
import Button from '@material-ui/core/Button';
import ChatIcon from '../../../icons/ChatIcon';
import clsx from 'clsx';
import { makeStyles } from '@material-ui/core';
import { makeStyles, Hidden } from '@material-ui/core';
import useChatContext from '../../../hooks/useChatContext/useChatContext';
import useVideoContext from '../../../hooks/useVideoContext/useVideoContext';
import { useAppState } from '../../../state';
Expand Down Expand Up @@ -78,7 +78,6 @@ export default function ToggleChatButton() {
setTimeout(() => setShouldAnimate(false), ANIMATION_DURATION);
}
}, [shouldAnimate]);

useEffect(() => {
if (conversation && !isChatWindowOpen) {
const handleNewMessage = () => setShouldAnimate(true);
Expand All @@ -94,6 +93,7 @@ export default function ToggleChatButton() {
data-cy-chat-button
onClick={toggleChatWindow}
disabled={!conversation}
className="MuiButton-mobileBackground"
startIcon={
<div className={classes.iconContainer}>
<ChatIcon />
Expand All @@ -102,7 +102,7 @@ export default function ToggleChatButton() {
</div>
}
>
Chat
<Hidden smDown>Chat</Hidden>
</Button>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import React, { useEffect, useState } from 'react';

import Button from '@material-ui/core/Button';
import clsx from 'clsx';
import { makeStyles } from '@material-ui/core';
import { makeStyles, Hidden } from '@material-ui/core';
import ParticipantIcon from '../../../icons/ParticipantIcon';
import { useAppState } from '../../../state';
import useVideoContext from '../../../hooks/useVideoContext/useVideoContext';
Expand Down Expand Up @@ -96,6 +96,7 @@ export default function ToggleParticipantWindowButton() {

return (
<Button
className="MuiButton-mobileBackground"
onClick={toggleParticipantWindow}
startIcon={
<div className={classes.iconContainer}>
Expand All @@ -109,7 +110,7 @@ export default function ToggleParticipantWindowButton() {
</div>
}
>
Participants
<Hidden smDown>Participants</Hidden>
</Button>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ export default function ToggleVideoButton(props: { disabled?: boolean; className
disabled={!hasVideoInputDevices || props.disabled}
startIcon={isVideoEnabled ? <VideoOnIcon /> : <VideoOffIcon />}
>
{!hasVideoInputDevices ? 'No Video' : isVideoEnabled ? 'Stop Video' : 'Start Video'}
<span className="MuiButton-textLabel">
{!hasVideoInputDevices ? 'No Video' : isVideoEnabled ? 'Stop Video' : 'Start Video'}
</span>
</Button>
);
}
15 changes: 9 additions & 6 deletions apps/web/src/components/ChatProvider/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import useVideoContext from '../../hooks/useVideoContext/useVideoContext';
type ChatContextType = {
isChatWindowOpen: boolean;
setIsChatWindowOpen: (isChatWindowOpen: boolean) => void;
connect: (token: string) => void;
connect: (token: string, roomSid: string) => void;
disconnect: () => void;
hasUnreadMessages: boolean;
messages: Message[];
Expand All @@ -17,18 +17,21 @@ type ChatContextType = {
export const ChatContext = createContext<ChatContextType>(null!);

export const ChatProvider: React.FC = ({ children }) => {
const { room, onError } = useVideoContext();
const { onError } = useVideoContext();
const isChatWindowOpenRef = useRef(false);
const [isChatWindowOpen, setIsChatWindowOpen] = useState(false);
const [conversation, setConversation] = useState<Conversation | null>(null);
const [messages, setMessages] = useState<Message[]>([]);
const [hasUnreadMessages, setHasUnreadMessages] = useState(false);
const [videoRoomSid, setVideoRoomSid] = useState('');
const [chatClient, setChatClient] = useState<Client>();

const connect = useCallback(
(token: string) => {
(token: string, roomSid: string) => {
if (!chatClient) {
setVideoRoomSid(roomSid);
let conversationOptions;

if (process.env.REACT_APP_TWILIO_ENVIRONMENT) {
conversationOptions = { region: `${process.env.REACT_APP_TWILIO_ENVIRONMENT}-us1` };
}
Expand Down Expand Up @@ -76,9 +79,9 @@ export const ChatProvider: React.FC = ({ children }) => {
}, [isChatWindowOpen]);

useEffect(() => {
if (room && chatClient) {
if (videoRoomSid && chatClient) {
chatClient
.getConversationByUniqueName(room.sid)
.getConversationByUniqueName(videoRoomSid)
.then(newConversation => {
//@ts-ignore
window.chatConversation = newConversation;
Expand All @@ -89,7 +92,7 @@ export const ChatProvider: React.FC = ({ children }) => {
onError(new Error('There was a problem getting the Conversation associated with this room.'));
});
}
}, [room, chatClient, onError]);
}, [chatClient, onError, videoRoomSid]);

return (
<ChatContext.Provider
Expand Down
60 changes: 1 addition & 59 deletions apps/web/src/components/ChatWindow/ChatInput/ChatInput.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import React, { useEffect, useRef, useState } from 'react';
import { Button, CircularProgress, Grid, makeStyles } from '@material-ui/core';
import { Button, Grid, makeStyles } from '@material-ui/core';
import clsx from 'clsx';
import { Conversation } from '@twilio/conversations/lib/conversation';
import FileAttachmentIcon from '../../../icons/FileAttachmentIcon';
import { isMobile } from '../../../utils';
import SendMessageIcon from '../../../icons/SendMessageIcon';
import Snackbar from '../../Snackbar/Snackbar';
Expand Down Expand Up @@ -36,17 +35,6 @@ const useStyles = makeStyles(theme => ({
margin: '1em 0 0 1em',
display: 'flex',
},
fileButtonContainer: {
position: 'relative',
marginRight: '1em',
},
fileButtonLoadingSpinner: {
position: 'absolute',
top: '50%',
left: '50%',
marginTop: -12,
marginLeft: -12,
},
textAreaContainer: {
display: 'flex',
marginTop: '0.4em',
Expand All @@ -64,17 +52,12 @@ interface ChatInputProps {
isChatWindowOpen: boolean;
}

const ALLOWED_FILE_TYPES =
'audio/*, image/*, text/*, video/*, application/msword, application/vnd.openxmlformats-officedocument.wordprocessingml.document .xslx, .ppt, .pdf, .key, .svg, .csv';

export default function ChatInput({ conversation, isChatWindowOpen }: ChatInputProps) {
const classes = useStyles();
const [messageBody, setMessageBody] = useState('');
const [isSendingFile, setIsSendingFile] = useState(false);
const [fileSendError, setFileSendError] = useState<string | null>(null);
const isValidMessage = /\S/.test(messageBody);
const textInputRef = useRef<HTMLTextAreaElement>(null);
const fileInputRef = useRef<HTMLInputElement>(null);
const [isTextareaFocused, setIsTextareaFocused] = useState(false);

useEffect(() => {
Expand Down Expand Up @@ -104,29 +87,6 @@ export default function ChatInput({ conversation, isChatWindowOpen }: ChatInputP
}
};

const handleSendFile = (event: React.ChangeEvent<HTMLInputElement>) => {
const file = event.target.files?.[0];
if (file) {
var formData = new FormData();
formData.append('userfile', file);
setIsSendingFile(true);
setFileSendError(null);
conversation
.sendMessage(formData)
.catch(e => {
if (e.code === 413) {
setFileSendError('File size is too large. Maximum file size is 150MB.');
} else {
setFileSendError('There was a problem uploading the file. Please try again.');
}
console.log('Problem sending file: ', e);
})
.finally(() => {
setIsSendingFile(false);
});
}
};

return (
<div className={classes.chatInputContainer}>
<Snackbar
Expand Down Expand Up @@ -159,25 +119,7 @@ export default function ChatInput({ conversation, isChatWindowOpen }: ChatInputP
</div>

<Grid container alignItems="flex-end" justifyContent="flex-end" wrap="nowrap">
{/* Since the file input element is invisible, we can hardcode an empty string as its value.
This allows users to upload the same file multiple times. */}
<input
ref={fileInputRef}
type="file"
style={{ display: 'none' }}
onChange={handleSendFile}
value={''}
accept={ALLOWED_FILE_TYPES}
/>
<div className={classes.buttonContainer}>
<div className={classes.fileButtonContainer}>
<Button className={classes.button} onClick={() => fileInputRef.current?.click()} disabled={isSendingFile}>
<FileAttachmentIcon />
</Button>

{isSendingFile && <CircularProgress size={24} className={classes.fileButtonLoadingSpinner} />}
</div>

<Button
className={classes.button}
onClick={() => handleSendMessage(messageBody)}
Expand Down
2 changes: 0 additions & 2 deletions apps/web/src/components/ChatWindow/ChatWindow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,9 @@ const useStyles = makeStyles((theme: Theme) =>
},
})
);

// In this component, we are toggling the visibility of the ChatWindow with CSS instead of
// conditionally rendering the component in the DOM. This is done so that the ChatWindow is
// not unmounted while a file upload is in progress.

export default function ChatWindow() {
const classes = useStyles();
const { isChatWindowOpen, messages, conversation } = useChatContext();
Expand Down
10 changes: 4 additions & 6 deletions apps/web/src/components/ChatWindow/MessageList/MessageList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import MessageListScrollContainer from './MessageListScrollContainer/MessageList
import TextMessage from './TextMessage/TextMessage';
import useVideoContext from '../../../hooks/useVideoContext/useVideoContext';
import MediaMessage from './MediaMessage/MediaMessage';

import { useAppState } from '../../../state';
interface MessageListProps {
messages: Message[];
}
Expand All @@ -15,19 +15,17 @@ const getFormattedTime = (message?: Message) =>

export default function MessageList({ messages }: MessageListProps) {
const { room } = useVideoContext();
const localParticipant = room!.localParticipant;
const { appState } = useAppState();
const localParticipant = room ? room.localParticipant.identity : appState.participantName;

return (
<MessageListScrollContainer messages={messages}>
{messages.map((message, idx) => {
const time = getFormattedTime(message)!;
const previousTime = getFormattedTime(messages[idx - 1]);

// Display the MessageInfo component when the author or formatted timestamp differs from the previous message
const shouldDisplayMessageInfo = time !== previousTime || message.author !== messages[idx - 1]?.author;

const isLocalParticipant = localParticipant.identity === message.author;

const isLocalParticipant = localParticipant === message.author;
return (
<React.Fragment key={message.sid}>
{shouldDisplayMessageInfo && (
Expand Down
Loading