diff --git a/packages/react-native-sdk/src/components/Call/CallContent/CallContent.tsx b/packages/react-native-sdk/src/components/Call/CallContent/CallContent.tsx index 0ec66f8df2..7aaadc3b34 100644 --- a/packages/react-native-sdk/src/components/Call/CallContent/CallContent.tsx +++ b/packages/react-native-sdk/src/components/Call/CallContent/CallContent.tsx @@ -33,6 +33,10 @@ import { ScreenShareOverlayProps, } from '../../utility/ScreenShareOverlay'; import RTCViewPipIOS from './RTCViewPipIOS'; +import { + CallParticipantsFullscreen, + CallParticipantsFullscreenProps, +} from '../CallLayout/CallParticipantsFullscreen'; export type StreamReactionType = StreamReaction & { icon: string; @@ -68,9 +72,9 @@ export type CallContentProps = Pick< > & CallContentComponentProps & { /** - * This switches the participant's layout between the grid and the spotlight mode. + * This switches the participant's layout between the grid, spotlight and fullscreen mode. */ - layout?: 'grid' | 'spotlight'; + layout?: 'grid' | 'spotlight' | 'fullscreen'; /** * Reactions that are to be supported in the call */ @@ -116,12 +120,8 @@ export const CallContent = ({ const { theme: { callContent }, } = useTheme(); - const { - useCallSettings, - useHasOngoingScreenShare, - useRemoteParticipants, - useLocalParticipant, - } = useCallStateHooks(); + const { useCallSettings, useRemoteParticipants, useLocalParticipant } = + useCallStateHooks(); useAutoEnterPiPEffect(disablePictureInPicture); @@ -132,14 +132,9 @@ export const CallContent = ({ const remoteParticipants = useDebouncedValue(_remoteParticipants, 300); // we debounce the remote participants to avoid unnecessary rerenders that happen when participant tracks are all subscribed simultaneously const localParticipant = useLocalParticipant(); const isInPiPMode = useIsInPiPMode(disablePictureInPicture); - const hasScreenShare = useHasOngoingScreenShare(); - const showSpotlightLayout = hasScreenShare || layout === 'spotlight'; - - const showFloatingView = - !showSpotlightLayout && - !isInPiPMode && - remoteParticipants.length > 0 && - remoteParticipants.length < 3; + const isFullScreen = layout === 'fullscreen'; + const showFloatingView = isFullScreen && remoteParticipants.length === 1; + const isRemoteParticipantInFloatingView = showFloatingView && showRemoteParticipantInFloatingView && @@ -174,6 +169,13 @@ export const CallContent = ({ const callParticipantsGridProps: CallParticipantsGridProps = { ...participantViewProps, landscape, + ParticipantView, + CallParticipantsList, + supportedReactions, + }; + + const callParticipantsFullscreenProps: CallParticipantsFullscreenProps = { + ...participantViewProps, showLocalParticipant: isRemoteParticipantInFloatingView, ParticipantView, CallParticipantsList, @@ -189,6 +191,21 @@ export const CallContent = ({ supportedReactions, }; + const renderCallParticipants = (selectedLayout: string) => { + switch (selectedLayout) { + case 'fullscreen': + return ( + + ); + case 'spotlight': + return ( + + ); + default: + return ; + } + }; + return ( <> {!disablePictureInPicture && ( @@ -197,7 +214,7 @@ export const CallContent = ({ /> )} - + {!isInPiPMode && CallTopView && ( )} @@ -220,11 +237,7 @@ export const CallContent = ({ /> )} - {showSpotlightLayout ? ( - - ) : ( - - )} + {renderCallParticipants(layout)} {!isInPiPMode && CallControls && ( diff --git a/packages/react-native-sdk/src/components/Call/CallControls/ToggleCameraFaceButton.tsx b/packages/react-native-sdk/src/components/Call/CallControls/ToggleCameraFaceButton.tsx index ac27479f69..10e85a0e0a 100644 --- a/packages/react-native-sdk/src/components/Call/CallControls/ToggleCameraFaceButton.tsx +++ b/packages/react-native-sdk/src/components/Call/CallControls/ToggleCameraFaceButton.tsx @@ -28,7 +28,7 @@ export const ToggleCameraFaceButton = ({ const isVideoEnabledInCall = callSettings?.video.enabled; const { - theme: { colors, toggleCameraFaceButton, defaults }, + theme: { colors, toggleCameraFaceButton, variants }, } = useTheme(); const onPress = async () => { if (onPressHandler) { @@ -54,7 +54,7 @@ export const ToggleCameraFaceButton = ({ > & + Pick & { + /** + * Boolean to decide if local participant will be visible in the grid when there is 1:1 call. + */ + showLocalParticipant?: boolean; + }; + +/** + * Component used to display a participant in fullscreen mode. + */ +export const CallParticipantsFullscreen = ({ + CallParticipantsList = DefaultCallParticipantsList, + ParticipantLabel, + ParticipantNetworkQualityIndicator, + ParticipantReaction, + ParticipantVideoFallback, + ParticipantView, + VideoRenderer, + supportedReactions, + showLocalParticipant, +}: CallParticipantsFullscreenProps) => { + const { + theme: { colors, callParticipantsFullscreen }, + } = useTheme(); + const { useRemoteParticipants, useLocalParticipant } = useCallStateHooks(); + const remoteParticipants = useDebouncedValue(useRemoteParticipants(), 300); + const localParticipant = useLocalParticipant(); + + let participants = + showLocalParticipant && localParticipant + ? [localParticipant] + : remoteParticipants; + + if (remoteParticipants.length === 0 && localParticipant) { + participants = [localParticipant]; + } + + const participantViewProps: CallParticipantsListComponentProps = { + ParticipantView, + ParticipantLabel, + ParticipantNetworkQualityIndicator, + ParticipantReaction, + ParticipantVideoFallback, + VideoRenderer, + }; + + return ( + + {CallParticipantsList && ( + + )} + + ); +}; + +const styles = StyleSheet.create({ + container: { flex: 1 }, +}); diff --git a/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsGrid.tsx b/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsGrid.tsx index d71bb8c128..ea9ca6e627 100644 --- a/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsGrid.tsx +++ b/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsGrid.tsx @@ -12,6 +12,7 @@ import { CallContentProps } from '../CallContent'; import { ParticipantViewComponentProps } from '../../Participant'; import { useIsInPiPMode } from '../../../hooks/useIsInPiPMode'; import { StreamVideoParticipant } from '@stream-io/video-client'; +// import { generateMockParticipants } from '.'; /** * Props for the CallParticipantsGrid component. @@ -22,10 +23,6 @@ export type CallParticipantsGridProps = ParticipantViewComponentProps & 'supportedReactions' | 'CallParticipantsList' | 'disablePictureInPicture' > & Pick & { - /** - * Boolean to decide if local participant will be visible in the grid when there is 1:1 call. - */ - showLocalParticipant?: boolean; /** * Check if device is in landscape mode. * This will apply the landscape mode styles to the component. @@ -44,7 +41,6 @@ export const CallParticipantsGrid = ({ ParticipantVideoFallback, ParticipantView, VideoRenderer, - showLocalParticipant = false, supportedReactions, landscape, disablePictureInPicture, @@ -64,19 +60,11 @@ export const CallParticipantsGrid = ({ flexDirection: landscape ? 'row' : 'column', }; - const isInPiPMode = useIsInPiPMode(disablePictureInPicture); - - const showFloatingView = - !isInPiPMode && - remoteParticipants.length > 0 && - remoteParticipants.length < 3; - - let participants = showFloatingView - ? showLocalParticipant && localParticipant - ? [localParticipant] - : remoteParticipants - : allParticipants; + let participants = allParticipants; + // console.log('🚀 ~ participants:', participants); + // let participants = generateMockParticipants(9); + const isInPiPMode = useIsInPiPMode(disablePictureInPicture); if (isInPiPMode) { participants = remoteParticipants.length > 0 diff --git a/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsSpotlight.tsx b/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsSpotlight.tsx index e58164a908..9f13d2fb17 100644 --- a/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsSpotlight.tsx +++ b/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsSpotlight.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { hasScreenShare, speakerLayoutSortPreset, @@ -18,6 +18,7 @@ import { import { useTheme } from '../../../contexts/ThemeContext'; import { CallContentProps } from '../CallContent'; import { useIsInPiPMode } from '../../../hooks/useIsInPiPMode'; +// import { generateMockParticipants } from '.'; /** * Props for the CallParticipantsSpotlight component. @@ -56,17 +57,19 @@ export const CallParticipantsSpotlight = ({ disablePictureInPicture, }: CallParticipantsSpotlightProps) => { const { - theme: { colors, callParticipantsSpotlight }, + theme: { callParticipantsSpotlight, variants }, } = useTheme(); + const styles = useStyles(landscape); const { useParticipants } = useCallStateHooks(); const _allParticipants = useParticipants({ sortBy: speakerLayoutSortPreset, }); - const allParticipants = useDebouncedValue(_allParticipants, 300); // we debounce the participants to avoid unnecessary rerenders that happen when participant tracks are all subscribed simultaneously + let allParticipants = useDebouncedValue(_allParticipants, 300); // we debounce the participants to avoid unnecessary rerenders that happen when participant tracks are all subscribed simultaneously + // allParticipants = generateMockParticipants(10); // for testing const [participantInSpotlight, ...otherParticipants] = allParticipants; const isScreenShareOnSpotlight = participantInSpotlight && hasScreenShare(participantInSpotlight); - const isUserAloneInCall = _allParticipants?.length === 1; + const isUserAloneInCall = allParticipants?.length === 1; const isInPiP = useIsInPiPMode(disablePictureInPicture); @@ -88,7 +91,7 @@ export const CallParticipantsSpotlight = ({ }; const spotlightContainerLandscapeStyles: ViewStyle = { - marginHorizontal: landscape ? 0 : 8, + marginHorizontal: landscape ? 0 : variants.spacingSizes.xs, }; return ( @@ -97,15 +100,12 @@ export const CallParticipantsSpotlight = ({ style={[ styles.container, landscapeStyles, - { - backgroundColor: colors.background2, - }, callParticipantsSpotlight.container, ]} > {participantInSpotlight && ParticipantView && - (participantInSpotlight.isLocalParticipant && ScreenShareOverlay ? ( + (isScreenShareOnSpotlight && ScreenShareOverlay ? ( ) : ( { + const { theme } = useTheme(); + return useMemo( + () => + StyleSheet.create({ + container: { + flex: 1, + padding: theme.variants.spacingSizes.xs, + backgroundColor: theme.colors.sheetPrimary, + }, + fullScreenSpotlightContainer: { + flex: 1, + }, + spotlightContainer: { + flex: landscape ? 3 : 4, + overflow: 'hidden', + borderRadius: theme.variants.borderRadiusSizes.md, + marginHorizontal: theme.variants.spacingSizes.xs, + }, + callParticipantsListContainer: { + flex: 1, + flexDirection: 'row', + backgroundColor: theme.colors.sheetPrimary, + marginLeft: landscape ? theme.variants.spacingSizes.sm : 0, + }, + }), + [theme, landscape] + ); +}; diff --git a/packages/react-native-sdk/src/components/Call/CallLayout/index.ts b/packages/react-native-sdk/src/components/Call/CallLayout/index.ts index 7a8274996f..abcb8fec49 100644 --- a/packages/react-native-sdk/src/components/Call/CallLayout/index.ts +++ b/packages/react-native-sdk/src/components/Call/CallLayout/index.ts @@ -1,2 +1,221 @@ export * from './CallParticipantsGrid'; export * from './CallParticipantsSpotlight'; + +// TODO: before merge remove this +export const generateMockParticipants = (count: number) => { + const mockParticipants = [ + { + audioLevel: 0, + connectionQuality: 3, + custom: { fields: {} }, + image: 'https://getstream.io/random_png/?id=john&name=john', + isDominantSpeaker: false, + isLocalParticipant: false, + isSpeaking: false, + joinedAt: { nanos: 361467495, seconds: '1729747222' }, + name: 'john', + publishedTracks: [], + reaction: undefined, + roles: ['user'], + sessionId: 'dd179c0a-2b5d-41b7-be4b-fa8758f92d5a', + trackLookupPrefix: '9c7a05feecd46070', + userId: 'john', + viewportVisibilityState: { + screenShareTrack: 'UNKNOWN', + videoTrack: 'VISIBLE', + }, + }, + { + audioLevel: 0, + connectionQuality: 3, + custom: { fields: {} }, + image: 'https://getstream.io/random_png/?id=Marko&name=Marko', + isDominantSpeaker: false, + isLocalParticipant: false, + isSpeaking: false, + joinedAt: { nanos: 227903648, seconds: '1729747225' }, + name: 'Marko', + publishedTracks: [], + reaction: undefined, + roles: ['user'], + sessionId: '05a4a2da-3760-4c01-b193-ede42027a3cw', + trackLookupPrefix: 'f055633e656610db', + userId: 'Marko', + viewportVisibilityState: { + screenShareTrack: 'UNKNOWN', + videoTrack: 'VISIBLE', + }, + }, + { + audioLevel: 0, + connectionQuality: 3, + custom: { fields: {} }, + image: 'https://getstream.io/random_png/?id=kristian&name=kristian', + isDominantSpeaker: false, + isLocalParticipant: true, + isSpeaking: false, + joinedAt: { nanos: 782654015, seconds: '1729747234' }, + name: 'kristian', + publishedTracks: [], + reaction: undefined, + roles: ['user'], + sessionId: '0c907764-84ee-40b4-a7b5-b02c8e6eeae4', + trackLookupPrefix: '03ded86c7006b12f', + userId: 'kristian', + viewportVisibilityState: { + screenShareTrack: 'UNKNOWN', + videoTrack: 'VISIBLE', + }, + }, + { + audioLevel: 0, + connectionQuality: 3, + custom: { fields: {} }, + image: 'https://getstream.io/random_png/?id=sarah&name=sarah', + isDominantSpeaker: false, + isLocalParticipant: false, + isSpeaking: false, + joinedAt: { nanos: 782654015, seconds: '1729747234' }, + name: 'Sarah', + publishedTracks: [], + reaction: undefined, + roles: ['user'], + sessionId: '0c907764-84ee-40b4-a7b5-b02c8e6eeaef4', + trackLookupPrefix: '03ded86c7006b12f', + userId: 'sarah', + viewportVisibilityState: { + screenShareTrack: 'UNKNOWN', + videoTrack: 'VISIBLE', + }, + }, + { + audioLevel: 0, + connectionQuality: 3, + custom: { fields: {} }, + image: 'https://getstream.io/random_png/?id=alice&name=alice', + isDominantSpeaker: false, + isLocalParticipant: false, + isSpeaking: false, + joinedAt: { nanos: 123456789, seconds: '1729747240' }, + name: 'Alice', + publishedTracks: [], + reaction: undefined, + roles: ['user'], + sessionId: 'f8d4a2cd-4599-41a8-a457-4e23876e1ac2', + trackLookupPrefix: 'b9d0f9c25f83c63a', + userId: 'alice', + viewportVisibilityState: { + screenShareTrack: 'UNKNOWN', + videoTrack: 'VISIBLE', + }, + }, + { + audioLevel: 0, + connectionQuality: 3, + custom: { fields: {} }, + image: 'https://getstream.io/random_png/?id=bob&name=bob', + isDominantSpeaker: false, + isLocalParticipant: false, + isSpeaking: false, + joinedAt: { nanos: 987654321, seconds: '1729747250' }, + name: 'Bob', + publishedTracks: [], + reaction: undefined, + roles: ['user'], + sessionId: 'a4d3f2b9-783d-4b87-9c02-e8c9a8a75d3f', + trackLookupPrefix: 'd3b2c9a75d3f4b87', + userId: 'bob', + viewportVisibilityState: { + screenShareTrack: 'UNKNOWN', + videoTrack: 'VISIBLE', + }, + }, + { + audioLevel: 0, + connectionQuality: 3, + custom: { fields: {} }, + image: 'https://getstream.io/random_png/?id=charlie&name=charlie', + isDominantSpeaker: false, + isLocalParticipant: false, + isSpeaking: false, + joinedAt: { nanos: 543210987, seconds: '1729747260' }, + name: 'Charlie', + publishedTracks: [], + reaction: undefined, + roles: ['user'], + sessionId: 'b5f2a3c4-2d6e-4f89-8a9e-1c3d5e6f7a89', + trackLookupPrefix: 'c3d5e6f7a89f4f89', + userId: 'charlie', + viewportVisibilityState: { + screenShareTrack: 'UNKNOWN', + videoTrack: 'VISIBLE', + }, + }, + { + audioLevel: 0, + connectionQuality: 3, + custom: { fields: {} }, + image: 'https://getstream.io/random_png/?id=david&name=david', + isDominantSpeaker: false, + isLocalParticipant: false, + isSpeaking: false, + joinedAt: { nanos: 876543210, seconds: '1729747270' }, + name: 'David', + publishedTracks: [], + reaction: undefined, + roles: ['user'], + sessionId: 'c2d5f3a4-9e7b-4c89-a03e-1b5f2d3e4a5b', + trackLookupPrefix: 'f2d3e4a5b3c9e7b4', + userId: 'david', + viewportVisibilityState: { + screenShareTrack: 'UNKNOWN', + videoTrack: 'VISIBLE', + }, + }, + { + audioLevel: 0, + connectionQuality: 3, + custom: { fields: {} }, + image: 'https://getstream.io/random_png/?id=emma&name=emma', + isDominantSpeaker: false, + isLocalParticipant: false, + isSpeaking: false, + joinedAt: { nanos: 654321098, seconds: '1729747280' }, + name: 'Emma', + publishedTracks: [], + reaction: undefined, + roles: ['user'], + sessionId: 'd3a5c4b2-8e7f-4c89-a2b1-3d5f6e7a8c90', + trackLookupPrefix: 'e7a8c90b4c89f4a7', + userId: 'emma', + viewportVisibilityState: { + screenShareTrack: 'UNKNOWN', + videoTrack: 'VISIBLE', + }, + }, + { + audioLevel: 0, + connectionQuality: 3, + custom: { fields: {} }, + image: 'https://getstream.io/random_png/?id=frank&name=frank', + isDominantSpeaker: false, + isLocalParticipant: false, + isSpeaking: false, + joinedAt: { nanos: 321098765, seconds: '1729747290' }, + name: 'Frank', + publishedTracks: [], + reaction: undefined, + roles: ['user'], + sessionId: 'e2b1d4c3-5f8e-4a7b-90c3-f5a6e8d7c9b0', + trackLookupPrefix: 'f8e7a6d9b0c4f5b3', + userId: 'frank', + viewportVisibilityState: { + screenShareTrack: 'UNKNOWN', + videoTrack: 'VISIBLE', + }, + }, + ]; + + // Limit the returned participants to the requested count + return mockParticipants.slice(0, count); +}; diff --git a/packages/react-native-sdk/src/components/Call/CallParticipantsList/CallParticipantsList.tsx b/packages/react-native-sdk/src/components/Call/CallParticipantsList/CallParticipantsList.tsx index afc4a53309..f1a2304d83 100644 --- a/packages/react-native-sdk/src/components/Call/CallParticipantsList/CallParticipantsList.tsx +++ b/packages/react-native-sdk/src/components/Call/CallParticipantsList/CallParticipantsList.tsx @@ -21,6 +21,7 @@ import { ParticipantViewProps, } from '../../Participant/ParticipantView'; import { CallContentProps } from '../CallContent'; +import { useTheme } from '../../..'; type FlatListProps = React.ComponentProps< typeof FlatList @@ -71,7 +72,6 @@ export type CallParticipantsListProps = CallParticipantsListComponentProps & * hence it should be used only in a flex parent container */ export const CallParticipantsList = ({ - numberOfColumns = 2, horizontal, participants, ParticipantView = DefaultParticipantView, @@ -82,7 +82,10 @@ export const CallParticipantsList = ({ VideoRenderer, supportedReactions, landscape, + numberOfColumns = landscape ? 3 : 2, }: CallParticipantsListProps) => { + const styles = useStyles(); + const { theme } = useTheme(); const [containerLayout, setContainerLayout] = useState({ width: 0, height: 0, @@ -166,15 +169,31 @@ export const CallParticipantsList = ({ }); const itemContainerStyle = useMemo>(() => { - const style = { width: itemWidth, height: itemHeight }; + const style = { + width: itemWidth, + height: itemHeight, + marginHorizontal: theme.variants.spacingSizes.xs, + marginVertical: theme.variants.spacingSizes.xs, + }; + if (horizontal) { - return [styles.participantWrapperHorizontal, style]; + const participantWrapperHorizontal = { + // note: if marginHorizontal is changed, be sure to change the width calculation in calculateParticipantViewSize function + marginHorizontal: theme.variants.spacingSizes.xs, + borderRadius: theme.variants.borderRadiusSizes.md, + }; + return [participantWrapperHorizontal, style]; } + if (landscape) { - return [styles.landScapeStyle, style]; + const landscapeStyle = { + marginVertical: theme.variants.spacingSizes.xs, + borderRadius: theme.variants.borderRadiusSizes.md, + }; + return [landscapeStyle, style]; } return style; - }, [itemWidth, itemHeight, horizontal, landscape]); + }, [itemWidth, itemHeight, horizontal, landscape, theme]); const participantProps: ParticipantViewComponentProps = { ParticipantLabel, @@ -208,9 +227,9 @@ export const CallParticipantsList = ({ [itemContainerStyle] ); - // in vertical mode, only when there are more than 2 participants in a call, the participants should be displayed in a grid - // else we display them both in a stretched row on the screen - const shouldWrapByColumns = !!horizontal || participants.length > 2; + // in vertical mode, only when there are more than 3 participants in a call, the participants should be displayed in a grid + // else we display them in a stretched rows on the screen + const shouldWrapByColumns = !!horizontal || participants.length > 3; if (!shouldWrapByColumns) { return ( @@ -251,17 +270,19 @@ export const CallParticipantsList = ({ ); }; -const styles = StyleSheet.create({ - flexed: { flex: 1 }, - participantWrapperHorizontal: { - // note: if marginHorizontal is changed, be sure to change the width calculation in calculateParticipantViewSize function - marginHorizontal: 8, - borderRadius: 10, - }, - landScapeStyle: { - borderRadius: 10, - }, -}); +const useStyles = () => { + const { theme } = useTheme(); + return useMemo( + () => + StyleSheet.create({ + flexed: { + flex: 1, + margin: theme.variants.spacingSizes.xs, + }, + }), + [theme] + ); +}; /** * This function calculates the size of the participant view based on the size of the container (the phone's screen size) and the number of participants. @@ -292,15 +313,16 @@ function calculateParticipantViewSize({ // special case: if there are 4 or less participants, we display them in 2 rows itemHeight = containerHeight / 2; } else { - // generally, we display the participants in 3 rows - itemHeight = containerHeight / 3; + // generally, we display the participants in 2 rows + itemHeight = containerHeight / 2; } } let itemWidth = containerWidth / numberOfColumns; - if (horizontal) { - // in horizontal mode we apply margin of 8 to the participant view and that should be subtracted from the width - itemWidth = itemWidth - 8 * 2; + itemWidth = itemWidth - 4 * 2; + if (!horizontal) { + // in vertical mode we apply margin of 4 to the participant view and that should be subtracted from the width + itemHeight = itemHeight - 4 * 2; } return { itemHeight, itemWidth }; diff --git a/packages/react-native-sdk/src/components/Call/Lobby/Lobby.tsx b/packages/react-native-sdk/src/components/Call/Lobby/Lobby.tsx index 23db831fba..e4f59c65bf 100644 --- a/packages/react-native-sdk/src/components/Call/Lobby/Lobby.tsx +++ b/packages/react-native-sdk/src/components/Call/Lobby/Lobby.tsx @@ -1,4 +1,4 @@ -import React, { ComponentType } from 'react'; +import React, { ComponentType, useMemo } from 'react'; import { StyleSheet, Text, View, ViewStyle } from 'react-native'; import { useCallStateHooks, @@ -63,6 +63,7 @@ export const Lobby = ({ const { theme: { colors, lobby, typefaces }, } = useTheme(); + const styles = useStyles(); const connectedUser = useConnectedUser(); const { useCameraState, useCallSettings } = useCallStateHooks(); const callSettings = useCallSettings(); @@ -159,6 +160,7 @@ const ParticipantStatus = () => { const { theme: { colors, typefaces, lobby }, } = useTheme(); + const styles = useStyles(); const connectedUser = useConnectedUser(); const participantLabel = connectedUser?.name ?? connectedUser?.id; return ( @@ -186,51 +188,62 @@ const ParticipantStatus = () => { ); }; -const styles = StyleSheet.create({ - container: { - flex: 1, - justifyContent: 'space-evenly', - }, - topContainer: { - flex: 2, - justifyContent: 'space-evenly', - paddingHorizontal: 12, - }, - heading: { - textAlign: 'center', - }, - subHeading: { - textAlign: 'center', - }, - videoContainer: { - height: LOBBY_VIDEO_VIEW_HEIGHT, - borderRadius: 20, - justifyContent: 'space-between', - alignItems: 'center', - overflow: 'hidden', - padding: 8, - }, - topView: {}, - bottomContainer: { - flex: 2, - justifyContent: 'space-evenly', - paddingHorizontal: 12, - }, - participantStatusContainer: { - alignSelf: 'flex-start', - flexDirection: 'row', - alignItems: 'center', - padding: 8, - borderRadius: 5, - }, - avatarContainer: { - flex: 2, - justifyContent: 'center', - }, - userNameLabel: { - flexShrink: 1, - }, - audioMutedIconContainer: { - marginLeft: 8, - }, -}); +const useStyles = () => { + const { theme } = useTheme(); + return useMemo( + () => + StyleSheet.create({ + container: { + flex: 1, + justifyContent: 'space-evenly', + paddingRight: theme.variants.insets.right, + paddingLeft: theme.variants.insets.left, + paddingTop: theme.variants.insets.top, + paddingBottom: theme.variants.insets.bottom, + }, + topContainer: { + flex: 2, + justifyContent: 'space-evenly', + paddingHorizontal: 12, + }, + heading: { + textAlign: 'center', + }, + subHeading: { + textAlign: 'center', + }, + videoContainer: { + height: LOBBY_VIDEO_VIEW_HEIGHT, + borderRadius: 20, + justifyContent: 'space-between', + alignItems: 'center', + overflow: 'hidden', + padding: 8, + }, + topView: {}, + bottomContainer: { + flex: 2, + justifyContent: 'space-evenly', + paddingHorizontal: 12, + }, + participantStatusContainer: { + alignSelf: 'flex-start', + flexDirection: 'row', + alignItems: 'center', + padding: 8, + borderRadius: 5, + }, + avatarContainer: { + flex: 2, + justifyContent: 'center', + }, + userNameLabel: { + flexShrink: 1, + }, + audioMutedIconContainer: { + marginLeft: 8, + }, + }), + [theme] + ); +}; diff --git a/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantLabel.tsx b/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantLabel.tsx index 5d4ee2ad81..36be9835c3 100644 --- a/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantLabel.tsx +++ b/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantLabel.tsx @@ -97,19 +97,11 @@ export const ParticipantLabel = ({ ]} > - + {participantLabel} - + {isPinningEnabled && ( @@ -152,6 +144,9 @@ const useStyles = () => { userNameLabel: { flexShrink: 1, marginTop: 2, + fontSize: 13, + fontWeight: '400', + color: theme.colors.iconPrimaryDefault, }, screenShareIconContainer: { marginRight: theme.variants.spacingSizes.sm, diff --git a/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantView.tsx b/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantView.tsx index 76b6e74921..758dd7538b 100644 --- a/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantView.tsx +++ b/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantView.tsx @@ -149,7 +149,13 @@ export const ParticipantView = ({ videoZOrder={videoZOrder} /> )} - + {ParticipantLabel && ( )} @@ -169,10 +175,7 @@ const useStyles = () => { container: { justifyContent: 'space-between', overflow: 'hidden', - borderWidth: 2, - borderColor: 'transparent', - margin: theme.variants.spacingSizes.sm, - borderRadius: 16, + borderRadius: theme.variants.borderRadiusSizes.md, }, footerContainer: { flexDirection: 'row', @@ -182,6 +185,7 @@ const useStyles = () => { highligtedContainer: { borderWidth: 2, }, + networkIndicatorOnly: { justifyContent: 'flex-end' }, }), [theme] ); diff --git a/packages/react-native-sdk/src/constants/TestIds.ts b/packages/react-native-sdk/src/constants/TestIds.ts index aadf5b2682..f5f38f92de 100644 --- a/packages/react-native-sdk/src/constants/TestIds.ts +++ b/packages/react-native-sdk/src/constants/TestIds.ts @@ -10,6 +10,7 @@ export enum ComponentTestIds { CALL_PARTICIPANTS_LIST = 'call-participants-list', CALL_PARTICIPANTS_SPOTLIGHT = 'call-participants-spotlight', CALL_PARTICIPANTS_GRID = 'call-participants-grid', + CALL_PARTICIPANTS_FULLSCREEN = 'call-participants-fullscreen', LOCAL_PARTICIPANT = 'local-participant', PARTICIPANT_MEDIA_STREAM = 'participant-media-stream', PARTICIPANTS_INFO = 'participants-info', diff --git a/packages/react-native-sdk/src/constants/index.ts b/packages/react-native-sdk/src/constants/index.ts index 42241a3ef0..3e893573b6 100644 --- a/packages/react-native-sdk/src/constants/index.ts +++ b/packages/react-native-sdk/src/constants/index.ts @@ -1,9 +1,9 @@ import { StreamReactionType } from '../components'; export const FLOATING_VIDEO_VIEW_STYLE = { - height: 140, - width: 80, - borderRadius: 10, + height: 228, + width: 140, + borderRadius: 16, }; export const LOBBY_VIDEO_VIEW_HEIGHT = 240; diff --git a/packages/react-native-sdk/src/icons/CameraSwitch.tsx b/packages/react-native-sdk/src/icons/CameraSwitch.tsx index a4fc8aa5f9..62bb662cd2 100644 --- a/packages/react-native-sdk/src/icons/CameraSwitch.tsx +++ b/packages/react-native-sdk/src/icons/CameraSwitch.tsx @@ -8,7 +8,7 @@ type Props = { }; export const CameraSwitch = ({ color, size }: Props) => ( - + ( - + ( - - - -); diff --git a/packages/react-native-sdk/src/icons/index.tsx b/packages/react-native-sdk/src/icons/index.tsx index f3b3ee722f..d3f655b6e4 100644 --- a/packages/react-native-sdk/src/icons/index.tsx +++ b/packages/react-native-sdk/src/icons/index.tsx @@ -8,7 +8,6 @@ export * from './Video'; export * from './VideoSlash'; export * from './ThreeDots'; export * from './PinVertical'; -export * from './Spotlight'; export * from './ScreenShareIndicator'; export * from './ScreenShare'; export * from './Reaction'; diff --git a/packages/react-native-sdk/src/theme/theme.ts b/packages/react-native-sdk/src/theme/theme.ts index a98abb9505..7bc813989e 100644 --- a/packages/react-native-sdk/src/theme/theme.ts +++ b/packages/react-native-sdk/src/theme/theme.ts @@ -18,6 +18,7 @@ export type Theme = { avatarSizes: DimensionType; fontSizes: DimensionType; spacingSizes: DimensionType; + borderRadiusSizes: DimensionType; insets: Insets; }; typefaces: Record; diff --git a/sample-apps/react-native/dogfood/src/assets/FullScreen.tsx b/sample-apps/react-native/dogfood/src/assets/FullScreen.tsx new file mode 100644 index 0000000000..553b889f9f --- /dev/null +++ b/sample-apps/react-native/dogfood/src/assets/FullScreen.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { Svg, Path } from 'react-native-svg'; +import { ColorValue } from 'react-native/types'; + +type Props = { + color: ColorValue; + size: number; +}; + +export const FullScreen = ({ color, size }: Props) => ( + + + +); diff --git a/sample-apps/react-native/dogfood/src/assets/Grid.tsx b/sample-apps/react-native/dogfood/src/assets/Grid.tsx new file mode 100644 index 0000000000..6b6e09b8b8 --- /dev/null +++ b/sample-apps/react-native/dogfood/src/assets/Grid.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { Svg, Path } from 'react-native-svg'; +import { ColorValue } from 'react-native/types'; + +type Props = { + color: ColorValue; + size: number; +}; + +export const Grid = ({ color, size }: Props) => ( + + + +); diff --git a/sample-apps/react-native/dogfood/src/assets/Spotlight.tsx b/sample-apps/react-native/dogfood/src/assets/Spotlight.tsx new file mode 100644 index 0000000000..a360b5edcd --- /dev/null +++ b/sample-apps/react-native/dogfood/src/assets/Spotlight.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { Svg, Path } from 'react-native-svg'; +import { ColorValue } from 'react-native/types'; + +type Props = { + color: ColorValue; + size: number; +}; + +export const SpotLight = ({ color, size }: Props) => ( + + + +); diff --git a/sample-apps/react-native/dogfood/src/components/ActiveCall.tsx b/sample-apps/react-native/dogfood/src/components/ActiveCall.tsx index b1eb969d48..f72378adec 100644 --- a/sample-apps/react-native/dogfood/src/components/ActiveCall.tsx +++ b/sample-apps/react-native/dogfood/src/components/ActiveCall.tsx @@ -15,6 +15,7 @@ import { useOrientation } from '../hooks/useOrientation'; import { Z_INDEX } from '../constants'; import { TopControls } from './CallControlls/TopControls'; import { useCallStateHooks } from '@stream-io/video-react-bindings'; +import { useLayout } from '../contexts/LayoutContext'; type ActiveCallProps = BottomControlsProps & { onHangupCallHandler?: () => void; @@ -33,6 +34,7 @@ export const ActiveCall = ({ const currentOrientation = useOrientation(); const styles = useStyles(); const { theme: colors } = useTheme(); + const { selectedLayout } = useLayout(); const onOpenCallParticipantsInfo = useCallback(() => { setIsCallParticipantsVisible(true); @@ -88,12 +90,15 @@ export const ActiveCall = ({ backgroundColor={colors.sheetPrimary} /> + + { const { theme } = useTheme(); - return useMemo( () => StyleSheet.create({ @@ -131,6 +135,22 @@ const useStyles = () => { height: theme.variants.insets.bottom, backgroundColor: theme.colors.sheetPrimary, }, + leftUnsafeArea: { + position: 'absolute', + top: 0, + bottom: 0, + left: 0, + width: theme.variants.insets.left, + backgroundColor: theme.colors.sheetPrimary, + }, + rightUnsafeArea: { + position: 'absolute', + top: 0, + bottom: 0, + right: 0, + width: theme.variants.insets.right, + backgroundColor: theme.colors.sheetPrimary, + }, view: { ...StyleSheet.absoluteFillObject, zIndex: Z_INDEX.IN_FRONT, diff --git a/sample-apps/react-native/dogfood/src/components/CallControlls/BottomControls.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/BottomControls.tsx index c2b92592b7..a4f95cd2fb 100644 --- a/sample-apps/react-native/dogfood/src/components/CallControlls/BottomControls.tsx +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/BottomControls.tsx @@ -37,7 +37,7 @@ export const BottomControls = ({ }: BottomControlsProps) => { const { useMicrophoneState } = useCallStateHooks(); const { isSpeakingWhileMuted } = useMicrophoneState(); - const styles = useStyles(); + const styles = useStyles(isSpeakingWhileMuted); return ( @@ -70,15 +70,20 @@ export const BottomControls = ({ ); }; -const useStyles = () => { +const useStyles = (showMicLabel: boolean) => { const { theme } = useTheme(); return useMemo( () => StyleSheet.create({ + container: { + paddingVertical: !showMicLabel ? theme.variants.spacingSizes.md : 0, + paddingHorizontal: theme.variants.spacingSizes.md, + backgroundColor: theme.colors.sheetPrimary, + height: 76, + }, speakingLabelContainer: { backgroundColor: appTheme.colors.static_overlay, - // paddingVertical: 10, width: '100%', }, label: { @@ -91,12 +96,6 @@ const useStyles = () => { justifyContent: 'flex-start', zIndex: Z_INDEX.IN_FRONT, }, - container: { - paddingVertical: theme.variants.spacingSizes.md, - paddingHorizontal: theme.variants.spacingSizes.md, - backgroundColor: theme.colors.sheetPrimary, - height: 76, - }, left: { flex: 2.5, flexDirection: 'row', @@ -110,6 +109,6 @@ const useStyles = () => { gap: theme.variants.spacingSizes.xs, }, }), - [theme], + [theme, showMicLabel], ); }; diff --git a/sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherButton.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherButton.tsx index 05f1324a16..3d9d9987b9 100644 --- a/sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherButton.tsx +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherButton.tsx @@ -1,10 +1,15 @@ import React, { useState } from 'react'; -import { Grid } from '@stream-io/video-react-native-sdk/src/icons/Grid'; import { CallControlsButton, useTheme, } from '@stream-io/video-react-native-sdk'; import { IconWrapper } from '@stream-io/video-react-native-sdk/src/icons'; +import LayoutSwitcherModal from './LayoutSwitcherModal'; +import { ColorValue } from 'react-native'; +import { Grid } from '../../assets/Grid'; +import { SpotLight } from '../../assets/Spotlight'; +import { FullScreen } from '../../assets/FullScreen'; +import { useLayout } from '../../contexts/LayoutContext'; export type LayoutSwitcherButtonProps = { /** @@ -14,6 +19,19 @@ export type LayoutSwitcherButtonProps = { onPressHandler?: () => void; }; +const getIcon = (selectedButton: string, color: ColorValue, size: number) => { + switch (selectedButton) { + case 'grid': + return ; + case 'spotlight': + return ; + case 'fullscreen': + return ; + default: + return 'grid'; + } +}; + /** * The layout switcher Button can be used to switch different layout arrangements * of the call participants. @@ -22,27 +40,51 @@ export const LayoutSwitcherButton = ({ onPressHandler, }: LayoutSwitcherButtonProps) => { const { - theme: { colors, defaults, variants }, + theme: { colors, variants }, } = useTheme(); - const [toggleLayoutMenu, setToggleLayoutMenu] = useState(false); - const buttonColor = toggleLayoutMenu + + const { selectedLayout } = useLayout(); + const [isModalVisible, setIsModalVisible] = useState(false); + const [anchorPosition, setAnchorPosition] = useState<{ + x: number; + y: number; + width: number; + height: number; + } | null>(null); + + const buttonColor = isModalVisible ? colors.iconPrimaryAccent : colors.iconPrimaryDefault; + const handleOpenModal = () => setIsModalVisible(true); + const handleCloseModal = () => setIsModalVisible(false); + + const handleLayout = (event: any) => { + const { x, y, width, height } = event.nativeEvent.layout; + setAnchorPosition({ x, y: y + height, width, height }); + }; + return ( { + handleOpenModal(); if (onPressHandler) { onPressHandler(); } - setToggleLayoutMenu(!toggleLayoutMenu); + setIsModalVisible(!isModalVisible); }} color={colors.sheetPrimary} > - + {getIcon(selectedLayout, buttonColor, variants.iconSizes.lg)} + ); }; diff --git a/sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherModal.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherModal.tsx new file mode 100644 index 0000000000..d9c6b84bcd --- /dev/null +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherModal.tsx @@ -0,0 +1,169 @@ +import React, { useEffect, useState, useMemo } from 'react'; +import { + View, + TouchableOpacity, + Text, + StyleSheet, + Dimensions, + Modal, +} from 'react-native'; +import { useTheme } from '@stream-io/video-react-native-sdk'; +import { Grid } from '../../assets/Grid'; +import { FullScreen } from '../../assets/FullScreen'; +import { SpotLight } from '../../assets/Spotlight'; +import { Layout, useLayout } from '../../contexts/LayoutContext'; + +interface AnchorPosition { + x: number; + y: number; + height: number; +} + +interface PopupComponentProps { + anchorPosition?: AnchorPosition | null; + isVisible: boolean; + onClose: () => void; +} + +const LayoutSwitcherModal: React.FC = ({ + isVisible, + onClose, + anchorPosition, +}) => { + const { theme } = useTheme(); + const styles = useStyles(); + const [popupPosition, setPopupPosition] = useState({ top: 0, left: 0 }); + const { selectedLayout, onLayoutSelection } = useLayout(); + const topInset = theme.variants.insets.top; + const leftInset = theme.variants.insets.left; + + useEffect(() => { + if (isVisible && anchorPosition) { + const windowHeight = Dimensions.get('window').height; + const windowWidth = Dimensions.get('window').width; + + let top = anchorPosition.y + anchorPosition.height / 2 + topInset; + let left = anchorPosition.x + leftInset; + + // Ensure the popup stays within the screen bounds + if (top + 150 > windowHeight) { + top = anchorPosition.y - 150; + } + if (left + 200 > windowWidth) { + left = windowWidth - 200; + } + + setPopupPosition({ top, left }); + } + }, [isVisible, anchorPosition, topInset, leftInset]); + + if (!isVisible || !anchorPosition) { + return null; + } + + const onPressHandler = (layout: Layout) => { + onLayoutSelection(layout); + onClose(); + }; + + return ( + + + + onPressHandler('grid')} + > + + Grid + + onPressHandler('spotlight')} + > + + Spotlight + + onPressHandler('fullscreen')} + > + + Fullscreen + + + + + ); +}; + +const useStyles = () => { + const { theme } = useTheme(); + + return useMemo( + () => + StyleSheet.create({ + overlay: { + flex: 1, + }, + modal: { + position: 'absolute', + width: 212, + backgroundColor: theme.colors.sheetSecondary, + borderRadius: theme.variants.borderRadiusSizes.lg, + padding: theme.variants.spacingSizes.md, + gap: theme.variants.spacingSizes.sm, + }, + button: { + backgroundColor: theme.colors.buttonSecondaryDefault, + borderRadius: theme.variants.borderRadiusSizes.lg, + flexDirection: 'row', + justifyContent: 'flex-start', + alignItems: 'center', + paddingHorizontal: theme.variants.spacingSizes.md, + paddingVertical: theme.variants.spacingSizes.sm, + }, + selectedButton: { + backgroundColor: theme.colors.primary, + }, + buttonText: { + color: 'white', + textAlign: 'center', + fontWeight: '600', + marginTop: 2, + marginLeft: theme.variants.spacingSizes.xs, + }, + }), + [theme], + ); +}; + +export default LayoutSwitcherModal; diff --git a/sample-apps/react-native/dogfood/src/components/CallControlls/TopControls.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/TopControls.tsx index 9843386716..29e1904eb2 100644 --- a/sample-apps/react-native/dogfood/src/components/CallControlls/TopControls.tsx +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/TopControls.tsx @@ -40,7 +40,7 @@ export const TopControls = ({ - {}} /> + {!isAwaitingResponse && } @@ -61,7 +61,6 @@ export const TopControls = ({ const useStyles = () => { const { theme } = useTheme(); - return useMemo( () => StyleSheet.create({ diff --git a/sample-apps/react-native/dogfood/src/components/MeetingUI.tsx b/sample-apps/react-native/dogfood/src/components/MeetingUI.tsx index b5765d0749..1c64338a0c 100644 --- a/sample-apps/react-native/dogfood/src/components/MeetingUI.tsx +++ b/sample-apps/react-native/dogfood/src/components/MeetingUI.tsx @@ -13,6 +13,7 @@ import { useAppGlobalStoreSetState } from '../contexts/AppContext'; import { AuthenticationProgress } from './AuthenticatingProgress'; import { CallErrorComponent } from './CallErrorComponent'; import { useUnreadCount } from '../hooks/useUnreadCount'; +import { LayoutProvider } from '../contexts/LayoutContext'; type Props = NativeStackScreenProps< MeetingStackParamList, @@ -129,12 +130,14 @@ export const MeetingUI = ({ callId, navigation, route }: Props) => { ); } else { return ( - + + + ); } }; diff --git a/sample-apps/react-native/dogfood/src/components/ParticipantsLayoutButton.tsx b/sample-apps/react-native/dogfood/src/components/ParticipantsLayoutButton.tsx deleted file mode 100644 index 982847e71a..0000000000 --- a/sample-apps/react-native/dogfood/src/components/ParticipantsLayoutButton.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import React, { useState } from 'react'; -import { Pressable, Text, Modal, StyleSheet, View } from 'react-native'; -import GridIconSvg from '../assets/GridIconSvg'; -import { appTheme } from '../theme'; - -type Layout = 'grid' | 'spotlight'; - -const LayoutSelectionItem = ({ - layout, - selectedLayout, - setSelectedLayout, - closeModal, -}: { - layout: Layout; - selectedLayout: Layout; - setSelectedLayout: (mode: Layout) => void; - closeModal: () => void; -}) => { - if (!layout) { - return null; - } - - return ( - { - setSelectedLayout(layout); - closeModal(); - }} - style={styles.modalButton} - > - - {layout[0].toUpperCase() + layout.substring(1)} - - - ); -}; - -export const ParticipantsLayoutSwitchButton = ({ - selectedLayout, - setSelectedLayout, -}: { - selectedLayout: Layout; - setSelectedLayout: (m: Layout) => void; -}) => { - const [modalVisible, setModalVisible] = useState(false); - const closeModal = () => setModalVisible(false); - - return ( - <> - - setModalVisible(false)} - > - true}> - - - - - - - setModalVisible(true)} - style={styles.gridButton} - > - - - - - ); -}; - -const styles = StyleSheet.create({ - centeredView: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - backgroundColor: 'rgba(0, 0, 0, 0.5)', - }, - modalView: { - backgroundColor: appTheme.colors.static_grey, - borderRadius: 20, - padding: appTheme.spacing.md, - alignItems: 'flex-start', - shadowColor: '#000', - shadowOffset: { - width: 0, - height: 2, - }, - shadowOpacity: 0.25, - shadowRadius: 4, - elevation: 5, - }, - gridButton: { - height: 30, - width: 30, - }, - modalButton: { - padding: appTheme.spacing.lg, - }, - modalText: { - fontSize: 20, - fontWeight: 'bold', - }, - buttonsContainer: { - paddingHorizontal: appTheme.spacing.sm, - }, -}); diff --git a/sample-apps/react-native/dogfood/src/components/VideoEffectsButton/index.tsx b/sample-apps/react-native/dogfood/src/components/VideoEffectsButton/index.tsx index fee172915d..05fddd8405 100644 --- a/sample-apps/react-native/dogfood/src/components/VideoEffectsButton/index.tsx +++ b/sample-apps/react-native/dogfood/src/components/VideoEffectsButton/index.tsx @@ -86,7 +86,7 @@ const FilterButton = () => { diff --git a/sample-apps/react-native/dogfood/src/contexts/LayoutContext.tsx b/sample-apps/react-native/dogfood/src/contexts/LayoutContext.tsx new file mode 100644 index 0000000000..582c6efafc --- /dev/null +++ b/sample-apps/react-native/dogfood/src/contexts/LayoutContext.tsx @@ -0,0 +1,44 @@ +import React, { + createContext, + useContext, + useState, + ReactNode, + useCallback, +} from 'react'; + +export type Layout = 'grid' | 'spotlight' | 'fullscreen'; + +interface LayoutContextState { + selectedLayout: Layout; + onLayoutSelection: (layout: Layout) => void; +} + +const LayoutContext = createContext(null); + +interface LayoutProviderProps { + children: ReactNode; +} + +const LayoutProvider: React.FC = ({ children }) => { + const [selectedLayout, setSelectedLayout] = useState('grid'); + + const onLayoutSelection = useCallback((layout: Layout) => { + setSelectedLayout(layout); + }, []); + + return ( + + {children} + + ); +}; + +const useLayout = (): LayoutContextState => { + const context = useContext(LayoutContext); + if (!context) { + throw new Error('useLayout must be used within a LayoutProvider'); + } + return context; +}; + +export { LayoutProvider, useLayout }; diff --git a/sample-apps/react-native/dogfood/src/screens/Meeting/JoinMeetingScreen.tsx b/sample-apps/react-native/dogfood/src/screens/Meeting/JoinMeetingScreen.tsx index 075359e82e..2587996510 100644 --- a/sample-apps/react-native/dogfood/src/screens/Meeting/JoinMeetingScreen.tsx +++ b/sample-apps/react-native/dogfood/src/screens/Meeting/JoinMeetingScreen.tsx @@ -1,4 +1,4 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState, useMemo } from 'react'; import { Image, KeyboardAvoidingView, @@ -16,7 +16,7 @@ import { appTheme } from '../../theme'; import { TextInput } from '../../components/TextInput'; import { Button } from '../../components/Button'; import { prontoCallId$ } from '../../hooks/useProntoLinkEffect'; -import { useI18n } from '@stream-io/video-react-native-sdk'; +import { useI18n, useTheme } from '@stream-io/video-react-native-sdk'; import { useOrientation } from '../../hooks/useOrientation'; type JoinMeetingScreenProps = NativeStackScreenProps< @@ -29,6 +29,7 @@ const JoinMeetingScreen = (props: JoinMeetingScreenProps) => { const [linking, setLinking] = useState(false); const { t } = useI18n(); const orientation = useOrientation(); + const styles = useStyles(); const { navigation } = props; const userImageUrl = useAppGlobalStoreValue((store) => store.userImageUrl); @@ -118,55 +119,64 @@ const JoinMeetingScreen = (props: JoinMeetingScreenProps) => { ); }; -const styles = StyleSheet.create({ - container: { - padding: appTheme.spacing.lg, - backgroundColor: appTheme.colors.static_grey, - flex: 1, - justifyContent: 'space-evenly', - }, - topContainer: { - flex: 1, - justifyContent: 'center', - }, - logo: { - height: 100, - width: 100, - borderRadius: 50, - alignSelf: 'center', - }, - title: { - fontSize: 30, - color: appTheme.colors.static_white, - fontWeight: '500', - textAlign: 'center', - marginTop: appTheme.spacing.lg, - }, - subTitle: { - color: appTheme.colors.light_gray, - fontSize: 16, - textAlign: 'center', - marginHorizontal: appTheme.spacing.xl, - }, - bottomContainer: { - flex: 1, - justifyContent: 'center', - }, - joinCallButton: { - marginLeft: appTheme.spacing.lg, - }, - startNewCallButton: { - width: '100%', - }, - iconButton: { - width: 40, - }, - createCall: { - display: 'flex', - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - }, -}); +const useStyles = () => { + const { theme } = useTheme(); + return useMemo( + () => + StyleSheet.create({ + container: { + padding: appTheme.spacing.lg, + backgroundColor: appTheme.colors.static_grey, + flex: 1, + justifyContent: 'space-evenly', + paddingRight: theme.variants.insets.right, + paddingLeft: theme.variants.insets.left, + }, + topContainer: { + flex: 1, + justifyContent: 'center', + }, + logo: { + height: 100, + width: 100, + borderRadius: 50, + alignSelf: 'center', + }, + title: { + fontSize: 30, + color: appTheme.colors.static_white, + fontWeight: '500', + textAlign: 'center', + marginTop: appTheme.spacing.lg, + }, + subTitle: { + color: appTheme.colors.light_gray, + fontSize: 16, + textAlign: 'center', + marginHorizontal: appTheme.spacing.xl, + }, + bottomContainer: { + flex: 1, + justifyContent: 'center', + }, + joinCallButton: { + marginLeft: appTheme.spacing.lg, + }, + startNewCallButton: { + width: '100%', + }, + iconButton: { + width: 40, + }, + createCall: { + display: 'flex', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + }, + }), + [theme], + ); +}; export default JoinMeetingScreen;