Add chat and screenshare options to the demo
This commit is contained in:
parent
ec770a189a
commit
209b9bd72e
|
|
@ -1,51 +1,13 @@
|
|||
import React, { useMemo } from 'react';
|
||||
import ExpiryTimer from '@custom/shared/components/ExpiryTimer';
|
||||
import { useCallState } from '@custom/shared/contexts/CallProvider';
|
||||
import { useCallUI } from '@custom/shared/hooks/useCallUI';
|
||||
import React from 'react';
|
||||
|
||||
import PropTypes from 'prop-types';
|
||||
import Room from '../Call/Room';
|
||||
import { Asides } from './Asides';
|
||||
import { Modals } from './Modals';
|
||||
import App from '@custom/basic-call/components/App';
|
||||
import { ChatProvider } from '../../contexts/ChatProvider';
|
||||
|
||||
export const App = ({ customComponentForState }) => {
|
||||
const { roomExp, state } = useCallState();
|
||||
// Extend our basic call app component with the chat context
|
||||
export const CustomApp = () => (
|
||||
<ChatProvider>
|
||||
<App />
|
||||
</ChatProvider>
|
||||
);
|
||||
|
||||
const componentForState = useCallUI({
|
||||
state,
|
||||
room: <Room />,
|
||||
...customComponentForState,
|
||||
});
|
||||
|
||||
// Memoize children to avoid unnecassary renders from HOC
|
||||
return useMemo(
|
||||
() => (
|
||||
<>
|
||||
{roomExp && <ExpiryTimer expiry={roomExp} />}
|
||||
<div className="app">
|
||||
{componentForState()}
|
||||
<Modals />
|
||||
<Asides />
|
||||
<style jsx>{`
|
||||
color: white;
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
.loader {
|
||||
margin: 0 auto;
|
||||
}
|
||||
`}</style>
|
||||
</div>
|
||||
</>
|
||||
),
|
||||
[componentForState, roomExp]
|
||||
);
|
||||
};
|
||||
|
||||
App.propTypes = {
|
||||
customComponentForState: PropTypes.any,
|
||||
};
|
||||
|
||||
export default App;
|
||||
export default CustomApp;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,136 @@
|
|||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { Aside } from '@custom/shared/components/Aside';
|
||||
import Button from '@custom/shared/components/Button';
|
||||
import { TextInput } from '@custom/shared/components/Input';
|
||||
import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider';
|
||||
import { useUIState } from '@custom/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 { localParticipant } = useParticipants();
|
||||
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]);
|
||||
|
||||
const isLocalUser = (id) => id === localParticipant.user_id;
|
||||
|
||||
if (!showAside || showAside !== CHAT_ASIDE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<Aside onClose={() => setShowAside(false)}>
|
||||
<div className="messages-container" ref={chatWindowRef}>
|
||||
{chatHistory.map((chatItem) => (
|
||||
<div
|
||||
className={isLocalUser(chatItem.senderID) ? '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-dark);
|
||||
}
|
||||
|
||||
.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;
|
||||
|
|
@ -18,7 +18,7 @@ import { useDeepCompareMemo } from 'use-deep-compare';
|
|||
export const VideoGrid = React.memo(
|
||||
() => {
|
||||
const containerRef = useRef();
|
||||
const { participants } = useParticipants();
|
||||
const { allParticipants } = useParticipants();
|
||||
const [dimensions, setDimensions] = useState({
|
||||
width: 1,
|
||||
height: 1,
|
||||
|
|
@ -48,7 +48,7 @@ export const VideoGrid = React.memo(
|
|||
// Basic brute-force packing algo
|
||||
const layout = useMemo(() => {
|
||||
const aspectRatio = DEFAULT_ASPECT_RATIO;
|
||||
const tileCount = participants.length || 0;
|
||||
const tileCount = allParticipants.length || 0;
|
||||
const w = dimensions.width;
|
||||
const h = dimensions.height;
|
||||
|
||||
|
|
@ -87,23 +87,23 @@ export const VideoGrid = React.memo(
|
|||
}
|
||||
|
||||
return bestLayout;
|
||||
}, [dimensions, participants]);
|
||||
}, [dimensions, allParticipants]);
|
||||
|
||||
// Memoize our tile list to avoid unnecassary re-renders
|
||||
const tiles = useDeepCompareMemo(
|
||||
() =>
|
||||
participants.map((p) => (
|
||||
allParticipants.map((p) => (
|
||||
<Tile
|
||||
participant={p}
|
||||
key={p.id}
|
||||
mirrored
|
||||
mirrored={!p.isScreenshare}
|
||||
style={{ maxWidth: layout.width, maxHeight: layout.height }}
|
||||
/>
|
||||
)),
|
||||
[layout, participants]
|
||||
[layout, allParticipants]
|
||||
);
|
||||
|
||||
if (!participants.length) {
|
||||
if (!allParticipants.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ import {
|
|||
import Field from '@custom/shared/components/Field';
|
||||
import { TextInput, BooleanInput, SelectInput } from '@custom/shared/components/Input';
|
||||
import Well from '@custom/shared/components/Well';
|
||||
import { slugify } from '@custom/shared/lib/slugify';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
/**
|
||||
|
|
@ -24,7 +25,7 @@ export const Intro = ({
|
|||
const [rooms, setRooms] = useState({});
|
||||
const [duration, setDuration] = useState("30");
|
||||
const [roomName, setRoomName] = useState();
|
||||
const [privacy, setPrivacy] = useState(false);
|
||||
const [privacy, setPrivacy] = useState(true);
|
||||
|
||||
const fetchRooms = async () => {
|
||||
const res = await fetch('/api/presence', {
|
||||
|
|
@ -62,12 +63,17 @@ export const Intro = ({
|
|||
{Object.keys(rooms).map(room => (
|
||||
<div className="room" key={room}>
|
||||
<div>
|
||||
<div className="label">{room}</div>
|
||||
<span>{`${rooms[room].length} people in class`}</span>
|
||||
<div className="label">{slugify.revert(room)}</div>
|
||||
<span>
|
||||
{`${rooms[room].length} ${rooms[room].length > 1 ? 'people' : 'person'} in class`}
|
||||
</span>
|
||||
</div>
|
||||
<div className="join-room">
|
||||
<Button variant="dark" size="tiny" onClick={() => onJoin(room, 'join')}>
|
||||
Join Room
|
||||
<Button
|
||||
variant="gray"
|
||||
size="tiny"
|
||||
onClick={() => onJoin(slugify.convert(room), 'join')}>
|
||||
Join Class
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -88,7 +94,7 @@ export const Intro = ({
|
|||
<Field label="Give you a class name">
|
||||
<TextInput
|
||||
type="text"
|
||||
placeholder="Eg. super-stretch"
|
||||
placeholder="Eg. Super stretchy morning flow"
|
||||
defaultValue={roomName}
|
||||
onChange={(e) => setRoomName(e.target.value)}
|
||||
required
|
||||
|
|
@ -104,13 +110,16 @@ export const Intro = ({
|
|||
</SelectInput>
|
||||
</Field>
|
||||
<Field label="Public (anyone can join)">
|
||||
<BooleanInput value={privacy} onChange={e => setPrivacy(e.target.checked)} />
|
||||
<BooleanInput
|
||||
value={privacy}
|
||||
onChange={e => setPrivacy(e.target.checked)}
|
||||
/>
|
||||
</Field>
|
||||
</CardBody>
|
||||
<CardFooter divider>
|
||||
<Button
|
||||
fullWidth
|
||||
onClick={() => onJoin(roomName, 'create', duration, privacy)}
|
||||
onClick={() => onJoin(slugify.convert(roomName), 'create', duration, privacy)}
|
||||
>
|
||||
Create class
|
||||
</Button>
|
||||
|
|
@ -131,7 +140,7 @@ export const Intro = ({
|
|||
display: flex;
|
||||
width: 25vw;
|
||||
border-bottom: 1px solid var(--gray-light);
|
||||
padding-bottom: var(--spacing-xxs);
|
||||
padding: var(--spacing-xxs) 0;
|
||||
gap: 10px;
|
||||
}
|
||||
.room .label {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,24 @@
|
|||
import React from 'react';
|
||||
|
||||
import { TrayButton } from '@custom/shared/components/Tray';
|
||||
import { useUIState } from '@custom/shared/contexts/UIStateProvider';
|
||||
import { ReactComponent as IconChat } from '@custom/shared/icons/chat-md.svg';
|
||||
import { useChat } from '../../contexts/ChatProvider';
|
||||
import { CHAT_ASIDE } from '../Call/ChatAside';
|
||||
|
||||
export const ChatTray = () => {
|
||||
const { toggleAside } = useUIState();
|
||||
const { hasNewMessages } = useChat();
|
||||
|
||||
return (
|
||||
<TrayButton
|
||||
label="Chat"
|
||||
bubble={hasNewMessages}
|
||||
onClick={() => toggleAside(CHAT_ASIDE)}
|
||||
>
|
||||
<IconChat />
|
||||
</TrayButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChatTray;
|
||||
|
|
@ -0,0 +1,43 @@
|
|||
import React, { useMemo } from 'react';
|
||||
|
||||
import { TrayButton } from '@custom/shared/components/Tray';
|
||||
import { useCallState } from '@custom/shared/contexts/CallProvider';
|
||||
import { useParticipants } from '@custom/shared/contexts/ParticipantsProvider';
|
||||
import { ReactComponent as IconShare } from '@custom/shared/icons/share-sm.svg';
|
||||
|
||||
const MAX_SCREEN_SHARES = 2;
|
||||
|
||||
export const ScreenShareTray = () => {
|
||||
const { callObject, enableScreenShare } = useCallState();
|
||||
const { screens, participants } = useParticipants();
|
||||
|
||||
const isSharingScreen = useMemo(
|
||||
() => screens.some((s) => s.isLocal),
|
||||
[screens]
|
||||
);
|
||||
|
||||
const screensLength = useMemo(() => screens.length, [screens]);
|
||||
|
||||
const toggleScreenShare = () =>
|
||||
isSharingScreen ? callObject.stopScreenShare() : callObject.startScreenShare();
|
||||
|
||||
const disabled =
|
||||
participants.length &&
|
||||
screensLength >= MAX_SCREEN_SHARES &&
|
||||
!isSharingScreen;
|
||||
|
||||
if (!enableScreenShare) return null;
|
||||
|
||||
return (
|
||||
<TrayButton
|
||||
label={isSharingScreen ? 'Stop': 'Share'}
|
||||
orange={isSharingScreen}
|
||||
disabled={disabled}
|
||||
onClick={toggleScreenShare}
|
||||
>
|
||||
<IconShare />
|
||||
</TrayButton>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScreenShareTray;
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import React from 'react';
|
||||
import ChatTray from './Chat';
|
||||
import ScreenShareTray from './ScreenShare';
|
||||
|
||||
export const Tray = () => {
|
||||
return (
|
||||
<>
|
||||
<ChatTray />
|
||||
<ScreenShareTray />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default Tray;
|
||||
|
|
@ -0,0 +1,104 @@
|
|||
import React, {
|
||||
createContext,
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useCallState } from '@custom/shared/contexts/CallProvider';
|
||||
import { useSharedState } from '@custom/shared/hooks/useSharedState';
|
||||
import { nanoid } from 'nanoid';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export const ChatContext = createContext();
|
||||
|
||||
export const ChatProvider = ({ children }) => {
|
||||
const { callObject } = useCallState();
|
||||
const { sharedState, setSharedState } = useSharedState({
|
||||
initialValues: {
|
||||
chatHistory: [],
|
||||
},
|
||||
broadcast: false,
|
||||
});
|
||||
const [hasNewMessages, setHasNewMessages] = useState(false);
|
||||
|
||||
const handleNewMessage = useCallback(
|
||||
(e) => {
|
||||
if (e?.data?.message?.type) return;
|
||||
const participants = callObject.participants();
|
||||
const sender = participants[e.fromId].user_name
|
||||
? participants[e.fromId].user_name
|
||||
: 'Guest';
|
||||
|
||||
setSharedState(values => ({
|
||||
...values,
|
||||
chatHistory: [
|
||||
...values.chatHistory,
|
||||
// nanoid - we use it to generate unique ID string
|
||||
{ sender, senderID: e.fromId, message: e.data.message, id: nanoid() },
|
||||
]
|
||||
}));
|
||||
|
||||
setHasNewMessages(true);
|
||||
},
|
||||
[callObject, setSharedState]
|
||||
);
|
||||
|
||||
|
||||
const sendMessage = useCallback(
|
||||
(message) => {
|
||||
if (!callObject) return;
|
||||
|
||||
console.log('💬 Sending app message');
|
||||
|
||||
callObject.sendAppMessage({ message }, '*');
|
||||
|
||||
const participants = callObject.participants();
|
||||
// Get the sender (local participant) name
|
||||
const sender = participants.local.user_name
|
||||
? participants.local.user_name
|
||||
: 'Guest';
|
||||
const senderID = participants.local.user_id;
|
||||
|
||||
// Update shared state chat history
|
||||
return setSharedState(values => ({
|
||||
...values,
|
||||
chatHistory: [
|
||||
...values.chatHistory,
|
||||
// nanoid - we use it to generate unique ID string
|
||||
{ sender, senderID, message, id: nanoid() }
|
||||
]
|
||||
}));
|
||||
},
|
||||
[callObject, setSharedState]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!callObject) return;
|
||||
|
||||
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: sharedState.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 '@custom/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('assets/message.mp3');
|
||||
|
||||
useEffect(() => {
|
||||
load();
|
||||
}, [load]);
|
||||
|
||||
return useMemo(() => debounce(() => play(), 5000, true), [play]);
|
||||
};
|
||||
|
||||
export default useMessageSound;
|
||||
|
|
@ -1,5 +1,8 @@
|
|||
const withPlugins = require('next-compose-plugins');
|
||||
const withTM = require('next-transpile-modules')(['@custom/shared']);
|
||||
const withTM = require('next-transpile-modules')([
|
||||
'@custom/shared',
|
||||
'@custom/basic-call',
|
||||
]);
|
||||
|
||||
const packageJson = require('./package.json');
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,9 @@ import React from 'react';
|
|||
import GlobalStyle from '@custom/shared/components/GlobalStyle';
|
||||
import Head from 'next/head';
|
||||
import PropTypes from 'prop-types';
|
||||
import { CustomApp } from '../components/App/App';
|
||||
import ChatAside from '../components/Call/ChatAside';
|
||||
import Tray from '../components/Tray';
|
||||
|
||||
function App({ Component, pageProps }) {
|
||||
return (
|
||||
|
|
@ -31,9 +34,9 @@ App.propTypes = {
|
|||
pageProps: PropTypes.object,
|
||||
};
|
||||
|
||||
App.asides = [];
|
||||
App.asides = [ChatAside];
|
||||
App.modals = [];
|
||||
App.customTrayComponent = null;
|
||||
App.customAppComponent = null;
|
||||
App.customTrayComponent = <Tray />;
|
||||
App.customAppComponent = <CustomApp />;
|
||||
|
||||
export default App;
|
||||
|
|
|
|||
|
|
@ -97,7 +97,7 @@ PersonRow.propTypes = {
|
|||
export const PeopleAside = () => {
|
||||
const { callObject } = useCallState();
|
||||
const { showAside, setShowAside } = useUIState();
|
||||
const { allParticipants, isOwner } = useParticipants();
|
||||
const { participants, isOwner } = useParticipants();
|
||||
|
||||
if (!showAside || showAside !== PEOPLE_ASIDE) {
|
||||
return null;
|
||||
|
|
@ -131,7 +131,7 @@ export const PeopleAside = () => {
|
|||
</div>
|
||||
)}
|
||||
<div className="rows">
|
||||
{allParticipants.map((p) => (
|
||||
{participants.map((p) => (
|
||||
<PersonRow participant={p} key={p.id} isOwner={isOwner} />
|
||||
))}
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -289,6 +289,24 @@ export const Button = forwardRef(
|
|||
.button.dark:disabled {
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.button.gray {
|
||||
background: ${theme.gray.light};
|
||||
color: ${theme.gray.dark};
|
||||
border: 0;
|
||||
}
|
||||
.button.gray:hover,
|
||||
.button.gray:focus,
|
||||
.button.gray:active {
|
||||
background: ${theme.gray.default};
|
||||
border: 0;
|
||||
}
|
||||
.button.gray:focus {
|
||||
box-shadow: 0 0 0 3px rgba(255, 255, 255, 0.15);
|
||||
}
|
||||
.button.gray:disabled {
|
||||
opacity: 0.35;
|
||||
}
|
||||
|
||||
.button.white {
|
||||
background: white;
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import React, {
|
|||
useEffect,
|
||||
useState,
|
||||
} from 'react';
|
||||
import DailyIframe from '@daily-co/daily-js';
|
||||
import Bowser from 'bowser';
|
||||
import PropTypes from 'prop-types';
|
||||
import {
|
||||
|
|
@ -31,6 +32,7 @@ export const CallProvider = ({
|
|||
token = '',
|
||||
subscribeToTracksAutomatically = true,
|
||||
}) => {
|
||||
const [enableScreenShare, setEnableScreenShare] = useState(false);
|
||||
const [videoQuality, setVideoQuality] = useState(VIDEO_QUALITY_AUTO);
|
||||
const [showLocalVideo, setShowLocalVideo] = useState(true);
|
||||
const [preJoinNonAuthorized, setPreJoinNonAuthorized] = useState(false);
|
||||
|
|
@ -52,7 +54,12 @@ export const CallProvider = ({
|
|||
if (!daily) return;
|
||||
const updateRoomConfigState = async () => {
|
||||
const roomConfig = await daily.room();
|
||||
const isOob = !!roomConfig.config?.owner_only_broadcast;
|
||||
const owner = roomConfig.tokenConfig?.is_owner;
|
||||
const config = roomConfig?.config;
|
||||
|
||||
const fullUI = !isOob || (isOob && owner);
|
||||
|
||||
if (!config) return;
|
||||
|
||||
if (config.exp) {
|
||||
|
|
@ -76,6 +83,12 @@ export const CallProvider = ({
|
|||
roomConfig?.tokenConfig?.start_cloud_recording ?? false
|
||||
);
|
||||
}
|
||||
setEnableScreenShare(
|
||||
fullUI &&
|
||||
(roomConfig?.tokenConfig?.enable_screenshare ??
|
||||
roomConfig?.config?.enable_screenshare) &&
|
||||
DailyIframe.supportedBrowser().supportsScreenShare
|
||||
);
|
||||
};
|
||||
updateRoomConfigState();
|
||||
}, [state, daily]);
|
||||
|
|
@ -115,11 +128,13 @@ export const CallProvider = ({
|
|||
showLocalVideo,
|
||||
roomExp,
|
||||
enableRecording,
|
||||
enableScreenShare,
|
||||
videoQuality,
|
||||
setVideoQuality,
|
||||
setBandwidth,
|
||||
setRedirectOnLeave,
|
||||
setShowLocalVideo,
|
||||
setEnableScreenShare,
|
||||
startCloudRecording,
|
||||
subscribeToTracksAutomatically,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M18 22H6" stroke="currentColor" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M1 2L8 9" stroke="currentColor" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M11 2H23V18H1V12" stroke="currentColor" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M7 2H1V8" stroke="currentColor" stroke-width="2" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 631 B |
|
|
@ -0,0 +1,17 @@
|
|||
const convert = (keyword) => {
|
||||
return keyword
|
||||
.toString()
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace(/\s+/g, '-')
|
||||
};
|
||||
|
||||
const revert = (keyword) => {
|
||||
return keyword
|
||||
.toString()
|
||||
.trim()
|
||||
.toLowerCase()
|
||||
.replace('-', ' ')
|
||||
}
|
||||
|
||||
export const slugify = { convert, revert };
|
||||
Loading…
Reference in New Issue