removed requirement for predefined room prop
This commit is contained in:
parent
34e7a1d26d
commit
4048c064b0
|
|
@ -117,7 +117,7 @@ export default function Index({
|
||||||
|
|
||||||
Index.propTypes = {
|
Index.propTypes = {
|
||||||
isConfigured: PropTypes.bool.isRequired,
|
isConfigured: PropTypes.bool.isRequired,
|
||||||
predefinedRoom: PropTypes.bool.isRequired,
|
predefinedRoom: PropTypes.bool,
|
||||||
domain: PropTypes.string,
|
domain: PropTypes.string,
|
||||||
asides: PropTypes.arrayOf(PropTypes.func),
|
asides: PropTypes.arrayOf(PropTypes.func),
|
||||||
customTrayComponent: PropTypes.node,
|
customTrayComponent: PropTypes.node,
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"presets": ["next/babel"],
|
||||||
|
"plugins": ["inline-react-svg"]
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,41 @@
|
||||||
|
# Text Chat
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
### Live example
|
||||||
|
|
||||||
|
**[See it in action here ➡️](https://dailyjs-text-chat.vercel.app)**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What does this demo do?
|
||||||
|
|
||||||
|
- Use [sendAppMessage](https://docs.daily.co/reference#%EF%B8%8F-sendappmessage) to send messages
|
||||||
|
- Listen for incoming messages using the call object `app-message` event
|
||||||
|
- Extend the basic call demo with a chat provider and aside
|
||||||
|
- Show a notification bubble on chat tray button when a new message is received
|
||||||
|
- Demonstrate how to play a sound whenever a message is received
|
||||||
|
|
||||||
|
Please note: this demo is not currently mobile optimised
|
||||||
|
|
||||||
|
### Getting started
|
||||||
|
|
||||||
|
```
|
||||||
|
# set both DAILY_API_KEY and DAILY_DOMAIN
|
||||||
|
mv env.example .env.local
|
||||||
|
|
||||||
|
yarn
|
||||||
|
yarn workspace @dailyjs/text-chat dev
|
||||||
|
```
|
||||||
|
|
||||||
|
## How does this example work?
|
||||||
|
|
||||||
|
In this example we extend the [basic call demo](../basic-call) with the ability to send chat messages.
|
||||||
|
|
||||||
|
We pass a custom tray object, a custom app object (wrapping the original in a new `ChatProvider`) as well as add our `ChatAside` panel. We also symlink both the `public` and `pages/api` folders from the basic call.
|
||||||
|
|
||||||
|
In a real world use case you would likely want to implement serverside logic so that participants joining a call can retrieve previously sent messages. This round trip could be done inside of the Chat context.
|
||||||
|
|
||||||
|
## Deploy your own on Vercel
|
||||||
|
|
||||||
|
[](https://vercel.com/new/daily-co/clone-flow?repository-url=https%3A%2F%2Fgithub.com%2Fdaily-demos%2Fexamples.git&env=DAILY_DOMAIN%2CDAILY_API_KEY&envDescription=Your%20Daily%20domain%20and%20API%20key%20can%20be%20found%20on%20your%20account%20dashboard&envLink=https%3A%2F%2Fdashboard.daily.co&project-name=daily-examples&repo-name=daily-examples)
|
||||||
|
|
@ -0,0 +1,13 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import App from '@dailyjs/basic-call/components/App';
|
||||||
|
import { ChatProvider } from '../../contexts/ChatProvider';
|
||||||
|
|
||||||
|
// Extend our basic call app component with the chat context
|
||||||
|
export const AppWithChat = () => (
|
||||||
|
<ChatProvider>
|
||||||
|
<App />
|
||||||
|
</ChatProvider>
|
||||||
|
);
|
||||||
|
|
||||||
|
export default AppWithChat;
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { AppWithChat as default } from './App';
|
||||||
|
|
@ -0,0 +1,132 @@
|
||||||
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
|
import Aside from '@dailyjs/shared/components/Aside';
|
||||||
|
import { Button } from '@dailyjs/shared/components/Button';
|
||||||
|
import { TextInput } from '@dailyjs/shared/components/Input';
|
||||||
|
import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider';
|
||||||
|
import { useChat } from '../../contexts/ChatProvider';
|
||||||
|
import { useMessageSound } from '../../hooks/useMessageSound';
|
||||||
|
|
||||||
|
export const CHAT_ASIDE = 'chat';
|
||||||
|
|
||||||
|
export const ChatAside = () => {
|
||||||
|
const { showAside, setShowAside } = useUIState();
|
||||||
|
const { sendMessage, chatHistory, hasNewMessages, setHasNewMessages } =
|
||||||
|
useChat();
|
||||||
|
const [newMessage, setNewMessage] = useState('');
|
||||||
|
const playMessageSound = useMessageSound();
|
||||||
|
|
||||||
|
const chatWindowRef = useRef();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
// Clear out any new message notifications if we're showing the chat screen
|
||||||
|
if (showAside === CHAT_ASIDE) {
|
||||||
|
setHasNewMessages(false);
|
||||||
|
}
|
||||||
|
}, [showAside, chatHistory.length, setHasNewMessages]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (hasNewMessages && showAside !== CHAT_ASIDE) {
|
||||||
|
playMessageSound();
|
||||||
|
}
|
||||||
|
}, [playMessageSound, showAside, hasNewMessages]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (chatWindowRef.current) {
|
||||||
|
chatWindowRef.current.scrollTop = chatWindowRef.current.scrollHeight;
|
||||||
|
}
|
||||||
|
}, [chatHistory?.length]);
|
||||||
|
|
||||||
|
if (!showAside || showAside !== CHAT_ASIDE) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Aside onClose={() => setShowAside(false)}>
|
||||||
|
<div className="messages-container" ref={chatWindowRef}>
|
||||||
|
{chatHistory.map((chatItem) => (
|
||||||
|
<div
|
||||||
|
className={chatItem.isLocal ? 'message local' : 'message'}
|
||||||
|
key={chatItem.id}
|
||||||
|
>
|
||||||
|
<span className="content">{chatItem.message}</span>
|
||||||
|
<span className="sender">{chatItem.sender}</span>
|
||||||
|
</div>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
<footer className="chat-footer">
|
||||||
|
<TextInput
|
||||||
|
value={newMessage}
|
||||||
|
placeholder="Type message here"
|
||||||
|
variant="transparent"
|
||||||
|
onChange={(e) => setNewMessage(e.target.value)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
className="send-button"
|
||||||
|
variant="transparent"
|
||||||
|
disabled={!newMessage}
|
||||||
|
onClick={() => {
|
||||||
|
sendMessage(newMessage);
|
||||||
|
setNewMessage('');
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Send
|
||||||
|
</Button>
|
||||||
|
</footer>
|
||||||
|
<style jsx>{`
|
||||||
|
.messages-container {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: scroll;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message {
|
||||||
|
margin: var(--spacing-xxs);
|
||||||
|
padding: var(--spacing-xxs);
|
||||||
|
background: var(--gray-wash);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
font-size: 0.875rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.local {
|
||||||
|
background: var(--gray-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.message.local .sender {
|
||||||
|
color: var(--primary-default);
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
color: var(--text-mid);
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sender {
|
||||||
|
font-weight: var(--weight-medium);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-footer {
|
||||||
|
flex-flow: row nowrap;
|
||||||
|
box-sizing: border-box;
|
||||||
|
padding: var(--spacing-xxs) 0;
|
||||||
|
display: flex;
|
||||||
|
position: relative;
|
||||||
|
border-top: 1px solid var(--gray-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-footer :global(.input-container) {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-footer :global(.input-container input) {
|
||||||
|
padding-right: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-footer :global(.send-button) {
|
||||||
|
padding: 0 var(--spacing-xs);
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
</Aside>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ChatAside;
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { ChatAside as default } from './ChatAside';
|
||||||
|
|
@ -0,0 +1,28 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { TrayButton } from '@dailyjs/shared/components/Tray';
|
||||||
|
import { useUIState } from '@dailyjs/shared/contexts/UIStateProvider';
|
||||||
|
import { ReactComponent as IconChat } from '@dailyjs/shared/icons/chat-md.svg';
|
||||||
|
import { useChat } from '../../contexts/ChatProvider';
|
||||||
|
import { CHAT_ASIDE } from '../ChatAside/ChatAside';
|
||||||
|
|
||||||
|
export const Tray = () => {
|
||||||
|
const { toggleAside } = useUIState();
|
||||||
|
const { hasNewMessages } = useChat();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<TrayButton
|
||||||
|
label="Chat"
|
||||||
|
bubble={hasNewMessages}
|
||||||
|
onClick={() => {
|
||||||
|
toggleAside(CHAT_ASIDE);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<IconChat />
|
||||||
|
</TrayButton>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Tray;
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
export { Tray as default } from './Tray';
|
||||||
|
|
@ -0,0 +1,90 @@
|
||||||
|
import React, {
|
||||||
|
createContext,
|
||||||
|
useCallback,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react';
|
||||||
|
import { useCallState } from '@dailyjs/shared/contexts/CallProvider';
|
||||||
|
import { nanoid } from 'nanoid';
|
||||||
|
import PropTypes from 'prop-types';
|
||||||
|
|
||||||
|
export const ChatContext = createContext();
|
||||||
|
|
||||||
|
export const ChatProvider = ({ children }) => {
|
||||||
|
const { callObject } = useCallState();
|
||||||
|
const [chatHistory, setChatHistory] = useState([]);
|
||||||
|
const [hasNewMessages, setHasNewMessages] = useState(false);
|
||||||
|
|
||||||
|
const handleNewMessage = useCallback(
|
||||||
|
(e) => {
|
||||||
|
const participants = callObject.participants();
|
||||||
|
const sender = participants[e.fromId].user_name
|
||||||
|
? participants[e.fromId].user_name
|
||||||
|
: 'Guest';
|
||||||
|
|
||||||
|
setChatHistory((oldState) => [
|
||||||
|
...oldState,
|
||||||
|
{ sender, message: e.data.message, id: nanoid() },
|
||||||
|
]);
|
||||||
|
|
||||||
|
setHasNewMessages(true);
|
||||||
|
},
|
||||||
|
[callObject]
|
||||||
|
);
|
||||||
|
|
||||||
|
const sendMessage = useCallback(
|
||||||
|
(message) => {
|
||||||
|
if (!callObject) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('💬 Sending app message');
|
||||||
|
|
||||||
|
callObject.sendAppMessage({ message }, '*');
|
||||||
|
|
||||||
|
// Get the sender (local participant) name
|
||||||
|
const sender = callObject.participants().local.user_name
|
||||||
|
? callObject.participants().local.user_name
|
||||||
|
: 'Guest';
|
||||||
|
|
||||||
|
// Update local chat history
|
||||||
|
return setChatHistory((oldState) => [
|
||||||
|
...oldState,
|
||||||
|
{ sender, message, id: nanoid(), isLocal: true },
|
||||||
|
]);
|
||||||
|
},
|
||||||
|
[callObject]
|
||||||
|
);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!callObject) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`💬 Chat provider listening for app messages`);
|
||||||
|
|
||||||
|
callObject.on('app-message', handleNewMessage);
|
||||||
|
|
||||||
|
return () => callObject.off('app-message', handleNewMessage);
|
||||||
|
}, [callObject, handleNewMessage]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ChatContext.Provider
|
||||||
|
value={{
|
||||||
|
sendMessage,
|
||||||
|
chatHistory,
|
||||||
|
hasNewMessages,
|
||||||
|
setHasNewMessages,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</ChatContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
ChatProvider.propTypes = {
|
||||||
|
children: PropTypes.node,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useChat = () => useContext(ChatContext);
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
import { useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
|
import { useSound } from '@dailyjs/shared/hooks/useSound';
|
||||||
|
import { debounce } from 'debounce';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convenience hook to play `join.mp3` when participants join the call
|
||||||
|
*/
|
||||||
|
export const useMessageSound = () => {
|
||||||
|
const { load, play } = useSound('message.mp3');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
load();
|
||||||
|
}, [load]);
|
||||||
|
|
||||||
|
return useMemo(() => debounce(() => play(), 5000, true), [play]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default useMessageSound;
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 135 KiB |
|
|
@ -0,0 +1,13 @@
|
||||||
|
const withPlugins = require('next-compose-plugins');
|
||||||
|
const withTM = require('next-transpile-modules')([
|
||||||
|
'@dailyjs/shared',
|
||||||
|
'@dailyjs/basic-call',
|
||||||
|
]);
|
||||||
|
|
||||||
|
const packageJson = require('./package.json');
|
||||||
|
|
||||||
|
module.exports = withPlugins([withTM], {
|
||||||
|
env: {
|
||||||
|
PROJECT_TITLE: packageJson.description,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"name": "@dailyjs/live-streaming",
|
||||||
|
"description": "Basic Call + Live Streaming",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "next dev",
|
||||||
|
"build": "next build",
|
||||||
|
"start": "next start"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@dailyjs/shared": "*",
|
||||||
|
"@dailyjs/basic-call": "*",
|
||||||
|
"next": "^11.0.0",
|
||||||
|
"pluralize": "^8.0.0",
|
||||||
|
"react": "^17.0.2",
|
||||||
|
"react-dom": "^17.0.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"babel-plugin-module-resolver": "^4.1.0",
|
||||||
|
"next-compose-plugins": "^2.2.1",
|
||||||
|
"next-transpile-modules": "^8.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,12 @@
|
||||||
|
import React from 'react';
|
||||||
|
import App from '@dailyjs/basic-call/pages/_app';
|
||||||
|
import AppWithChat from '../components/App';
|
||||||
|
|
||||||
|
import ChatAside from '../components/ChatAside';
|
||||||
|
import Tray from '../components/Tray';
|
||||||
|
|
||||||
|
App.asides = [ChatAside];
|
||||||
|
App.customAppComponent = <AppWithChat />;
|
||||||
|
App.customTrayComponent = <Tray />;
|
||||||
|
|
||||||
|
export default App;
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
../../basic-call/pages/api
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
import Index from '@dailyjs/basic-call/pages';
|
||||||
|
|
||||||
|
export async function getStaticProps() {
|
||||||
|
// Check that both domain and key env vars are set
|
||||||
|
const isConfigured =
|
||||||
|
!!process.env.DAILY_DOMAIN && !!process.env.DAILY_API_KEY;
|
||||||
|
|
||||||
|
// Pass through domain as prop
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
domain: process.env.DAILY_DOMAIN || null,
|
||||||
|
isConfigured,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Index;
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
../basic-call/public
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g clip-path="url(#clip0)">
|
||||||
|
<path d="M17.5 15.5C19.5 13.5 19.5 10.4 17.5 8.40002" stroke="currentColor" stroke-width="2" stroke-miterlimit="10" stroke-linecap="square"/>
|
||||||
|
<path d="M20.4004 18.4C23.9004 14.9 23.9004 9.20001 20.4004 5.70001" stroke="currentColor" stroke-width="2" stroke-miterlimit="10" stroke-linecap="square"/>
|
||||||
|
<path d="M6.5 15.5C4.5 13.5 4.5 10.4 6.5 8.40002" stroke="currentColor" stroke-width="2" stroke-miterlimit="10" stroke-linecap="square"/>
|
||||||
|
<path d="M3.59961 18.4C0.0996094 14.9 0.0996094 9.20001 3.59961 5.70001" stroke="currentColor" stroke-width="2" stroke-miterlimit="10" stroke-linecap="square"/>
|
||||||
|
<path d="M12 14C13.1046 14 14 13.1046 14 12C14 10.8954 13.1046 10 12 10C10.8954 10 10 10.8954 10 12C10 13.1046 10.8954 14 12 14Z" stroke="currentColor" stroke-width="2" stroke-miterlimit="10" stroke-linecap="square"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 952 B |
Loading…
Reference in New Issue