initial commit

This commit is contained in:
Jon 2021-06-24 15:09:47 +01:00
parent ea1fac4ebc
commit c67b79af28
17 changed files with 358 additions and 0 deletions

View File

@ -0,0 +1,4 @@
{
"presets": ["next/babel"],
"plugins": ["inline-react-svg"]
}

View File

@ -0,0 +1 @@
# Live Streaming

View File

@ -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;

View File

@ -0,0 +1 @@
export { AppWithChat as default } from './App';

View File

@ -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;

View File

@ -0,0 +1 @@
export { ChatAside as default } from './ChatAside';

View File

@ -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;

View File

@ -0,0 +1 @@
export { Tray as default } from './Tray';

View File

@ -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);

View File

@ -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

View File

@ -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,
},
});

View File

@ -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"
}
}

View File

@ -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;

View File

@ -0,0 +1 @@
../../basic-call/pages/api

View File

@ -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;

View File

@ -0,0 +1 @@
../basic-call/public