diff --git a/apps/100ms-web/src/components/Header/ParticipantList.jsx b/apps/100ms-web/src/components/Header/ParticipantList.jsx index bb1c7a0577..06cf5204d3 100644 --- a/apps/100ms-web/src/components/Header/ParticipantList.jsx +++ b/apps/100ms-web/src/components/Header/ParticipantList.jsx @@ -373,8 +373,8 @@ export const ParticipantSearch = ({ onSearch, placeholder }) => { { event.stopPropagation(); diff --git a/packages/roomkit-react/src/Dropdown/Dropdown.tsx b/packages/roomkit-react/src/Dropdown/Dropdown.tsx index 9a382a8c12..141d16af8a 100644 --- a/packages/roomkit-react/src/Dropdown/Dropdown.tsx +++ b/packages/roomkit-react/src/Dropdown/Dropdown.tsx @@ -53,7 +53,7 @@ const DropdownItem = styled(Item, { display: 'flex', alignItems: 'center', outline: 'none', - backgroundColor: '$surface_default', + backgroundColor: '$surface_dim', '&:hover': { cursor: 'pointer', bg: '$surface_bright', @@ -74,7 +74,7 @@ const DropdownContent = styled(Content, { maxHeight: '$64', r: '$1', py: '$4', - backgroundColor: '$surface_default', + backgroundColor: '$surface_dim', overflowY: 'auto', boxShadow: '0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23)', zIndex: 20, diff --git a/packages/roomkit-react/src/Popover/index.tsx b/packages/roomkit-react/src/Popover/index.tsx index e2768ed96d..9b4325a029 100644 --- a/packages/roomkit-react/src/Popover/index.tsx +++ b/packages/roomkit-react/src/Popover/index.tsx @@ -1,4 +1,4 @@ -import { Arrow, Content, Popover as Root, Portal, Trigger } from '@radix-ui/react-popover'; +import { Arrow, Close, Content, Popover as Root, Portal, Trigger } from '@radix-ui/react-popover'; import { styled } from '../Theme'; import { popoverAnimation } from '../utils/animations'; @@ -23,4 +23,5 @@ export const Popover = { Trigger: StyledTrigger, Portal: Portal, Arrow: StyledArrow, + Close: Close, }; diff --git a/packages/roomkit-react/src/Prebuilt/Prebuilt.stories.tsx b/packages/roomkit-react/src/Prebuilt/Prebuilt.stories.tsx index 3468752840..b8920ca71a 100644 --- a/packages/roomkit-react/src/Prebuilt/Prebuilt.stories.tsx +++ b/packages/roomkit-react/src/Prebuilt/Prebuilt.stories.tsx @@ -6,7 +6,7 @@ export default { title: 'UI Components/Prebuilt', component: HMSPrebuilt, argTypes: { - roomCode: { control: { type: 'text' }, defaultValue: 'tsj-obqh-lwx' }, + roomCode: { control: { type: 'text' }, defaultValue: 'cuf-wywo-trf' }, logo: { control: { type: 'object' }, defaultValue: undefined }, typography: { control: { type: 'object' }, defaultValue: 'Roboto' }, }, @@ -18,7 +18,7 @@ const PrebuiltRoomCodeStory: StoryFn = ({ roomCode = '', log export const Example = PrebuiltRoomCodeStory.bind({}); Example.args = { - roomCode: 'tsj-obqh-lwx', + roomCode: 'cuf-wywo-trf', options: { endpoints: { roomLayout: 'https://demo8271564.mockable.io/v2/layouts/ui', diff --git a/packages/roomkit-react/src/Prebuilt/common/hooks.js b/packages/roomkit-react/src/Prebuilt/common/hooks.js index 60fc49c1d0..82df0d3278 100644 --- a/packages/roomkit-react/src/Prebuilt/common/hooks.js +++ b/packages/roomkit-react/src/Prebuilt/common/hooks.js @@ -1,6 +1,8 @@ // @ts-check import { useEffect, useRef, useState } from 'react'; +import { JoinForm_JoinBtnType } from '@100mslive/types-prebuilt/elements/join_form'; import { selectAvailableRoleNames, selectIsConnectedToRoom, selectPeerCount, useHMSStore } from '@100mslive/react-sdk'; +import { useRoomLayout } from '../provider/roomLayoutProvider'; import { isInternalRole } from './utils'; /** @@ -45,3 +47,9 @@ export const useFilteredRoles = () => { const roles = useHMSStore(selectAvailableRoleNames).filter(role => !isInternalRole(role)); return roles; }; + +export const useShowStreamingUI = () => { + const layout = useRoomLayout(); + const { join_form } = layout?.screens?.preview?.default?.elements || {}; + return join_form?.join_btn_type === JoinForm_JoinBtnType.JOIN_BTN_TYPE_JOIN_AND_GO_LIVE; +}; diff --git a/packages/roomkit-react/src/Prebuilt/common/utils.js b/packages/roomkit-react/src/Prebuilt/common/utils.js index b3a876c703..819f001fda 100644 --- a/packages/roomkit-react/src/Prebuilt/common/utils.js +++ b/packages/roomkit-react/src/Prebuilt/common/utils.js @@ -83,3 +83,18 @@ export const getUpdatedHeight = (e, MINIMUM_HEIGHT) => { const sheetHeightInVH = Math.max(MINIMUM_HEIGHT, heightToPercentage > 80 ? 100 : heightToPercentage); return `${sheetHeightInVH}vh`; }; + +export const getFormattedCount = num => { + const formatter = new Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 2 }); + const formattedNum = formatter.format(num); + return formattedNum; +}; + +export const formatTime = timeInSeconds => { + timeInSeconds = Math.floor(timeInSeconds / 1000); + const hours = Math.floor(timeInSeconds / 3600); + const minutes = Math.floor((timeInSeconds % 3600) / 60); + const seconds = timeInSeconds % 60; + const hour = hours !== 0 ? `${hours < 10 ? '0' : ''}${hours}:` : ''; + return `${hour}${minutes < 10 ? '0' : ''}${minutes}:${seconds < 10 ? '0' : ''}${seconds}`; +}; diff --git a/packages/roomkit-react/src/Prebuilt/components/AudioVideoToggle.jsx b/packages/roomkit-react/src/Prebuilt/components/AudioVideoToggle.jsx index 4b68f82746..50b1bfd3ba 100644 --- a/packages/roomkit-react/src/Prebuilt/components/AudioVideoToggle.jsx +++ b/packages/roomkit-react/src/Prebuilt/components/AudioVideoToggle.jsx @@ -18,7 +18,7 @@ import { isMacOS } from '../common/constants'; const optionsCSS = { fontWeight: '$semiBold', color: '$on_surface_high', w: '100%', p: '$8' }; -export const AudioVideoToggle = () => { +export const AudioVideoToggle = ({ hideOptions = false }) => { const { allDevices, selectedDeviceIDs, updateDevice } = useDevices(); const { videoInput, audioInput } = allDevices; @@ -68,33 +68,61 @@ export const AudioVideoToggle = () => { return ( {toggleAudio ? ( - : - } - active={isLocalAudioEnabled} - onClick={toggleAudio} - key="toggleAudio" - /> + hideOptions ? ( + + + {!isLocalAudioEnabled ? ( + + ) : ( + + )} + + + ) : ( + + ) : ( + + ) + } + active={isLocalAudioEnabled} + onClick={toggleAudio} + key="toggleAudio" + /> + ) ) : null} {toggleVideo ? ( - - ) : ( - - ) - } - key="toggleVideo" - active={isLocalVideoEnabled} - onClick={toggleVideo} - /> + hideOptions ? ( + + + {!isLocalVideoEnabled ? ( + + ) : ( + + )} + + + ) : ( + + ) : ( + + ) + } + key="toggleVideo" + active={isLocalVideoEnabled} + onClick={toggleVideo} + /> + ) ) : null} {localVideoTrack?.facingMode ? ( diff --git a/packages/roomkit-react/src/Prebuilt/components/BottomActionSheet/BottomActionSheet.jsx b/packages/roomkit-react/src/Prebuilt/components/BottomActionSheet/BottomActionSheet.jsx deleted file mode 100644 index 8f339d15f8..0000000000 --- a/packages/roomkit-react/src/Prebuilt/components/BottomActionSheet/BottomActionSheet.jsx +++ /dev/null @@ -1,96 +0,0 @@ -import React, { useEffect, useRef, useState } from 'react'; -import { CrossIcon } from '@100mslive/react-icons'; -import { Box, Flex, Popover, Text } from '../../..'; -import { getUpdatedHeight } from '../../common/utils'; - -const BottomActionSheet = ({ - title = '', - children = <>, - triggerContent, - containerCSS = {}, - // By default the component starts just above the trigger. - // A negative offset allows it to start from the bottom of the screen. - sideOffset = -50, - defaultHeight = 50, -}) => { - const MINIMUM_HEIGHT = 40; // vh - const [sheetOpen, setSheetOpen] = useState(false); - const [sheetHeight, setSheetHeight] = useState(`${Math.min(Math.max(MINIMUM_HEIGHT, defaultHeight), 100)}vh`); - const closeRef = useRef(null); - - // Close the sheet if height goes under MINIMUM_HEIGHT - useEffect(() => { - if (closeRef?.current && parseFloat(sheetHeight.slice(0, -2)) <= MINIMUM_HEIGHT) { - setSheetOpen(false); - // Delay for showing the opacity animation, can be removed if not needed - setTimeout(() => closeRef.current?.click(), 200); - } - }, [sheetHeight]); - - return ( - <> - { - if (!open) { - setSheetHeight('0'); - } - setSheetOpen(open); - }} - > - {triggerContent} - - - - { - const updatedSheetHeight = getUpdatedHeight(e, MINIMUM_HEIGHT); - setSheetHeight(updatedSheetHeight); - }} - css={{ - borderBottom: '1px solid $border_bright', - px: '$8', - pb: '$4', - mb: '$4', - w: '100%', - }} - > - - {title} - - - - - - - - {children} - - - - - - ); -}; - -export default BottomActionSheet; diff --git a/packages/roomkit-react/src/Prebuilt/components/BottomActionSheet/BottomActionSheet.stories.tsx b/packages/roomkit-react/src/Prebuilt/components/BottomActionSheet/BottomActionSheet.stories.tsx deleted file mode 100644 index 1c2caa4bcb..0000000000 --- a/packages/roomkit-react/src/Prebuilt/components/BottomActionSheet/BottomActionSheet.stories.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React, { ReactElement } from 'react'; -import { Meta } from '@storybook/react'; -import { Button } from '../../../Button'; -import { Box } from '../../../Layout'; -import { Text } from '../../../Text'; -import { CSS } from '../../../Theme'; -import BottomActionSheet from './BottomActionSheet'; - -// WIP - -export default { - title: 'Components/BottomActionSheet', - component: BottomActionSheet, - argTypes: { - title: { control: 'text' }, - triggerContent: { control: 'jsx' }, - containerCSS: { control: 'object' }, - sideOffset: { control: 'number' }, - defaultHeight: { control: 'number' }, - }, -} as Meta; - -interface BottomActionSheetProps { - title: string; - triggerContent: ReactElement; - children: ReactElement; - containerCSS: CSS; - sideOffset: number; - defaultHeight: number; -} - -const Template = (args: BottomActionSheetProps) => ( - - - This is the content of the BottomActionSheet. - You can put any content you like here. - - -); - -// Example story with default props -export const Default = Template.bind({}); -Default.args = { - title: 'Example BottomActionSheet', - triggerContent: , -}; diff --git a/packages/roomkit-react/src/Prebuilt/components/Chat/ChatSelector.jsx b/packages/roomkit-react/src/Prebuilt/components/Chat/ChatSelector.jsx index 5513c124d6..fe99845ce5 100644 --- a/packages/roomkit-react/src/Prebuilt/components/Chat/ChatSelector.jsx +++ b/packages/roomkit-react/src/Prebuilt/components/Chat/ChatSelector.jsx @@ -10,7 +10,7 @@ import { } from '@100mslive/react-sdk'; import { CheckIcon } from '@100mslive/react-icons'; import { Box, Dropdown, Flex, HorizontalDivider, Text, Tooltip } from '../../../'; -import { ParticipantSearch } from '../Header/ParticipantList'; +import { ParticipantSearch } from '../Footer/ParticipantList'; import { useFilteredRoles } from '../../common/hooks'; const ChatDotIcon = () => { diff --git a/packages/roomkit-react/src/Prebuilt/components/EmojiReaction.jsx b/packages/roomkit-react/src/Prebuilt/components/EmojiReaction.jsx index 2024d5ec9b..2fa9c8751b 100644 --- a/packages/roomkit-react/src/Prebuilt/components/EmojiReaction.jsx +++ b/packages/roomkit-react/src/Prebuilt/components/EmojiReaction.jsx @@ -1,76 +1,25 @@ -import React, { Fragment, useCallback, useMemo, useState } from 'react'; +import React, { Fragment, useState } from 'react'; import data from '@emoji-mart/data/sets/14/apple.json'; import { init } from 'emoji-mart'; -import { - selectAvailableRoleNames, - selectIsConnectedToRoom, - selectLocalPeerID, - selectLocalPeerRoleName, - useCustomEvent, - useHMSActions, - useHMSStore, - useRecordingStreaming, -} from '@100mslive/react-sdk'; +import { selectIsConnectedToRoom, useHMSStore } from '@100mslive/react-sdk'; import { EmojiIcon } from '@100mslive/react-icons'; +import { EmojiCard } from './Footer/EmojiCard'; import { Dropdown } from '../../Dropdown'; -import { Flex } from '../../Layout'; -import { Text } from '../../Text'; -import { styled } from '../../Theme'; import { Tooltip } from '../../Tooltip'; import IconButton from '../IconButton'; -import { useHLSViewerRole } from './AppData/useUISettings'; import { useDropdownList } from './hooks/useDropdownList'; import { useIsFeatureEnabled } from './hooks/useFeatures'; -import { EMOJI_REACTION_TYPE, FEATURE_LIST, HLS_TIMED_METADATA_DOC_URL } from '../common/constants'; +import { FEATURE_LIST } from '../common/constants'; init({ data }); -// When changing emojis in the grid, keep in mind that the payload used in sendHLSTimedMetadata has a limit of 100 characters. Using bigger emoji Ids can cause the limit to be exceeded. -const emojiReactionList = [ - [{ emojiId: '+1' }, { emojiId: '-1' }, { emojiId: 'wave' }, { emojiId: 'clap' }, { emojiId: 'fire' }], - [{ emojiId: 'tada' }, { emojiId: 'heart_eyes' }, { emojiId: 'joy' }, { emojiId: 'open_mouth' }, { emojiId: 'sob' }], -]; - export const EmojiReaction = () => { const [open, setOpen] = useState(false); - const hmsActions = useHMSActions(); const isConnected = useHMSStore(selectIsConnectedToRoom); - const roles = useHMSStore(selectAvailableRoleNames); - const localPeerRole = useHMSStore(selectLocalPeerRoleName); - const localPeerId = useHMSStore(selectLocalPeerID); - const hlsViewerRole = useHLSViewerRole(); - const { isStreamingOn } = useRecordingStreaming(); const isFeatureEnabled = useIsFeatureEnabled(FEATURE_LIST.EMOJI_REACTION); - const filteredRoles = useMemo(() => roles.filter(role => role !== hlsViewerRole), [roles, hlsViewerRole]); useDropdownList({ open: open, name: 'EmojiReaction' }); - const onEmojiEvent = useCallback(data => { - window.showFlyingEmoji(data?.emojiId, data?.senderId); - }, []); - - const { sendEvent } = useCustomEvent({ - type: EMOJI_REACTION_TYPE, - onEvent: onEmojiEvent, - }); - - const sendReaction = async emojiId => { - const data = { - type: EMOJI_REACTION_TYPE, - emojiId: emojiId, - senderId: localPeerId, - }; - sendEvent(data, { roleNames: filteredRoles }); - if (isStreamingOn) { - await hmsActions.sendHLSTimedMetadata([ - { - payload: JSON.stringify(data), - duration: 2, - }, - ]); - } - }; - - if (!isConnected || localPeerRole === hlsViewerRole || !isFeatureEnabled) { + if (!isConnected || !isFeatureEnabled) { return null; } return ( @@ -84,58 +33,9 @@ export const EmojiReaction = () => { - {emojiReactionList.map((emojiLine, index) => ( - - {emojiLine.map(emoji => ( - sendReaction(emoji.emojiId)}> - - - ))} - - ))} -
- - Reactions will be timed for Live Streaming viewers.{' '} - - - - Learn more. - - -
+
); }; - -const EmojiContainer = styled('span', { - position: 'relative', - cursor: 'pointer', - width: '$16', - height: '$16', - p: '$4', - '&:hover': { - p: '7px', - bg: '$surface_brighter', - borderRadius: '$1', - }, -}); diff --git a/packages/roomkit-react/src/Prebuilt/components/EndSessionContent.jsx b/packages/roomkit-react/src/Prebuilt/components/EndSessionContent.jsx new file mode 100644 index 0000000000..865c79104b --- /dev/null +++ b/packages/roomkit-react/src/Prebuilt/components/EndSessionContent.jsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { AlertTriangleIcon, CrossIcon } from '@100mslive/react-icons'; +import { Button } from '../../Button'; +import { Box, Flex } from '../../Layout'; +import { Text } from '../../Text'; +import { useShowStreamingUI } from '../common/hooks'; + +export const EndSessionContent = ({ setShowEndRoomAlert, endRoom, isModal = false }) => { + const showStreamingUI = useShowStreamingUI(); + return ( + + + + + End {showStreamingUI ? 'Stream' : 'Session'} + + {isModal ? null : ( + setShowEndRoomAlert(false)}> + + + )} + + + The {showStreamingUI ? 'stream' : 'session'} will end for everyone and all the activities will stop. You can't + undo this action. + + + + + + + ); +}; diff --git a/packages/roomkit-react/src/Prebuilt/components/Footer.jsx b/packages/roomkit-react/src/Prebuilt/components/Footer.jsx index 03f8f564b7..c205d31b12 100644 --- a/packages/roomkit-react/src/Prebuilt/components/Footer.jsx +++ b/packages/roomkit-react/src/Prebuilt/components/Footer.jsx @@ -1,8 +1,9 @@ import React from 'react'; import { ConferencingFooter } from './Footer/ConferencingFooter'; import { StreamingFooter } from './Footer/StreamingFooter'; -import { isStreamingKit } from '../common/utils'; +import { useShowStreamingUI } from '../common/hooks'; export const Footer = () => { - return isStreamingKit() ? : ; + const showStreamingUI = useShowStreamingUI(); + return showStreamingUI ? : ; }; diff --git a/packages/roomkit-react/src/Prebuilt/components/Footer/ConferencingFooter.jsx b/packages/roomkit-react/src/Prebuilt/components/Footer/ConferencingFooter.jsx index fa169876e6..51c3d28cf4 100644 --- a/packages/roomkit-react/src/Prebuilt/components/Footer/ConferencingFooter.jsx +++ b/packages/roomkit-react/src/Prebuilt/components/Footer/ConferencingFooter.jsx @@ -1,101 +1,57 @@ -import React, { Fragment, Suspense, useState } from 'react'; +import React, { Suspense } from 'react'; import { useMedia } from 'react-use'; -import { selectIsAllowedToPublish, useHMSStore, useScreenShare } from '@100mslive/react-sdk'; -import { MusicIcon } from '@100mslive/react-icons'; -import { config as cssConfig, Flex, Footer as AppFooter, Tooltip } from '../../../'; -import IconButton from '../../IconButton'; +import { selectIsLocalVideoEnabled, useHMSStore } from '@100mslive/react-sdk'; +import { config as cssConfig, Footer as AppFooter } from '../../../'; import { AudioVideoToggle } from '../AudioVideoToggle'; import { EmojiReaction } from '../EmojiReaction'; import { LeaveRoom } from '../LeaveRoom'; -import MetaActions from '../MetaActions'; import { MoreSettings } from '../MoreSettings/MoreSettings'; -import { PIP } from '../PIP'; import { ScreenshareToggle } from '../ScreenShare'; -import { ScreenShareHintModal } from '../ScreenshareHintModal'; import { ChatToggle } from './ChatToggle'; -import { useIsFeatureEnabled } from '../hooks/useFeatures'; -import { isScreenshareSupported } from '../../common/utils'; +import { ParticipantCount } from './ParticipantList'; import { FeatureFlags } from '../../services/FeatureFlags'; -import { FEATURE_LIST } from '../../common/constants'; const TranscriptionButton = React.lazy(() => import('../../plugins/transcription')); const VirtualBackground = React.lazy(() => import('../../plugins/VirtualBackground/VirtualBackground')); -const ScreenshareAudio = () => { - const { - amIScreenSharing, - screenShareVideoTrackId: video, - screenShareAudioTrackId: audio, - toggleScreenShare, - } = useScreenShare(); - const isAllowedToPublish = useHMSStore(selectIsAllowedToPublish); - const isAudioScreenshare = amIScreenSharing && !video && !!audio; - const [showModal, setShowModal] = useState(false); - const isFeatureEnabled = useIsFeatureEnabled(FEATURE_LIST.AUDIO_ONLY_SCREENSHARE); - if (!isFeatureEnabled || !isAllowedToPublish.screen || !isScreenshareSupported()) { - return null; - } - return ( - - - { - if (amIScreenSharing) { - toggleScreenShare(); - } else { - setShowModal(true); - } - }} - data-testid="screenshare_audio" - > - - - - {showModal && setShowModal(false)} />} - - ); -}; - export const ConferencingFooter = () => { const isMobile = useMedia(cssConfig.media.md); + const isVideoOn = useHMSStore(selectIsLocalVideoEnabled); + return ( - - - - - - - {FeatureFlags.enableTranscription ? : null} - - {isMobile && } - - - - - - - - - - - - - - - {!isMobile && } - - - + + {isMobile ? ( + <> + + + + + + + + ) : ( + <> + + {isVideoOn ? ( + + + + ) : null} + {FeatureFlags.enableTranscription ? : null} + + + + + + + + + + + + + + )} ); }; diff --git a/packages/roomkit-react/src/Prebuilt/components/Footer/EmojiCard.jsx b/packages/roomkit-react/src/Prebuilt/components/Footer/EmojiCard.jsx new file mode 100644 index 0000000000..09a2a0896b --- /dev/null +++ b/packages/roomkit-react/src/Prebuilt/components/Footer/EmojiCard.jsx @@ -0,0 +1,80 @@ +import React, { useCallback, useMemo } from 'react'; +import data from '@emoji-mart/data/sets/14/apple.json'; +import { init } from 'emoji-mart'; +import { + selectAvailableRoleNames, + selectLocalPeerID, + useCustomEvent, + useHMSActions, + useHMSStore, + useRecordingStreaming, +} from '@100mslive/react-sdk'; +import { Flex } from '../../../Layout'; +import { styled } from '../../../Theme'; +import { useHLSViewerRole } from '../AppData/useUISettings'; +import { EMOJI_REACTION_TYPE } from '../../common/constants'; + +init({ data }); + +// When changing emojis in the grid, keep in mind that the payload used in sendHLSTimedMetadata has a limit of 100 characters. Using bigger emoji Ids can cause the limit to be exceeded. +const emojiReactionList = [ + [{ emojiId: '+1' }, { emojiId: '-1' }, { emojiId: 'wave' }, { emojiId: 'clap' }, { emojiId: 'fire' }], + [{ emojiId: 'tada' }, { emojiId: 'heart_eyes' }, { emojiId: 'joy' }, { emojiId: 'open_mouth' }, { emojiId: 'sob' }], +]; + +export const EmojiCard = () => { + const hmsActions = useHMSActions(); + const roles = useHMSStore(selectAvailableRoleNames); + const localPeerId = useHMSStore(selectLocalPeerID); + const hlsViewerRole = useHLSViewerRole(); + const { isStreamingOn } = useRecordingStreaming(); + const filteredRoles = useMemo(() => roles.filter(role => role !== hlsViewerRole), [roles, hlsViewerRole]); + + const onEmojiEvent = useCallback(data => { + window.showFlyingEmoji(data?.emojiId, data?.senderId); + }, []); + + const { sendEvent } = useCustomEvent({ + type: EMOJI_REACTION_TYPE, + onEvent: onEmojiEvent, + }); + + const sendReaction = async emojiId => { + const data = { + type: EMOJI_REACTION_TYPE, + emojiId: emojiId, + senderId: localPeerId, + }; + sendEvent(data, { roleNames: filteredRoles }); + if (isStreamingOn) { + await hmsActions.sendHLSTimedMetadata([ + { + payload: JSON.stringify(data), + duration: 2, + }, + ]); + } + }; + return emojiReactionList.map((emojiLine, index) => ( + + {emojiLine.map(emoji => ( + sendReaction(emoji.emojiId)}> + + + ))} + + )); +}; + +const EmojiContainer = styled('span', { + position: 'relative', + cursor: 'pointer', + width: '$16', + height: '$16', + p: '$4', + '&:hover': { + p: '7px', + bg: '$surface_brighter', + borderRadius: '$1', + }, +}); diff --git a/packages/roomkit-react/src/Prebuilt/components/Header/ParticipantList.jsx b/packages/roomkit-react/src/Prebuilt/components/Footer/ParticipantList.jsx similarity index 97% rename from packages/roomkit-react/src/Prebuilt/components/Header/ParticipantList.jsx rename to packages/roomkit-react/src/Prebuilt/components/Footer/ParticipantList.jsx index eaed314876..c528cc08d9 100644 --- a/packages/roomkit-react/src/Prebuilt/components/Header/ParticipantList.jsx +++ b/packages/roomkit-react/src/Prebuilt/components/Footer/ParticipantList.jsx @@ -21,11 +21,11 @@ import { SpeakerIcon, VerticalMenuIcon, } from '@100mslive/react-icons'; -import { Avatar, Box, Dropdown, Flex, Input, Slider, Text, textEllipsis } from '../../../'; +import { Avatar, Box, Dropdown, Flex, Input, Slider, Text, textEllipsis } from '../../..'; import IconButton from '../../IconButton'; import { ConnectionIndicator } from '../Connection/ConnectionIndicator'; +import { ParticipantFilter } from '../Header/ParticipantFilter'; import { RoleChangeModal } from '../RoleChangeModal'; -import { ParticipantFilter } from './ParticipantFilter'; import { useIsSidepaneTypeOpen, useSidepaneToggle } from '../AppData/useSidepane'; import { isInternalRole } from '../../common/utils'; import { SIDE_PANE_OPTIONS } from '../../common/constants'; @@ -320,8 +320,8 @@ export const ParticipantSearch = ({ onSearch, placeholder }) => { { event.stopPropagation(); diff --git a/packages/roomkit-react/src/Prebuilt/components/Footer/StreamingFooter.jsx b/packages/roomkit-react/src/Prebuilt/components/Footer/StreamingFooter.jsx index a1addf548f..83708e75c9 100644 --- a/packages/roomkit-react/src/Prebuilt/components/Footer/StreamingFooter.jsx +++ b/packages/roomkit-react/src/Prebuilt/components/Footer/StreamingFooter.jsx @@ -1,22 +1,23 @@ import React from 'react'; -import { Box, Flex, Footer as AppFooter } from '../../../'; +import { useMedia } from 'react-use'; +import { config as cssConfig, Footer as AppFooter } from '../../../'; import { AudioVideoToggle } from '../AudioVideoToggle'; import { EmojiReaction } from '../EmojiReaction'; -import { StreamActions } from '../Header/StreamActions'; import { LeaveRoom } from '../LeaveRoom'; -import MetaActions from '../MetaActions'; import { MoreSettings } from '../MoreSettings/MoreSettings'; -import { PIP } from '../PIP'; import { ScreenshareToggle } from '../ScreenShare'; import { ChatToggle } from './ChatToggle'; +import { ParticipantCount } from './ParticipantList'; export const StreamingFooter = () => { + const isMobile = useMedia(cssConfig.media.md); return ( @@ -25,46 +26,38 @@ export const StreamingFooter = () => { '@md': { w: 'unset', p: '0', + gap: '$10', }, }} > - + {isMobile ? : null} + - - - - - - - - - - - - - - + {isMobile ? ( + <> + + + + ) : ( + <> + + + + + )} - - + + ); diff --git a/packages/roomkit-react/src/Prebuilt/components/Header/ConferencingHeader.jsx b/packages/roomkit-react/src/Prebuilt/components/Header/ConferencingHeader.jsx index 6f68d1570f..a5cc671816 100644 --- a/packages/roomkit-react/src/Prebuilt/components/Header/ConferencingHeader.jsx +++ b/packages/roomkit-react/src/Prebuilt/components/Header/ConferencingHeader.jsx @@ -1,16 +1,42 @@ import React from 'react'; -import { Flex } from '../../../'; -import { SpeakerTag } from './HeaderComponents'; -import { ParticipantCount } from './ParticipantList'; +import { useMedia } from 'react-use'; +import { HMSRoomState, selectRoomState, useHMSStore } from '@100mslive/react-sdk'; +import { config as cssConfig, Flex, VerticalDivider } from '../../../'; +import { Logo, SpeakerTag } from './HeaderComponents'; import { StreamActions } from './StreamActions'; +import { AudioOutputActions, CamaraFlipActions } from './common'; export const ConferencingHeader = () => { + const roomState = useHMSStore(selectRoomState); + const isMobile = useMedia(cssConfig.media.md); + const isPreview = roomState === HMSRoomState.Preview; + + if (isPreview) { + return ( + + + + + + + + + ); + } return ( + + - { }} > - + {isMobile && ( + <> + + {' '} + + )} ); diff --git a/packages/roomkit-react/src/Prebuilt/components/Header/StreamActions.jsx b/packages/roomkit-react/src/Prebuilt/components/Header/StreamActions.jsx index 3813f9ac7f..a062b0d6ec 100644 --- a/packages/roomkit-react/src/Prebuilt/components/Header/StreamActions.jsx +++ b/packages/roomkit-react/src/Prebuilt/components/Header/StreamActions.jsx @@ -1,7 +1,8 @@ -import React, { Fragment, useState } from 'react'; +import React, { useCallback, useEffect, useRef, useState } from 'react'; import { useMedia } from 'react-use'; import { HMSRoomState, + selectHLSState, selectIsConnectedToRoom, selectPermissions, selectRoomState, @@ -9,30 +10,69 @@ import { useHMSStore, useRecordingStreaming, } from '@100mslive/react-sdk'; -import { RecordIcon, WrenchIcon } from '@100mslive/react-icons'; -import { Box, Button, config as cssConfig, Flex, Loading, Popover, Text, Tooltip } from '../../../'; -import GoLiveButton from '../GoLiveButton'; +import { AlertTriangleIcon, CrossIcon, RecordIcon } from '@100mslive/react-icons'; +import { Box, Button, config as cssConfig, Flex, HorizontalDivider, Loading, Popover, Text, Tooltip } from '../../../'; +import { Sheet } from '../../../Sheet'; import { ResolutionInput } from '../Streaming/ResolutionInput'; import { getResolution } from '../Streaming/RTMPStreaming'; import { ToastManager } from '../Toast/ToastManager'; import { AdditionalRoomState, getRecordingText } from './AdditionalRoomState'; -import { useSidepaneToggle } from '../AppData/useSidepane'; import { useSetAppDataByKey } from '../AppData/useUISettings'; -import { APP_DATA, RTMP_RECORD_DEFAULT_RESOLUTION, SIDE_PANE_OPTIONS } from '../../common/constants'; +import { formatTime } from '../../common/utils'; +import { APP_DATA, RTMP_RECORD_DEFAULT_RESOLUTION } from '../../common/constants'; export const LiveStatus = () => { const { isHLSRunning, isRTMPRunning } = useRecordingStreaming(); + const hlsState = useHMSStore(selectHLSState); + const isMobile = useMedia(cssConfig.media.md); + const intervalRef = useRef(null); + + const [liveTime, setLiveTime] = useState(0); + + const startTimer = useCallback(() => { + intervalRef.current = setInterval(() => { + if (hlsState?.running) { + setLiveTime(Date.now() - hlsState?.variants[0]?.startedAt.getTime()); + } + }, 1000); + }, [hlsState?.running, hlsState?.variants]); + + useEffect(() => { + if (hlsState?.running && !isMobile) { + startTimer(); + } + if (!hlsState?.running && intervalRef.current) { + clearInterval(intervalRef.current); + } + return () => { + if (intervalRef.current) { + clearInterval(intervalRef.current); + } + }; + }, [hlsState.running, isMobile, startTimer]); + if (!isHLSRunning && !isRTMPRunning) { return null; } return ( - - - - Live - -  with {isHLSRunning ? 'HLS' : 'RTMP'} + + + {isMobile ? ( + + Live + ) : ( + LIVE + )} + + {hlsState?.variants?.length > 0 ? formatTime(liveTime) : ''} ); @@ -41,6 +81,7 @@ export const LiveStatus = () => { export const RecordingStatus = () => { const { isBrowserRecordingOn, isServerRecordingOn, isHLSRecordingOn, isRecordingOn } = useRecordingStreaming(); const permissions = useHMSStore(selectPermissions); + const isMobile = useMedia(cssConfig.media.md); if ( !isRecordingOn || @@ -50,8 +91,10 @@ export const RecordingStatus = () => { value => !!value, ) ) { - return null; + // show recording icon in mobile without popover + if (!(isMobile && isRecordingOn)) return null; } + return ( { isHLSRecordingOn, })} > - - + ); }; -const EndStream = () => { - const toggleStreaming = useSidepaneToggle(SIDE_PANE_OPTIONS.STREAMING); - - return ( - - ); -}; - const StartRecording = () => { const permissions = useHMSStore(selectPermissions); const [resolution, setResolution] = useState(RTMP_RECORD_DEFAULT_RESOLUTION); @@ -192,24 +225,57 @@ const StartRecording = () => { ); }; +/** + * @description only start recording button will be shown. + */ export const StreamActions = () => { const isConnected = useHMSStore(selectIsConnectedToRoom); - const permissions = useHMSStore(selectPermissions); const isMobile = useMedia(cssConfig.media.md); - const { isStreamingOn } = useRecordingStreaming(); const roomState = useHMSStore(selectRoomState); return ( - - {roomState !== HMSRoomState.Preview ? : null} + + {roomState !== HMSRoomState.Preview ? : null} {isConnected && !isMobile ? : null} - {isConnected && (permissions.hlsStreaming || permissions.rtmpStreaming) && ( - {isStreamingOn ? : } - )} ); }; + +export const StopRecordingInSheet = ({ onStopRecording, onClose }) => { + return ( + + + + + + + Stop Recording + + + + + + + + + + Are you sure you want to stop recording? You can’t undo this action. + + + + + + ); +}; diff --git a/packages/roomkit-react/src/Prebuilt/components/Header/StreamingHeader.jsx b/packages/roomkit-react/src/Prebuilt/components/Header/StreamingHeader.jsx index 71dd67fb2c..de16f09399 100644 --- a/packages/roomkit-react/src/Prebuilt/components/Header/StreamingHeader.jsx +++ b/packages/roomkit-react/src/Prebuilt/components/Header/StreamingHeader.jsx @@ -2,10 +2,10 @@ import React from 'react'; import { useMedia } from 'react-use'; import { config as cssConfig, Flex } from '../../../'; import { EmojiReaction } from '../EmojiReaction'; +import { ParticipantCount } from '../Footer/ParticipantList'; import { LeaveRoom } from '../LeaveRoom'; import MetaActions from '../MetaActions'; import { SpeakerTag } from './HeaderComponents'; -import { ParticipantCount } from './ParticipantList'; import { LiveStatus, RecordingStatus, StreamActions } from './StreamActions'; export const StreamingHeader = () => { diff --git a/packages/roomkit-react/src/Prebuilt/components/Header/common.jsx b/packages/roomkit-react/src/Prebuilt/components/Header/common.jsx new file mode 100644 index 0000000000..bf2c575c0d --- /dev/null +++ b/packages/roomkit-react/src/Prebuilt/components/Header/common.jsx @@ -0,0 +1,164 @@ +import React from 'react'; +import { + DeviceType, + selectIsLocalVideoEnabled, + selectLocalVideoTrackID, + selectVideoTrackByID, + useDevices, + useHMSActions, + useHMSStore, +} from '@100mslive/react-sdk'; +import { CameraFlipIcon, CheckIcon, CrossIcon, SpeakerIcon } from '@100mslive/react-icons'; +import { HorizontalDivider } from '../../../Divider'; +import { Label } from '../../../Label'; +import { Box, Flex } from '../../../Layout'; +import { Sheet } from '../../../Sheet'; +import { Text } from '../../../Text'; +import IconButton from '../../IconButton'; +import { ToastManager } from '../Toast/ToastManager'; + +export const CamaraFlipActions = () => { + const actions = useHMSActions(); + const { allDevices } = useDevices(); + const { videoInput } = allDevices; + const isVideoOn = useHMSStore(selectIsLocalVideoEnabled); + + const videoTrackId = useHMSStore(selectLocalVideoTrackID); + const localVideoTrack = useHMSStore(selectVideoTrackByID(videoTrackId)); + + return ( + + { + try { + await actions.switchCamera(); + } catch (e) { + ToastManager.addToast({ + title: `Error while flipping camera ${e.message || ''}`, + variant: 'error', + }); + } + }} + > + + + + ); +}; + +export const AudioOutputActions = () => { + const { allDevices, selectedDeviceIDs, updateDevice } = useDevices(); + const { audioOutput } = allDevices; + // don't show speaker selector where the API is not supported, and use + // a generic word("Audio") for Mic. In some cases(Chrome Android for e.g.) this changes both mic and speaker keeping them in sync. + const shouldShowAudioOutput = 'setSinkId' in HTMLMediaElement.prototype; + + /** + * Chromium browsers return an audioOutput with empty label when no permissions are given + */ + const audioOutputFiltered = audioOutput?.filter(item => !!item.label) ?? []; + if (!shouldShowAudioOutput || !audioOutputFiltered?.length > 0) { + return null; + } + return ( + { + try { + await updateDevice({ + deviceId, + deviceType: DeviceType.audioOutput, + }); + } catch (e) { + ToastManager.addToast({ + title: `Error while changing audio output ${e.message || ''}`, + variant: 'error', + }); + } + }} + > + + + + + + + ); +}; + +const AudioOutputSelectionSheet = ({ outputDevices, outputSelected, onChange, children }) => { + return ( + + {children} + + + + + Audio Output + + + + + + + + + + + {outputDevices.map(audioDevice => { + return ( + onChange(audioDevice.deviceId)} + /> + ); + })} + + + + ); +}; + +const SelectWithLabel = ({ label, icon = <>, checked, id, onChange }) => { + return ( + + + {checked && } + + ); +}; diff --git a/packages/roomkit-react/src/Prebuilt/components/LeaveCard.jsx b/packages/roomkit-react/src/Prebuilt/components/LeaveCard.jsx new file mode 100644 index 0000000000..32beacc46e --- /dev/null +++ b/packages/roomkit-react/src/Prebuilt/components/LeaveCard.jsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { Box, Flex } from '../../Layout'; +import { Text } from '../../Text'; + +export const LeaveCard = ({ icon, title, subtitle, onClick, bg, titleColor, subtitleColor, css = {} }) => { + return ( + + {icon} + + + {title} + + + {subtitle} + + + + ); +}; diff --git a/packages/roomkit-react/src/Prebuilt/components/LeaveRoom.jsx b/packages/roomkit-react/src/Prebuilt/components/LeaveRoom.jsx index c7d2c508ce..d851cf3f5c 100644 --- a/packages/roomkit-react/src/Prebuilt/components/LeaveRoom.jsx +++ b/packages/roomkit-react/src/Prebuilt/components/LeaveRoom.jsx @@ -1,33 +1,24 @@ -import React, { Fragment, useState } from 'react'; +import React from 'react'; import { useParams } from 'react-router-dom'; +import { useMedia } from 'react-use'; import { selectIsConnectedToRoom, selectPermissions, useHMSActions, useHMSStore } from '@100mslive/react-sdk'; -import { AlertTriangleIcon, ExitIcon, HangUpIcon, VerticalMenuIcon } from '@100mslive/react-icons'; +import { DesktopLeaveRoom } from './MoreSettings/SplitComponents/DesktopLeaveRoom'; +import { MwebLeaveRoom } from './MoreSettings/SplitComponents/MwebLeaveRoom'; import { ToastManager } from './Toast/ToastManager'; -import { Button } from '../../Button'; -import { Dropdown } from '../../Dropdown'; import { IconButton } from '../../IconButton'; -import { Box, Flex } from '../../Layout'; -import { Dialog } from '../../Modal'; -import { Text } from '../../Text'; -import { styled } from '../../Theme'; -import { Tooltip } from '../../Tooltip'; +import { config as cssConfig, styled } from '../../Theme'; import { useHMSPrebuiltContext } from '../AppContext'; -import { DialogCheckbox, DialogContent, DialogRow } from '../primitives/DialogContent'; -import { useDropdownList } from './hooks/useDropdownList'; import { useNavigation } from './hooks/useNavigation'; -import { isStreamingKit } from '../common/utils'; export const LeaveRoom = () => { const navigate = useNavigation(); const params = useParams(); - const [open, setOpen] = useState(false); - const [showEndRoomModal, setShowEndRoomModal] = useState(false); - const [lockRoom, setLockRoom] = useState(false); const isConnected = useHMSStore(selectIsConnectedToRoom); const permissions = useHMSStore(selectPermissions); + const isMobile = useMedia(cssConfig.media.md); + const hmsActions = useHMSActions(); const { showLeave, onLeave } = useHMSPrebuiltContext(); - useDropdownList({ open, name: 'LeaveRoom' }); const redirectToLeavePage = () => { if (showLeave) { @@ -47,129 +38,22 @@ export const LeaveRoom = () => { }; const endRoom = () => { - hmsActions.endRoom(lockRoom, 'End Room'); + hmsActions.endRoom(false, 'End Room'); redirectToLeavePage(); }; - const isStreamKit = isStreamingKit(); if (!permissions || !isConnected) { return null; } - - return ( - - {permissions.endRoom ? ( - - - - {!isStreamKit ? ( - - - - ) : ( - - - - - - Leave Studio - - - )} - - - - - - - - - - { - setShowEndRoomModal(true); - }} - data-testid="end_room_btn" - > - - - - - - - End Room for All - - - Warning: You can’t undo this action - - - - - - - - - - - Leave {isStreamKit ? 'Studio' : 'Room'} - - You can always rejoin later - - - - - - - - ) : ( - - - - {isStreamKit ? ( - - - - ) : ( - - )} - - - - )} - - { - if (!value) { - setLockRoom(false); - } - setShowEndRoomModal(value); - }} - > - - - - - - - - + return isMobile ? ( + + ) : ( + ); }; @@ -180,7 +64,7 @@ const LeaveIconButton = styled(IconButton, { r: '$1', bg: '$alert_error_default', '&:not([disabled]):hover': { - bg: '$alert_error_default', + bg: '$alert_error_bright', }, '&:not([disabled]):active': { bg: '$alert_error_default', diff --git a/packages/roomkit-react/src/Prebuilt/components/MoreSettings/ActionTile.jsx b/packages/roomkit-react/src/Prebuilt/components/MoreSettings/ActionTile.jsx new file mode 100644 index 0000000000..962ac2b85a --- /dev/null +++ b/packages/roomkit-react/src/Prebuilt/components/MoreSettings/ActionTile.jsx @@ -0,0 +1,33 @@ +import React from 'react'; +import { Flex } from '../../../Layout'; +import { Text } from '../../../Text'; + +export const ActionTile = ({ icon, title, active, onClick, disabled = false, setOpenOptionsSheet }) => { + return ( + { + if (!disabled) { + onClick(); + setOpenOptionsSheet(false); + } + }} + css={{ + p: '$4 $2', + bg: active ? '$surface_bright' : '', + color: disabled ? '$on_surface_low' : '$on_surface_high', + gap: '$4', + r: '$1', + '&:hover': { + bg: '$surface_bright', + }, + }} + > + {icon} + + {title} + + + ); +}; diff --git a/packages/roomkit-react/src/Prebuilt/components/MoreSettings/ChangeNameContent.jsx b/packages/roomkit-react/src/Prebuilt/components/MoreSettings/ChangeNameContent.jsx new file mode 100644 index 0000000000..af1b3dc6ff --- /dev/null +++ b/packages/roomkit-react/src/Prebuilt/components/MoreSettings/ChangeNameContent.jsx @@ -0,0 +1,90 @@ +import React from 'react'; +import { ChevronLeftIcon, CrossIcon } from '@100mslive/react-icons'; +import { Button } from '../../../Button'; +import { Input } from '../../../Input'; +import { Box, Flex } from '../../../Layout'; +import { Text } from '../../../Text'; + +export const ChangeNameContent = ({ + changeName, + setCurrentName, + currentName, + localPeerName, + isMobile, + onExit, + onBackClick, +}) => { + return ( +
{ + e.preventDefault(); + await changeName(); + }} + > + + {isMobile ? : null} + Change Name + + + + + + { + setCurrentName(e.target.value); + }} + autoComplete="name" + required + data-testid="change_name_field" + /> + + + + {isMobile ? null : ( + + )} + + + +
+ ); +}; diff --git a/packages/roomkit-react/src/Prebuilt/components/MoreSettings/ChangeNameModal.jsx b/packages/roomkit-react/src/Prebuilt/components/MoreSettings/ChangeNameModal.jsx index d0e99e667b..bcdeb9eae3 100644 --- a/packages/roomkit-react/src/Prebuilt/components/MoreSettings/ChangeNameModal.jsx +++ b/packages/roomkit-react/src/Prebuilt/components/MoreSettings/ChangeNameModal.jsx @@ -1,14 +1,18 @@ import React, { useState } from 'react'; +import { useMedia } from 'react-use'; import { selectLocalPeerName, useHMSActions, useHMSStore } from '@100mslive/react-sdk'; -import { Box, Button, Dialog, Flex, Input, Text } from '../../../'; +import { config as cssConfig, Dialog } from '../../../'; +import { Sheet } from '../../../Sheet'; import { ToastManager } from '../Toast/ToastManager'; +import { ChangeNameContent } from './ChangeNameContent'; import { UserPreferencesKeys, useUserPreferences } from '../hooks/useUserPreferences'; -export const ChangeNameModal = ({ onOpenChange }) => { +export const ChangeNameModal = ({ onOpenChange, openParentSheet = null }) => { const [previewPreference, setPreviewPreference] = useUserPreferences(UserPreferencesKeys.PREVIEW); const hmsActions = useHMSActions(); const localPeerName = useHMSStore(selectLocalPeerName); const [currentName, setCurrentName] = useState(localPeerName); + const isMobile = useMedia(cssConfig.media.md); const changeName = async () => { const name = currentName.trim(); @@ -29,62 +33,35 @@ export const ChangeNameModal = ({ onOpenChange }) => { } }; + const props = { + changeName, + setCurrentName, + currentName, + localPeerName, + isMobile, + onExit: () => onOpenChange(false), + onBackClick: () => { + onOpenChange(false); + openParentSheet(); + }, + }; + + if (isMobile) { + return ( + + + + + + ); + } + return ( - - - Change Name - -
{ - e.preventDefault(); - await changeName(); - }} - > - - { - setCurrentName(e.target.value); - }} - autoComplete="name" - required - data-testid="change_name_field" - /> - - - - - - - - - - - - -
+ +
diff --git a/packages/roomkit-react/src/Prebuilt/components/MoreSettings/EmbedUrl.jsx b/packages/roomkit-react/src/Prebuilt/components/MoreSettings/EmbedUrl.jsx index 577a82edff..7e7ba13350 100644 --- a/packages/roomkit-react/src/Prebuilt/components/MoreSettings/EmbedUrl.jsx +++ b/packages/roomkit-react/src/Prebuilt/components/MoreSettings/EmbedUrl.jsx @@ -1,7 +1,6 @@ import React, { useState } from 'react'; -import { ViewIcon } from '@100mslive/react-icons'; -import { Button, Dialog, Dropdown, Text } from '../../../'; -import { DialogContent, DialogInput, DialogRow } from '../../primitives/DialogContent'; +import { LinkIcon } from '@100mslive/react-icons'; +import { Button, Dialog, Dropdown, Flex, Input, Text } from '../../../'; import { useSetAppDataByKey } from '../AppData/useUISettings'; import { APP_DATA } from '../../common/constants'; @@ -17,7 +16,7 @@ export const EmbedUrl = ({ setShowOpenUrl }) => { }} data-testid="embed_url_btn" > - + Embed URL @@ -29,78 +28,54 @@ export function EmbedUrlModal({ onOpenChange }) { const [embedConfig, setEmbedConfig] = useSetAppDataByKey(APP_DATA.embedConfig); const [url, setUrl] = useState(embedConfig?.url || ''); - const isAnythingEmbedded = !!embedConfig?.url; - const isModifying = isAnythingEmbedded && url && url !== embedConfig.url; - return ( - - - - - Embed a url and share with everyone in the room. Ensure that you're sharing the current tab when the prompt - opens. Note that not all websites support being embedded. + + + + + Embed URL + + + Ensure that you're sharing the current tab when the prompt opens. Note that not all websites support being + embedded. + + + URL - - - {isAnythingEmbedded ? ( - <> - - - - ) : ( - <> - - - - )} - - + setUrl(e.target.value)} + type="url" + /> + + + + + + ); } diff --git a/packages/roomkit-react/src/Prebuilt/components/MoreSettings/MoreSettings.jsx b/packages/roomkit-react/src/Prebuilt/components/MoreSettings/MoreSettings.jsx index 5eb7c4c588..6ab8112628 100644 --- a/packages/roomkit-react/src/Prebuilt/components/MoreSettings/MoreSettings.jsx +++ b/packages/roomkit-react/src/Prebuilt/components/MoreSettings/MoreSettings.jsx @@ -1,226 +1,10 @@ -import React, { Fragment, useState } from 'react'; +import React from 'react'; import { useMedia } from 'react-use'; -import Hls from 'hls.js'; -import { - selectAppData, - selectIsAllowedToPublish, - selectLocalPeerID, - selectLocalPeerRoleName, - selectPermissions, - useHMSActions, - useHMSStore, - useRecordingStreaming, -} from '@100mslive/react-sdk'; -import { - ChangeRoleIcon, - CheckIcon, - InfoIcon, - MicOffIcon, - PencilIcon, - RecordIcon, - SettingsIcon, - VerticalMenuIcon, -} from '@100mslive/react-icons'; -import { Box, Checkbox, config as cssConfig, Dropdown, Flex, Text, Tooltip } from '../../../'; -import IconButton from '../../IconButton'; -import { RoleChangeModal } from '../RoleChangeModal'; -import SettingsModal from '../Settings/SettingsModal'; -import StartRecording from '../Settings/StartRecording'; -import { StatsForNerds } from '../StatsForNerds'; -import { BulkRoleChangeModal } from './BulkRoleChangeModal'; -import { ChangeNameModal } from './ChangeNameModal'; -import { ChangeSelfRole } from './ChangeSelfRole'; -import { EmbedUrl, EmbedUrlModal } from './EmbedUrl'; -import { FullScreenItem } from './FullScreenItem'; -import { MuteAllModal } from './MuteAllModal'; -import { useDropdownList } from '../hooks/useDropdownList'; -import { useIsFeatureEnabled } from '../hooks/useFeatures'; -import { FeatureFlags } from '../../services/FeatureFlags'; -import { APP_DATA, FEATURE_LIST, isAndroid, isIOS, isMacOS } from '../../common/constants'; - -const isMobileOS = isAndroid || isIOS; - -const MODALS = { - CHANGE_NAME: 'changeName', - SELF_ROLE_CHANGE: 'selfRoleChange', - MORE_SETTINGS: 'moreSettings', - START_RECORDING: 'startRecording', - DEVICE_SETTINGS: 'deviceSettings', - STATS_FOR_NERDS: 'statsForNerds', - BULK_ROLE_CHANGE: 'bulkRoleChange', - MUTE_ALL: 'muteAll', - EMBED_URL: 'embedUrl', -}; +import { DesktopOptions } from './SplitComponents/DesktopOptions'; +import { MwebOptions } from './SplitComponents/MwebOptions'; +import { config as cssConfig } from '../../../'; export const MoreSettings = () => { - const permissions = useHMSStore(selectPermissions); - const isAllowedToPublish = useHMSStore(selectIsAllowedToPublish); - const localPeerId = useHMSStore(selectLocalPeerID); - const localPeerRole = useHMSStore(selectLocalPeerRoleName); - const hmsActions = useHMSActions(); - const enablHlsStats = useHMSStore(selectAppData(APP_DATA.hlsStats)); const isMobile = useMedia(cssConfig.media.md); - const { isBrowserRecordingOn } = useRecordingStreaming(); - const isChangeNameEnabled = useIsFeatureEnabled(FEATURE_LIST.CHANGE_NAME); - const isEmbedEnabled = useIsFeatureEnabled(FEATURE_LIST.EMBED_URL); - const isSFNEnabled = useIsFeatureEnabled(FEATURE_LIST.STARTS_FOR_NERDS); - const [openModals, setOpenModals] = useState(new Set()); - useDropdownList({ open: openModals.size > 0, name: 'MoreSettings' }); - - const updateState = (modalName, value) => { - setOpenModals(modals => { - const copy = new Set(modals); - if (value) { - copy.add(modalName); - } else { - copy.delete(modalName); - } - return copy; - }); - }; - - return ( - - updateState(MODALS.MORE_SETTINGS, value)} - > - - - - - - - - - - - - {isMobile && permissions?.browserRecording ? ( - <> - updateState(MODALS.START_RECORDING, true)}> - - - {isBrowserRecordingOn ? 'Stop' : 'Start'} Recording - - - - - ) : null} - {isChangeNameEnabled && ( - updateState(MODALS.CHANGE_NAME, true)} data-testid="change_name_btn"> - - - Change Name - - - )} - updateState(MODALS.SELF_ROLE_CHANGE, true)} /> - {permissions?.changeRole && ( - updateState(MODALS.BULK_ROLE_CHANGE, true)} - data-testid="bulk_role_change_btn" - > - - - Bulk Role Change - - - )} - - {isAllowedToPublish.screen && isEmbedEnabled && ( - updateState(MODALS.EMBED_URL, true)} /> - )} - {permissions.mute && ( - updateState(MODALS.MUTE_ALL, true)} data-testid="mute_all_btn"> - - - Mute All - - - )} - - updateState(MODALS.DEVICE_SETTINGS, true)} data-testid="device_settings_btn"> - - - Settings - - - {FeatureFlags.enableStatsForNerds && - isSFNEnabled && - (localPeerRole === 'hls-viewer' ? ( - Hls.isSupported() ? ( - hmsActions.setAppData(APP_DATA.hlsStats, !enablHlsStats)} - data-testid="hls_stats" - > - hmsActions.setAppData(APP_DATA.hlsStats, !enablHlsStats)} - > - - - - - - - Show HLS Stats - - {!isMobileOS ? ( - - {`${isMacOS ? '⌘' : 'ctrl'} + ]`} - - ) : null} - - - ) : null - ) : ( - updateState(MODALS.STATS_FOR_NERDS, true)} - data-testid="stats_for_nreds_btn" - > - - - Stats for Nerds - - - ))} - - - {openModals.has(MODALS.BULK_ROLE_CHANGE) && ( - updateState(MODALS.BULK_ROLE_CHANGE, value)} /> - )} - {openModals.has(MODALS.MUTE_ALL) && updateState(MODALS.MUTE_ALL, value)} />} - {openModals.has(MODALS.CHANGE_NAME) && ( - updateState(MODALS.CHANGE_NAME, value)} /> - )} - {openModals.has(MODALS.DEVICE_SETTINGS) && ( - updateState(MODALS.DEVICE_SETTINGS, value)} /> - )} - {FeatureFlags.enableStatsForNerds && openModals.has(MODALS.STATS_FOR_NERDS) && ( - updateState(MODALS.STATS_FOR_NERDS, value)} /> - )} - {openModals.has(MODALS.SELF_ROLE_CHANGE) && ( - updateState(MODALS.SELF_ROLE_CHANGE, value)} /> - )} - {openModals.has(MODALS.START_RECORDING) && ( - updateState(MODALS.START_RECORDING, value)} /> - )} - {openModals.has(MODALS.EMBED_URL) && ( - updateState(MODALS.EMBED_URL, value)} /> - )} - - ); + return isMobile ? : ; }; diff --git a/packages/roomkit-react/src/Prebuilt/components/MoreSettings/MuteAllContent.jsx b/packages/roomkit-react/src/Prebuilt/components/MoreSettings/MuteAllContent.jsx new file mode 100644 index 0000000000..d7af0a9080 --- /dev/null +++ b/packages/roomkit-react/src/Prebuilt/components/MoreSettings/MuteAllContent.jsx @@ -0,0 +1,61 @@ +import React from 'react'; +import { Button } from '../../../Button'; +import { Label } from '../../../Label'; +import { Flex } from '../../../Layout'; +import { RadioGroup } from '../../../RadioGroup'; +import { Text } from '../../../Text'; +import { DialogRow, DialogSelect } from '../../primitives/DialogContent'; + +export const MuteAllContent = props => { + const roles = props.roles || []; + return ( + <> + ({ label: role, value: role }))]} + selected={props.selectedRole} + keyField="value" + labelField="label" + onChange={props.setRole} + /> + + + + Track status + + + + + + + + + + + + + + + + + + + + ); +}; diff --git a/packages/roomkit-react/src/Prebuilt/components/MoreSettings/MuteAllModal.jsx b/packages/roomkit-react/src/Prebuilt/components/MoreSettings/MuteAllModal.jsx index c6b7e0252b..aca0d15517 100644 --- a/packages/roomkit-react/src/Prebuilt/components/MoreSettings/MuteAllModal.jsx +++ b/packages/roomkit-react/src/Prebuilt/components/MoreSettings/MuteAllModal.jsx @@ -1,8 +1,10 @@ import React, { useCallback, useState } from 'react'; import { useHMSActions } from '@100mslive/react-sdk'; import { MicOffIcon } from '@100mslive/react-icons'; -import { Button, Dialog, Flex, Label, RadioGroup, Text } from '../../../'; -import { DialogContent, DialogRow, DialogSelect } from '../../primitives/DialogContent'; +import { Dialog } from '../../../'; +import { Sheet } from '../../../Sheet'; +import { DialogContent } from '../../primitives/DialogContent'; +import { MuteAllContent } from './MuteAllContent'; import { useFilteredRoles } from '../../common/hooks'; const trackSourceOptions = [ @@ -17,7 +19,7 @@ const trackTypeOptions = [ { label: 'audio', value: 'audio' }, { label: 'video', value: 'video' }, ]; -export const MuteAllModal = ({ onOpenChange }) => { +export const MuteAllModal = ({ onOpenChange, isMobile = false }) => { const roles = useFilteredRoles(); const hmsActions = useHMSActions(); const [enabled, setEnabled] = useState(false); @@ -35,55 +37,36 @@ export const MuteAllModal = ({ onOpenChange }) => { onOpenChange(false); }, [selectedRole, enabled, trackType, selectedSource, hmsActions, onOpenChange]); + const props = { + muteAll, + roles, + enabled, + setEnabled, + trackType, + setTrackType, + selectedRole, + setRole, + selectedSource, + setSource, + trackSourceOptions, + trackTypeOptions, + isMobile, + }; + + if (isMobile) { + return ( + + + + + + ); + } + return ( - ({ label: role, value: role }))]} - selected={selectedRole} - keyField="value" - labelField="label" - onChange={setRole} - /> - - - - Track status - - - - - - - - - - - - - - - - - - + ); diff --git a/packages/roomkit-react/src/Prebuilt/components/MoreSettings/SplitComponents/DesktopLeaveRoom.jsx b/packages/roomkit-react/src/Prebuilt/components/MoreSettings/SplitComponents/DesktopLeaveRoom.jsx new file mode 100644 index 0000000000..66b29aaa50 --- /dev/null +++ b/packages/roomkit-react/src/Prebuilt/components/MoreSettings/SplitComponents/DesktopLeaveRoom.jsx @@ -0,0 +1,116 @@ +import React, { Fragment, useState } from 'react'; +import { selectIsConnectedToRoom, selectPermissions, useHMSStore } from '@100mslive/react-sdk'; +import { ExitIcon, HangUpIcon, StopIcon, VerticalMenuIcon } from '@100mslive/react-icons'; +import { Dropdown } from '../../../../Dropdown'; +import { Box, Flex } from '../../../../Layout'; +import { Dialog } from '../../../../Modal'; +import { Tooltip } from '../../../../Tooltip'; +import { EndSessionContent } from '../../EndSessionContent'; +import { LeaveCard } from '../../LeaveCard'; +import { useDropdownList } from '../../hooks/useDropdownList'; +import { useShowStreamingUI } from '../../../common/hooks'; + +export const DesktopLeaveRoom = ({ + menuTriggerButton: MenuTriggerButton, + leaveIconButton: LeaveIconButton, + leaveRoom, + endRoom, +}) => { + const [open, setOpen] = useState(false); + const [showEndRoomAlert, setShowEndRoomAlert] = useState(false); + const isConnected = useHMSStore(selectIsConnectedToRoom); + const permissions = useHMSStore(selectPermissions); + const showStreamingUI = useShowStreamingUI(); + useDropdownList({ open, name: 'LeaveRoom' }); + + if (!permissions || !isConnected) { + return null; + } + + return ( + + {permissions.endRoom ? ( + + + + + + + + + + + + + + + + + } + onClick={leaveRoom} + css={{ p: 0 }} + /> + + + } + onClick={() => { + setOpen(false); + setShowEndRoomAlert(true); + }} + css={{ p: 0 }} + /> + + + + + ) : ( + + + {showStreamingUI ? : } + + + )} + + + + + + + + + + + ); +}; diff --git a/packages/roomkit-react/src/Prebuilt/components/MoreSettings/SplitComponents/DesktopOptions.jsx b/packages/roomkit-react/src/Prebuilt/components/MoreSettings/SplitComponents/DesktopOptions.jsx new file mode 100644 index 0000000000..b809e9d08f --- /dev/null +++ b/packages/roomkit-react/src/Prebuilt/components/MoreSettings/SplitComponents/DesktopOptions.jsx @@ -0,0 +1,249 @@ +import React, { Fragment, useState } from 'react'; +import Hls from 'hls.js'; +import { + selectAppData, + selectIsAllowedToPublish, + selectLocalPeerID, + selectLocalPeerRoleName, + selectPermissions, + useHMSActions, + useHMSStore, +} from '@100mslive/react-sdk'; +import { + BrbIcon, + CheckIcon, + DragHandleIcon, + HandIcon, + InfoIcon, + MicOffIcon, + PencilIcon, + PipIcon, + SettingsIcon, +} from '@100mslive/react-icons'; +import { Checkbox, Dropdown, Flex, Text, Tooltip } from '../../../../'; +import IconButton from '../../../IconButton'; +import { PIP } from '../../PIP'; +import { RoleChangeModal } from '../../RoleChangeModal'; +import SettingsModal from '../../Settings/SettingsModal'; +import StartRecording from '../../Settings/StartRecording'; +import { StatsForNerds } from '../../StatsForNerds'; +import { BulkRoleChangeModal } from '.././BulkRoleChangeModal'; +import { ChangeNameModal } from '.././ChangeNameModal'; +import { ChangeSelfRole } from '.././ChangeSelfRole'; +import { EmbedUrl, EmbedUrlModal } from '.././EmbedUrl'; +import { FullScreenItem } from '.././FullScreenItem'; +import { MuteAllModal } from '.././MuteAllModal'; +import { useDropdownList } from '../../hooks/useDropdownList'; +import { useIsFeatureEnabled } from '../../hooks/useFeatures'; +import { useMyMetadata } from '../../hooks/useMetadata'; +import { FeatureFlags } from '../../../services/FeatureFlags'; +import { APP_DATA, FEATURE_LIST, isMacOS } from '../../../common/constants'; + +const MODALS = { + CHANGE_NAME: 'changeName', + SELF_ROLE_CHANGE: 'selfRoleChange', + MORE_SETTINGS: 'moreSettings', + START_RECORDING: 'startRecording', + DEVICE_SETTINGS: 'deviceSettings', + STATS_FOR_NERDS: 'statsForNerds', + BULK_ROLE_CHANGE: 'bulkRoleChange', + MUTE_ALL: 'muteAll', + EMBED_URL: 'embedUrl', +}; + +export const DesktopOptions = ({ showStreamingUI = false }) => { + const permissions = useHMSStore(selectPermissions); + const isAllowedToPublish = useHMSStore(selectIsAllowedToPublish); + const localPeerId = useHMSStore(selectLocalPeerID); + const localPeerRole = useHMSStore(selectLocalPeerRoleName); + const hmsActions = useHMSActions(); + const enablHlsStats = useHMSStore(selectAppData(APP_DATA.hlsStats)); + const isChangeNameEnabled = useIsFeatureEnabled(FEATURE_LIST.CHANGE_NAME); + const isEmbedEnabled = useIsFeatureEnabled(FEATURE_LIST.EMBED_URL); + const isSFNEnabled = useIsFeatureEnabled(FEATURE_LIST.STARTS_FOR_NERDS); + const [openModals, setOpenModals] = useState(new Set()); + const { isHandRaised, isBRBOn, toggleHandRaise, toggleBRB } = useMyMetadata(); + const isHandRaiseEnabled = useIsFeatureEnabled(FEATURE_LIST.HAND_RAISE); + const isBRBEnabled = useIsFeatureEnabled(FEATURE_LIST.BRB); + const isPIPEnabled = useIsFeatureEnabled(FEATURE_LIST.PICTURE_IN_PICTURE); + + useDropdownList({ open: openModals.size > 0, name: 'MoreSettings' }); + + const updateState = (modalName, value) => { + setOpenModals(modals => { + const copy = new Set(modals); + if (value) { + copy.add(modalName); + } else { + copy.delete(modalName); + } + return copy; + }); + }; + + return ( + + updateState(MODALS.MORE_SETTINGS, value)} + > + + + + + + + + + + {isHandRaiseEnabled && !showStreamingUI ? ( + + + + Raise Hand + + + {isHandRaised ? : null} + + + ) : null} + + {isBRBEnabled && !showStreamingUI ? ( + + + + Be Right Back + + + {isBRBOn ? : null} + + + ) : null} + + {(isBRBEnabled || isHandRaiseEnabled) && !showStreamingUI ? ( + + ) : null} + + {isPIPEnabled ? ( + + + + + Picture in picture mode + +
+ } + /> + + ) : null} + + {isChangeNameEnabled && ( + updateState(MODALS.CHANGE_NAME, true)} data-testid="change_name_btn"> + + + Change Name + + + )} + updateState(MODALS.SELF_ROLE_CHANGE, true)} /> + + {isAllowedToPublish.screen && isEmbedEnabled && ( + updateState(MODALS.EMBED_URL, true)} /> + )} + + {permissions.mute && ( + updateState(MODALS.MUTE_ALL, true)} data-testid="mute_all_btn"> + + + Mute All + + + )} + + + updateState(MODALS.DEVICE_SETTINGS, true)} data-testid="device_settings_btn"> + + + Settings + + + + {FeatureFlags.enableStatsForNerds && + isSFNEnabled && + (localPeerRole === 'hls-viewer' ? ( + Hls.isSupported() ? ( + hmsActions.setAppData(APP_DATA.hlsStats, !enablHlsStats)} + data-testid="hls_stats" + > + hmsActions.setAppData(APP_DATA.hlsStats, !enablHlsStats)} + > + + + + + + + Show HLS Stats + + + + {`${isMacOS ? '⌘' : 'ctrl'} + ]`} + + + + ) : null + ) : ( + updateState(MODALS.STATS_FOR_NERDS, true)} + data-testid="stats_for_nreds_btn" + > + + + Stats for Nerds + + + ))} + + + {openModals.has(MODALS.BULK_ROLE_CHANGE) && ( + updateState(MODALS.BULK_ROLE_CHANGE, value)} /> + )} + {openModals.has(MODALS.MUTE_ALL) && updateState(MODALS.MUTE_ALL, value)} />} + {openModals.has(MODALS.CHANGE_NAME) && ( + updateState(MODALS.CHANGE_NAME, value)} /> + )} + {openModals.has(MODALS.START_RECORDING) && ( + updateState(MODALS.START_RECORDING, value)} /> + )} + {openModals.has(MODALS.DEVICE_SETTINGS) && ( + updateState(MODALS.DEVICE_SETTINGS, value)} /> + )} + {FeatureFlags.enableStatsForNerds && openModals.has(MODALS.STATS_FOR_NERDS) && ( + updateState(MODALS.STATS_FOR_NERDS, value)} /> + )} + {openModals.has(MODALS.SELF_ROLE_CHANGE) && ( + updateState(MODALS.SELF_ROLE_CHANGE, value)} /> + )} + {openModals.has(MODALS.EMBED_URL) && ( + updateState(MODALS.EMBED_URL, value)} /> + )} + + ); +}; diff --git a/packages/roomkit-react/src/Prebuilt/components/MoreSettings/SplitComponents/MwebLeaveRoom.jsx b/packages/roomkit-react/src/Prebuilt/components/MoreSettings/SplitComponents/MwebLeaveRoom.jsx new file mode 100644 index 0000000000..c62c144d93 --- /dev/null +++ b/packages/roomkit-react/src/Prebuilt/components/MoreSettings/SplitComponents/MwebLeaveRoom.jsx @@ -0,0 +1,87 @@ +import React, { Fragment, useState } from 'react'; +import { selectIsConnectedToRoom, selectPermissions, useHMSStore } from '@100mslive/react-sdk'; +import { ExitIcon, HangUpIcon, StopIcon } from '@100mslive/react-icons'; +import { Box } from '../../../../Layout'; +import { Sheet } from '../../../../Sheet'; +import { Tooltip } from '../../../../Tooltip'; +import { EndSessionContent } from '../../EndSessionContent'; +import { LeaveCard } from '../../LeaveCard'; +import { useDropdownList } from '../../hooks/useDropdownList'; +import { useShowStreamingUI } from '../../../common/hooks'; + +export const MwebLeaveRoom = ({ leaveIconButton: LeaveIconButton, endRoom, leaveRoom }) => { + const [open, setOpen] = useState(false); + const [showEndRoomAlert, setShowEndRoomAlert] = useState(false); + const isConnected = useHMSStore(selectIsConnectedToRoom); + const permissions = useHMSStore(selectPermissions); + + const showStreamingUI = useShowStreamingUI(); + useDropdownList({ open, name: 'LeaveRoom' }); + + if (!permissions || !isConnected) { + return null; + } + + return ( + + {permissions.endRoom ? ( + + + + + {showStreamingUI ? : } + + + + + } + onClick={leaveRoom} + css={{ pt: 0, mt: '$10' }} + /> + } + onClick={() => { + setOpen(false); + setShowEndRoomAlert(true); + }} + /> + + + ) : ( + + + {showStreamingUI ? : } + + + )} + + + + + + + ); +}; diff --git a/packages/roomkit-react/src/Prebuilt/components/MoreSettings/SplitComponents/MwebOptions.jsx b/packages/roomkit-react/src/Prebuilt/components/MoreSettings/SplitComponents/MwebOptions.jsx new file mode 100644 index 0000000000..b98b6376e0 --- /dev/null +++ b/packages/roomkit-react/src/Prebuilt/components/MoreSettings/SplitComponents/MwebOptions.jsx @@ -0,0 +1,260 @@ +import React, { Suspense, useRef, useState } from 'react'; +import { useClickAway } from 'react-use'; +import { + selectIsConnectedToRoom, + selectIsLocalVideoEnabled, + selectPermissions, + useHMSActions, + useHMSStore, + useRecordingStreaming, +} from '@100mslive/react-sdk'; +import { + BrbIcon, + CrossIcon, + DragHandleIcon, + EmojiIcon, + HandIcon, + MicOffIcon, + PencilIcon, + RecordIcon, + SettingsIcon, +} from '@100mslive/react-icons'; +import { Box, Tooltip } from '../../../../'; +import { Sheet } from '../../../../Sheet'; +import IconButton from '../../../IconButton'; +import { EmojiCard } from '../../Footer/EmojiCard'; +import { StopRecordingInSheet } from '../../Header/StreamActions'; +import SettingsModal from '../../Settings/SettingsModal'; +import { ToastManager } from '../../Toast/ToastManager'; +import { ActionTile } from '.././ActionTile'; +import { ChangeNameModal } from '.././ChangeNameModal'; +import { MuteAllModal } from '.././MuteAllModal'; +import { useDropdownList } from '../../hooks/useDropdownList'; +import { useIsFeatureEnabled } from '../../hooks/useFeatures'; +import { useMyMetadata } from '../../hooks/useMetadata'; +import { FEATURE_LIST } from '../../../common/constants'; + +const VirtualBackground = React.lazy(() => import('../../../plugins/VirtualBackground/VirtualBackground')); + +const MODALS = { + CHANGE_NAME: 'changeName', + SELF_ROLE_CHANGE: 'selfRoleChange', + MORE_SETTINGS: 'moreSettings', + START_RECORDING: 'startRecording', + DEVICE_SETTINGS: 'deviceSettings', + STATS_FOR_NERDS: 'statsForNerds', + BULK_ROLE_CHANGE: 'bulkRoleChange', + MUTE_ALL: 'muteAll', + EMBED_URL: 'embedUrl', +}; + +export const MwebOptions = () => { + const hmsActions = useHMSActions(); + const permissions = useHMSStore(selectPermissions); + const isConnected = useHMSStore(selectIsConnectedToRoom); + const { isBrowserRecordingOn, isStreamingOn, isHLSRunning } = useRecordingStreaming(); + + const [openModals, setOpenModals] = useState(new Set()); + const { isHandRaised, isBRBOn, toggleHandRaise, toggleBRB } = useMyMetadata(); + const isHandRaiseEnabled = useIsFeatureEnabled(FEATURE_LIST.HAND_RAISE); + const isBRBEnabled = useIsFeatureEnabled(FEATURE_LIST.BRB); + + const [openOptionsSheet, setOpenOptionsSheet] = useState(false); + const [openSettingsSheet, setOpenSettingsSheet] = useState(false); + const [showEmojiCard, setShowEmojiCard] = useState(false); + const [showRecordingOn, setShowRecordingOn] = useState(false); + + const emojiCardRef = useRef(null); + const isVideoOn = useHMSStore(selectIsLocalVideoEnabled); + + useDropdownList({ open: openModals.size > 0, name: 'MoreSettings' }); + + const updateState = (modalName, value) => { + setOpenModals(modals => { + const copy = new Set(modals); + if (value) { + copy.add(modalName); + } else { + copy.delete(modalName); + } + return copy; + }); + }; + + useClickAway(emojiCardRef, () => setShowEmojiCard(false)); + + return ( + <> + + + + + + + + + + + Options + + + + + + + + {isHandRaiseEnabled ? ( + } + onClick={toggleHandRaise} + active={isHandRaised} + setOpenOptionsSheet={setOpenOptionsSheet} + /> + ) : null} + {isBRBEnabled ? ( + } + onClick={toggleBRB} + active={isBRBOn} + setOpenOptionsSheet={setOpenOptionsSheet} + /> + ) : null} + {permissions.mute ? ( + } + onClick={() => updateState(MODALS.MUTE_ALL, true)} + setOpenOptionsSheet={setOpenOptionsSheet} + /> + ) : null} + } + onClick={() => updateState(MODALS.CHANGE_NAME, true)} + setOpenOptionsSheet={setOpenOptionsSheet} + /> + {isVideoOn ? ( + + + + ) : null} + } + onClick={() => setShowEmojiCard(true)} + setOpenOptionsSheet={setOpenOptionsSheet} + /> + } onClick={() => setOpenSettingsSheet(true)} /> + {isConnected && permissions?.browserRecording && ( + } + onClick={async () => { + if (isBrowserRecordingOn || isStreamingOn) { + setShowRecordingOn(true); + } else { + // start recording + setOpenOptionsSheet(false); + try { + await hmsActions.startRTMPOrRecording({ + record: true, + }); + } catch (error) { + if (error.message.includes('stream already running')) { + ToastManager.addToast({ + title: 'Recording already running', + variant: 'error', + }); + } else { + ToastManager.addToast({ + title: error.message, + variant: 'error', + }); + } + } + } + }} + setOpenOptionsSheet={setOpenOptionsSheet} + /> + )} + + + + + {openModals.has(MODALS.MUTE_ALL) && ( + updateState(MODALS.MUTE_ALL, value)} isMobile /> + )} + {openModals.has(MODALS.CHANGE_NAME) && ( + updateState(MODALS.CHANGE_NAME, value)} + openParentSheet={() => setOpenOptionsSheet(true)} + /> + )} + + {showEmojiCard && ( + setShowEmojiCard(false)} + ref={emojiCardRef} + css={{ + maxWidth: '100%', + w: '100%', + position: 'absolute', + left: 0, + right: 0, + bottom: '$18', + bg: '$surface_default', + zIndex: '10', + p: '$8', + pb: 0, + r: '$1', + mx: '$4', + }} + > + + + )} + {showRecordingOn && ( + setShowRecordingOn(false)} + onStopRecording={async () => { + try { + await hmsActions.stopRTMPAndRecording(); + setShowRecordingOn(false); + } catch (error) { + ToastManager.addToast({ + title: error.message, + variant: 'error', + }); + } + }} + /> + )} + + ); +}; diff --git a/packages/roomkit-react/src/Prebuilt/components/PIP/PIPComponent.jsx b/packages/roomkit-react/src/Prebuilt/components/PIP/PIPComponent.jsx index 17c8c04ba7..3a6a7368cc 100644 --- a/packages/roomkit-react/src/Prebuilt/components/PIP/PIPComponent.jsx +++ b/packages/roomkit-react/src/Prebuilt/components/PIP/PIPComponent.jsx @@ -9,7 +9,7 @@ import { useHMSVanillaStore, } from '@100mslive/react-sdk'; import { PipIcon } from '@100mslive/react-icons'; -import { Tooltip } from '../../../'; +import { Flex, Tooltip } from '../../../'; import IconButton from '../../IconButton'; import { PictureInPicture } from './PIPManager'; import { MediaSession } from './SetupMediaSession'; @@ -20,7 +20,7 @@ import { DEFAULT_HLS_VIEWER_ROLE, FEATURE_LIST } from '../../common/constants'; * shows a button which when clicked shows some videos in PIP, clicking * again turns it off. */ -const PIPComponent = ({ peers, showLocalPeer }) => { +const PIPComponent = ({ peers, showLocalPeer, content = null }) => { const localPeerRole = useHMSStore(selectLocalPeerRoleName); const [isPipOn, setIsPipOn] = useState(PictureInPicture.isOn()); const hmsActions = useHMSActions(); @@ -48,11 +48,17 @@ const PIPComponent = ({ peers, showLocalPeer }) => { } return ( <> - - onPipToggle()} data-testid="pip_btn"> - - - + {content ? ( + onPipToggle()} data-testid="pip_btn"> + {content} + + ) : ( + + onPipToggle()} data-testid="pip_btn"> + + + + )} {isPipOn && } ); diff --git a/packages/roomkit-react/src/Prebuilt/components/PIP/index.jsx b/packages/roomkit-react/src/Prebuilt/components/PIP/index.jsx index f0a62b5396..e17018678c 100644 --- a/packages/roomkit-react/src/Prebuilt/components/PIP/index.jsx +++ b/packages/roomkit-react/src/Prebuilt/components/PIP/index.jsx @@ -2,10 +2,14 @@ import React from 'react'; import PIPComponent from './PIPComponent'; import { usePinnedTrack } from '../AppData/useUISettings'; -export const PIP = () => { +export const PIP = ({ content = null }) => { const pinnedTrack = usePinnedTrack(); return ( - + ); }; diff --git a/packages/roomkit-react/src/Prebuilt/components/Preview/PreviewForm.jsx b/packages/roomkit-react/src/Prebuilt/components/Preview/PreviewForm.jsx index 819ac92604..5efbc73f80 100644 --- a/packages/roomkit-react/src/Prebuilt/components/Preview/PreviewForm.jsx +++ b/packages/roomkit-react/src/Prebuilt/components/Preview/PreviewForm.jsx @@ -1,11 +1,11 @@ import React from 'react'; import { useMedia } from 'react-use'; -import { JoinForm_JoinBtnType } from '@100mslive/types-prebuilt/elements/join_form'; import { selectPermissions, useHMSStore, useRecordingStreaming } from '@100mslive/react-sdk'; import { RadioIcon } from '@100mslive/react-icons'; import { Button, config as cssConfig, Flex, Input, styled } from '../../..'; import { useRoomLayout } from '../../provider/roomLayoutProvider'; import { PreviewSettings } from './PreviewJoin'; +import { useShowStreamingUI } from '../../common/hooks'; const PreviewForm = ({ name, @@ -24,10 +24,8 @@ const PreviewForm = ({ const permissions = useHMSStore(selectPermissions); const layout = useRoomLayout(); const { join_form: joinForm = {} } = layout?.screens?.preview?.default?.elements || {}; - const showGoLive = - joinForm.join_btn_type === JoinForm_JoinBtnType.JOIN_BTN_TYPE_JOIN_AND_GO_LIVE && - !isHLSRunning && - permissions?.hlsStreaming; + const showStreamingUI = useShowStreamingUI(); + const showGoLive = showStreamingUI && !isHLSRunning && permissions?.hlsStreaming; return (
import('../../plugins/VirtualBackground/VirtualBackground')); @@ -44,8 +45,7 @@ const getParticipantChipContent = (peerCount = 0) => { if (peerCount === 0) { return 'You are the first to join'; } - const formatter = new Intl.NumberFormat('en', { notation: 'compact', maximumFractionDigits: 2 }); - const formattedNum = formatter.format(peerCount); + const formattedNum = getFormattedCount(peerCount); return `${formattedNum} other${parseInt(formattedNum) === 1 ? '' : 's'} in the session`; }; @@ -246,7 +246,6 @@ const PreviewControls = ({ hideSettings }) => { ); }; -// Bottom action sheet goes here, if isMobile export const PreviewSettings = React.memo(() => { const [open, setOpen] = useState(false); diff --git a/packages/roomkit-react/src/Prebuilt/components/ScreenShare.jsx b/packages/roomkit-react/src/Prebuilt/components/ScreenShare.jsx index 7d9d27f001..c6d4514383 100644 --- a/packages/roomkit-react/src/Prebuilt/components/ScreenShare.jsx +++ b/packages/roomkit-react/src/Prebuilt/components/ScreenShare.jsx @@ -21,7 +21,7 @@ export const ScreenshareToggle = ({ css = {} }) => { return ( - + { r: '$3', m: '0 auto', color: '$on_surface_high', - bg: '$surface_default', + bg: '$background_default', textAlign: 'center', }} > - + You are sharing your screen + + + + + + + Sheet Heading + + + + + + + + + + Body 2: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. Ut enim ad minim veniam,im venitetur adipiscing elit, sed do eiusmod tempor incididunt ut + labore et dolore magna aliqua. Ut enim ad minim veniam,im veni + +
+ + +
+
+ + +
+ + Body 2: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. Ut enim ad minim veniam,im venitetur adipiscing elit, sed do eiusmod tempor incididunt ut + labore et dolore magna aliqua. Ut enim ad minim veniam,im veni + +
+ + +
+
+ + +
+ + Body 2: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. Ut enim ad minim veniam,im venitetur adipiscing elit, sed do eiusmod tempor incididunt ut + labore et dolore magna aliqua. Ut enim ad minim veniam,im veni + +
+ + +
+
+ + +
+ + Body 2: Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et + dolore magna aliqua. Ut enim ad minim veniam,im venitetur adipiscing elit, sed do eiusmod tempor incididunt ut + labore et dolore magna aliqua. Ut enim ad minim veniam,im veni + +
+ + +
+
+ + +
+
+
+ +); + +export const Example = Template.bind({}); +Example.storyName = 'Sheet'; diff --git a/packages/roomkit-react/src/Sheet/Sheet.tsx b/packages/roomkit-react/src/Sheet/Sheet.tsx new file mode 100644 index 0000000000..57a35482d1 --- /dev/null +++ b/packages/roomkit-react/src/Sheet/Sheet.tsx @@ -0,0 +1,118 @@ +import React from 'react'; +import * as DialogPrimitive from '@radix-ui/react-dialog'; +import { CSS, VariantProps } from '@stitches/react'; +import { Dialog } from '../Modal'; +import { styled } from '../Theme'; +import { sheetFadeIn, sheetFadeOut, sheetSlideIn, sheetSlideOut } from '../utils'; + +const SheetRoot = styled(DialogPrimitive.Root, { + minHeight: '240px', + maxWidth: '100%', +}); +const SheetTrigger = styled(DialogPrimitive.Trigger, { + appearance: 'none !important', // Needed for safari it shows white overlay +}); + +const StyledOverlay = styled(Dialog.Overlay, { + top: 0, + right: 0, + bottom: 0, + left: 0, + + '&[data-state="open"]': { + animation: `${sheetFadeIn} 150ms cubic-bezier(0.22, 1, 0.36, 1)`, + }, + + '&[data-state="closed"]': { + animation: `${sheetFadeOut} 150ms cubic-bezier(0.22, 1, 0.36, 1)`, + }, +}); + +const StyledContent = styled(DialogPrimitive.Content, { + color: '$on_surface_medium', + backgroundColor: '$surface_default', + borderTopLeftRadius: '$3', + borderTopRightRadius: '$3', + boxShadow: '0 10px 20px rgba(0,0,0,0.19), 0 6px 6px rgba(0,0,0,0.23)', + position: 'fixed', + zIndex: 999, + top: 0, + right: 0, + left: 0, + bottom: 0, + maxHeight: '96%', + + // Among other things, prevents text alignment inconsistencies when dialog can't be centered in the viewport evenly. + // Affects animated and non-animated dialogs alike. + willChange: 'transform', + + '&:focus': { + outline: 'none', + }, + '@allowMotion': { + '&[data-state="open"]': { + animation: `${sheetSlideIn} 150ms cubic-bezier(0.22, 1, 0.36, 1)`, + }, + + '&[data-state="closed"]': { + animation: `${sheetSlideOut} 150ms cubic-bezier(0.22, 1, 0.36, 1)`, + }, + }, + + variants: { + side: { + top: { + $$transformValue: 'translate3d(0,-100%,0)', + bottom: 'auto', + }, + right: { + $$transformValue: 'translate3d(100%,0,0)', + right: 0, + }, + bottom: { + $$transformValue: 'translate3d(0,100%,0)', + bottom: 0, + top: 'auto', + }, + left: { + $$transformValue: 'translate3d(-100%,0,0)', + left: 0, + }, + }, + }, + + defaultVariants: { + side: 'bottom', + }, +}); + +type SheetContentVariants = VariantProps; +type DialogContentPrimitiveProps = React.ComponentProps; +type SheetContentProps = DialogContentPrimitiveProps & SheetContentVariants & { css?: CSS }; + +const SheetContent = React.forwardRef, SheetContentProps>( + ({ children, ...props }, forwardedRef) => ( + + + + {children} + + + ), +); +const SheetClose = Dialog.Close; +const SheetTitle = styled(DialogPrimitive.Title, { + margin: 0, +}); +const SheetDescription = Dialog.Description; +const SheetDefaultCloseIcon = Dialog.DefaultClose; + +export const Sheet = { + Root: SheetRoot, + Trigger: SheetTrigger, + Content: SheetContent, + Description: SheetDescription, + Title: SheetTitle, + Close: SheetClose, + DefaultClose: SheetDefaultCloseIcon, +}; diff --git a/packages/roomkit-react/src/Sheet/index.ts b/packages/roomkit-react/src/Sheet/index.ts new file mode 100644 index 0000000000..3a9dafd2c3 --- /dev/null +++ b/packages/roomkit-react/src/Sheet/index.ts @@ -0,0 +1 @@ +export { Sheet } from './Sheet'; diff --git a/packages/roomkit-react/src/utils/animations.ts b/packages/roomkit-react/src/utils/animations.ts index 977ab0f591..0ed9484084 100644 --- a/packages/roomkit-react/src/utils/animations.ts +++ b/packages/roomkit-react/src/utils/animations.ts @@ -34,6 +34,24 @@ export const slideRightAndFade = (start = '-2px') => '100%': { opacity: 1, transform: 'translateX(0)' }, }); +export const sheetSlideIn = keyframes({ + from: { transform: '$$transformValue' }, + to: { transform: 'translate3d(0,0,0)' }, +}); + +export const sheetSlideOut = keyframes({ + from: { transform: 'translate3d(0,0,0)' }, + to: { transform: '$$transformValue' }, +}); +export const sheetFadeIn = keyframes({ + from: { opacity: '0' }, + to: { opacity: '1' }, +}); + +export const sheetFadeOut = keyframes({ + from: { opacity: '1' }, + to: { opacity: '0' }, +}); export const slideDownAndFade = (start = '-2px') => keyframes({ '0%': { opacity: 0, transform: `translateY(${start})` },