diff --git a/dailyjs/live-streaming/.babelrc b/dailyjs/live-streaming/.babelrc new file mode 100644 index 0000000..a6f4434 --- /dev/null +++ b/dailyjs/live-streaming/.babelrc @@ -0,0 +1,4 @@ +{ + "presets": ["next/babel"], + "plugins": ["inline-react-svg"] +} diff --git a/dailyjs/live-streaming/README.md b/dailyjs/live-streaming/README.md new file mode 100644 index 0000000..b49b3dd --- /dev/null +++ b/dailyjs/live-streaming/README.md @@ -0,0 +1 @@ +# Live Streaming diff --git a/dailyjs/live-streaming/components/App/App.js b/dailyjs/live-streaming/components/App/App.js new file mode 100644 index 0000000..7a9a4ce --- /dev/null +++ b/dailyjs/live-streaming/components/App/App.js @@ -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 = () => ( + + + +); + +export default AppWithChat; diff --git a/dailyjs/live-streaming/components/App/index.js b/dailyjs/live-streaming/components/App/index.js new file mode 100644 index 0000000..770f031 --- /dev/null +++ b/dailyjs/live-streaming/components/App/index.js @@ -0,0 +1 @@ +export { AppWithChat as default } from './App'; diff --git a/dailyjs/live-streaming/components/ChatAside/ChatAside.js b/dailyjs/live-streaming/components/ChatAside/ChatAside.js new file mode 100644 index 0000000..2c88d3e --- /dev/null +++ b/dailyjs/live-streaming/components/ChatAside/ChatAside.js @@ -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 ( + + ); +}; + +export default ChatAside; diff --git a/dailyjs/live-streaming/components/ChatAside/index.js b/dailyjs/live-streaming/components/ChatAside/index.js new file mode 100644 index 0000000..8e15e50 --- /dev/null +++ b/dailyjs/live-streaming/components/ChatAside/index.js @@ -0,0 +1 @@ +export { ChatAside as default } from './ChatAside'; diff --git a/dailyjs/live-streaming/components/Tray/Tray.js b/dailyjs/live-streaming/components/Tray/Tray.js new file mode 100644 index 0000000..84290bf --- /dev/null +++ b/dailyjs/live-streaming/components/Tray/Tray.js @@ -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 ( + <> + { + toggleAside(CHAT_ASIDE); + }} + > + + + + ); +}; + +export default Tray; diff --git a/dailyjs/live-streaming/components/Tray/index.js b/dailyjs/live-streaming/components/Tray/index.js new file mode 100644 index 0000000..100bcc8 --- /dev/null +++ b/dailyjs/live-streaming/components/Tray/index.js @@ -0,0 +1 @@ +export { Tray as default } from './Tray'; diff --git a/dailyjs/live-streaming/contexts/ChatProvider.js b/dailyjs/live-streaming/contexts/ChatProvider.js new file mode 100644 index 0000000..6ad611a --- /dev/null +++ b/dailyjs/live-streaming/contexts/ChatProvider.js @@ -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 ( + + {children} + + ); +}; + +ChatProvider.propTypes = { + children: PropTypes.node, +}; + +export const useChat = () => useContext(ChatContext); diff --git a/dailyjs/live-streaming/hooks/useMessageSound.js b/dailyjs/live-streaming/hooks/useMessageSound.js new file mode 100644 index 0000000..e894a1c --- /dev/null +++ b/dailyjs/live-streaming/hooks/useMessageSound.js @@ -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; diff --git a/dailyjs/live-streaming/image.png b/dailyjs/live-streaming/image.png new file mode 100644 index 0000000..cca9672 Binary files /dev/null and b/dailyjs/live-streaming/image.png differ diff --git a/dailyjs/live-streaming/next.config.js b/dailyjs/live-streaming/next.config.js new file mode 100644 index 0000000..9a0a6ee --- /dev/null +++ b/dailyjs/live-streaming/next.config.js @@ -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, + }, +}); diff --git a/dailyjs/live-streaming/package.json b/dailyjs/live-streaming/package.json new file mode 100644 index 0000000..c340eb5 --- /dev/null +++ b/dailyjs/live-streaming/package.json @@ -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" + } +} diff --git a/dailyjs/live-streaming/pages/_app.js b/dailyjs/live-streaming/pages/_app.js new file mode 100644 index 0000000..ad38f4e --- /dev/null +++ b/dailyjs/live-streaming/pages/_app.js @@ -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 = ; +App.customTrayComponent = ; + +export default App; diff --git a/dailyjs/live-streaming/pages/api b/dailyjs/live-streaming/pages/api new file mode 120000 index 0000000..999f604 --- /dev/null +++ b/dailyjs/live-streaming/pages/api @@ -0,0 +1 @@ +../../basic-call/pages/api \ No newline at end of file diff --git a/dailyjs/live-streaming/pages/index.js b/dailyjs/live-streaming/pages/index.js new file mode 100644 index 0000000..5f31f95 --- /dev/null +++ b/dailyjs/live-streaming/pages/index.js @@ -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; diff --git a/dailyjs/live-streaming/public b/dailyjs/live-streaming/public new file mode 120000 index 0000000..33a6e67 --- /dev/null +++ b/dailyjs/live-streaming/public @@ -0,0 +1 @@ +../basic-call/public \ No newline at end of file