From c5a7182d7e348aa29eed8eebc96910c01e3464cd Mon Sep 17 00:00:00 2001 From: Kristian Martinoski Date: Tue, 15 Oct 2024 06:54:40 +0200 Subject: [PATCH 01/36] feat: add call duration badge and hangup call button --- packages/client/src/Call.ts | 1 + .../Call/CallContent/CallContent.tsx | 54 ++++--- .../Call/CallControls/HangupCallButton.tsx | 17 +-- .../Call/CallTopView/CallTopView.tsx | 138 +++++++++--------- .../Call/CallTopView/DurationBadge.tsx | 93 ++++++++++++ .../src/icons/CallDuration.tsx | 17 +++ .../react-native-sdk/src/icons/PhoneDown.tsx | 12 +- packages/react-native-sdk/src/icons/index.tsx | 1 + .../react-native/dogfood/ios/Podfile.lock | 4 +- .../dogfood/src/components/ActiveCall.tsx | 5 +- .../dogfood/src/components/Button.tsx | 18 ++- yarn.lock | 6 +- 12 files changed, 245 insertions(+), 121 deletions(-) create mode 100644 packages/react-native-sdk/src/components/Call/CallTopView/DurationBadge.tsx create mode 100644 packages/react-native-sdk/src/icons/CallDuration.tsx diff --git a/packages/client/src/Call.ts b/packages/client/src/Call.ts index 31e5cf08ac..6cba3020d0 100644 --- a/packages/client/src/Call.ts +++ b/packages/client/src/Call.ts @@ -504,6 +504,7 @@ export class Call { await withoutConcurrency(this.joinLeaveConcurrencyTag, async () => { const callingState = this.state.callingState; if (callingState === CallingState.LEFT) { + // TODO: check why this error is happening throw new Error('Cannot leave call that has already been left.'); } 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 51637a173e..f31f6c1725 100644 --- a/packages/react-native-sdk/src/components/Call/CallContent/CallContent.tsx +++ b/packages/react-native-sdk/src/components/Call/CallContent/CallContent.tsx @@ -71,10 +71,7 @@ export type CallContentProps = Pick< HangUpCallButtonProps, 'onHangupCallHandler' > & - Pick< - CallTopViewProps, - 'onBackPressed' | 'onParticipantInfoPress' | 'ParticipantsInfoBadge' - > & + Pick & CallContentComponentProps & { /** * This switches the participant's layout between the grid and the spotlight mode. @@ -101,7 +98,6 @@ export type CallContentProps = Pick< export const CallContent = ({ onBackPressed, - onParticipantInfoPress, onHangupCallHandler, CallParticipantsList, CallTopView = DefaultCallTopView, @@ -113,7 +109,6 @@ export const CallContent = ({ ParticipantReaction, ParticipantVideoFallback, ParticipantView, - ParticipantsInfoBadge, VideoRenderer, layout = 'grid', landscape = false, @@ -220,24 +215,35 @@ export const CallContent = ({ // and allows only the top and floating view (its child views) to take up the touches pointerEvents="box-none" > - {!isInPiPMode && CallTopView && ( - - )} - {showFloatingView && FloatingParticipantView && ( - + + {!isInPiPMode && CallTopView && ( + + )} + {showFloatingView && FloatingParticipantView && ( + + )} + + {showSpotlightLayout ? ( + + ) : ( + )} {showSpotlightLayout ? ( diff --git a/packages/react-native-sdk/src/components/Call/CallControls/HangupCallButton.tsx b/packages/react-native-sdk/src/components/Call/CallControls/HangupCallButton.tsx index 277b42ca6f..36dab6b8e0 100644 --- a/packages/react-native-sdk/src/components/Call/CallControls/HangupCallButton.tsx +++ b/packages/react-native-sdk/src/components/Call/CallControls/HangupCallButton.tsx @@ -42,7 +42,7 @@ export const HangUpCallButton = ({ const { useCallCallingState } = useCallStateHooks(); const callingState = useCallCallingState(); const { - theme: { colors, hangupCallButton }, + theme: { colors, hangupCallButton, variants }, } = useTheme(); const onPress = async () => { @@ -72,15 +72,12 @@ export const HangUpCallButton = ({ size={size} testID={ButtonTestIds.HANG_UP_CALL} > - + {/* */} + + {/* */} ); }; - -// TODO: Check if this style is needed -// This was passed to CallControlsButton as style prop -// const styles = StyleSheet.create({ -// button: { -// shadowColor: theme.light.error, -// }, -// }); diff --git a/packages/react-native-sdk/src/components/Call/CallTopView/CallTopView.tsx b/packages/react-native-sdk/src/components/Call/CallTopView/CallTopView.tsx index 19b4993d86..f395fe39fb 100644 --- a/packages/react-native-sdk/src/components/Call/CallTopView/CallTopView.tsx +++ b/packages/react-native-sdk/src/components/Call/CallTopView/CallTopView.tsx @@ -1,4 +1,4 @@ -import React, { useState } from 'react'; +import React, { useState, useMemo } from 'react'; import { View, StyleSheet, @@ -7,15 +7,15 @@ import { StyleProp, ViewStyle, } from 'react-native'; -import { - ParticipantsInfoBadge as DefaultParticipantsInfoBadge, - ParticipantsInfoBadgeProps, -} from './ParticipantsInfoBadge'; +import { ParticipantsInfoBadgeProps } from './ParticipantsInfoBadge'; import { Back } from '../../../icons/Back'; -import { TopViewBackground } from '../../../icons'; +import { CallDuration, TopViewBackground } from '../../../icons'; import { useCallStateHooks, useI18n } from '@stream-io/video-react-bindings'; import { CallingState } from '@stream-io/video-client'; import { useTheme } from '../../../contexts/ThemeContext'; +import { HangUpCallButton } from '..'; +import { colors } from '../../..'; +import { DurationBadge } from './DurationBadge'; export type CallTopViewProps = { /** @@ -24,10 +24,10 @@ export type CallTopViewProps = { */ onBackPressed?: () => void; /** - * Handler to be called when the Participant icon is pressed in the CallTopView. - * @returns + * Handler to be called when the hangup button is pressed in the CallTopView. + * @returns void */ - onParticipantInfoPress?: () => void; + onHangupCallHandler?: () => void; /** * Title to be rendered at the center of the Header. */ @@ -44,10 +44,9 @@ export type CallTopViewProps = { export const CallTopView = ({ onBackPressed, - onParticipantInfoPress, - title, + onHangupCallHandler, + title = 'test', style: styleProp, - ParticipantsInfoBadge = DefaultParticipantsInfoBadge, }: CallTopViewProps) => { const [callTopViewHeight, setCallTopViewHeight] = useState(0); const [callTopViewWidth, setCallTopViewWidth] = useState(0); @@ -59,6 +58,7 @@ export const CallTopView = ({ callTopView, }, } = useTheme(); + const styles = useStyles(); const { useCallCallingState } = useCallStateHooks(); const callingState = useCallCallingState(); const { t } = useI18n(); @@ -96,67 +96,67 @@ export const CallTopView = ({ )} - {title ? ( - - {title} - - ) : ( - isCallReconnecting && ( - - {t('Reconnecting...')} - - ) - )} + + + - - {ParticipantsInfoBadge && ( - - )} + + ); }; -const styles = StyleSheet.create({ - content: { - position: 'absolute', - top: 0, - flexDirection: 'row', - paddingTop: 24, - paddingBottom: 12, - alignItems: 'center', - }, - backIconContainer: { - // Added to compensate the participant badge surface area - marginLeft: 8, - }, - leftElement: { - flex: 1, - alignItems: 'flex-start', - }, - centerElement: { - flex: 1, - alignItems: 'center', - flexGrow: 3, - }, - rightElement: { - flex: 1, - alignItems: 'flex-end', - }, -}); +const useStyles = () => { + const { theme } = useTheme(); + + return useMemo( + () => + StyleSheet.create({ + content: { + position: 'absolute', + top: 0, + flexDirection: 'row', + paddingTop: 24, + paddingBottom: 12, + alignItems: 'center', + }, + backIconContainer: { + // Added to compensate the participant badge surface area + marginLeft: 8, + }, + leftElement: { + flex: 1, + alignItems: 'flex-start', + }, + centerElement: { + flex: 1, + alignItems: 'center', + flexGrow: 3, + }, + rightElement: { + flex: 1, + alignItems: 'flex-end', + }, + centerWrapper: { + backgroundColor: colors.buttonSecondaryDefault, + borderRadius: 8, + width: 60, + display: 'flex', + flexDirection: 'row', + height: 32, + padding: 6, + justifyContent: 'center', + alignItems: 'center', + // gap: 4, + }, + timer: { + color: colors.typePrimary, + fontSize: 13, + fontWeight: '600', + }, + }), + [theme] + ); +}; diff --git a/packages/react-native-sdk/src/components/Call/CallTopView/DurationBadge.tsx b/packages/react-native-sdk/src/components/Call/CallTopView/DurationBadge.tsx new file mode 100644 index 0000000000..ba052d7780 --- /dev/null +++ b/packages/react-native-sdk/src/components/Call/CallTopView/DurationBadge.tsx @@ -0,0 +1,93 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import { CallDuration } from '../../../icons'; +import { useTheme } from '../../..'; + +// TODO: move to dogfood app +export const DurationBadge = () => { + const { + theme: { + colors, + typefaces, + variants: { iconSizes }, + callTopView, + }, + } = useTheme(); + const [duration, setDuration] = useState(0); + useEffect(() => { + const id = setInterval(() => { + setDuration((d) => d + 1); + }, 1000); + return () => { + clearInterval(id); + }; + }, []); + const styles = useStyles(); + + // Format duration to MM:SS + const minutes = Math.floor(duration / 60) + .toString() + .padStart(2, '0'); + const seconds = (duration % 60).toString().padStart(2, '0'); + const timestamp = `${minutes}:${seconds}`; + + return ( + + + {timestamp} + + ); +}; + +const useStyles = () => { + const { theme } = useTheme(); + + return useMemo( + () => + StyleSheet.create({ + content: { + position: 'absolute', + top: 0, + flexDirection: 'row', + paddingTop: 24, + paddingBottom: 12, + alignItems: 'center', + }, + backIconContainer: { + // Added to compensate the participant badge surface area + marginLeft: 8, + }, + leftElement: { + flex: 1, + alignItems: 'flex-start', + }, + centerElement: { + flex: 1, + alignItems: 'center', + flexGrow: 3, + }, + rightElement: { + flex: 1, + alignItems: 'flex-end', + }, + centerWrapper: { + backgroundColor: theme.colors.buttonSecondaryDefault, + borderRadius: 8, + width: 90, + display: 'flex', + flexDirection: 'row', + height: 32, + padding: 6, + justifyContent: 'center', + alignItems: 'center', + // gap: 4, + }, + timer: { + color: theme.colors.typePrimary, + fontSize: 13, + fontWeight: '600', + }, + }), + [theme] + ); +}; diff --git a/packages/react-native-sdk/src/icons/CallDuration.tsx b/packages/react-native-sdk/src/icons/CallDuration.tsx new file mode 100644 index 0000000000..70c0e04b8e --- /dev/null +++ b/packages/react-native-sdk/src/icons/CallDuration.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 CallDuration = ({ color, size }: Props) => ( + + + +); diff --git a/packages/react-native-sdk/src/icons/PhoneDown.tsx b/packages/react-native-sdk/src/icons/PhoneDown.tsx index 14a92d3737..346f5aa971 100644 --- a/packages/react-native-sdk/src/icons/PhoneDown.tsx +++ b/packages/react-native-sdk/src/icons/PhoneDown.tsx @@ -5,12 +5,18 @@ import { IconTestIds } from '../constants/TestIds'; type Props = { color: ColorValue; + size: number; }; -export const PhoneDown = ({ color }: Props) => ( - +export const PhoneDown = ({ color, size }: Props) => ( + diff --git a/packages/react-native-sdk/src/icons/index.tsx b/packages/react-native-sdk/src/icons/index.tsx index 814d18d34d..2de769c80b 100644 --- a/packages/react-native-sdk/src/icons/index.tsx +++ b/packages/react-native-sdk/src/icons/index.tsx @@ -21,3 +21,4 @@ export * from './StartStreamIcon'; export * from './StopScreenShare'; export * from './EndStreamIcon'; export * from './LeaveStreamIcon'; +export * from './CallDuration'; diff --git a/sample-apps/react-native/dogfood/ios/Podfile.lock b/sample-apps/react-native/dogfood/ios/Podfile.lock index d95247de93..348033fe71 100644 --- a/sample-apps/react-native/dogfood/ios/Podfile.lock +++ b/sample-apps/react-native/dogfood/ios/Podfile.lock @@ -1148,7 +1148,7 @@ PODS: - stream-react-native-webrtc (118.1.0): - JitsiWebRTC (~> 118.0.0) - React-Core - - stream-video-react-native (1.1.0): + - stream-video-react-native (1.1.3): - glog - RCT-Folly (= 2022.05.16.00) - React-Core @@ -1479,7 +1479,7 @@ SPEC CHECKSUMS: SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 stream-io-video-filters-react-native: 8c345e6adf5164646696d45f9962c4199b2fe3b9 stream-react-native-webrtc: 4ccf61161f77c57b9aa45f78cb7f69b7d91f3e9f - stream-video-react-native: ffc867d9eb2a97f0eaa8b4376322ed6e82a5383d + stream-video-react-native: 363520e27b8cf267eb824cb32d10ea7f09bf09e8 TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654 Yoga: 1b901a6d6eeba4e8a2e8f308f708691cdb5db312 diff --git a/sample-apps/react-native/dogfood/src/components/ActiveCall.tsx b/sample-apps/react-native/dogfood/src/components/ActiveCall.tsx index 07ec207fbe..8189c23467 100644 --- a/sample-apps/react-native/dogfood/src/components/ActiveCall.tsx +++ b/sample-apps/react-native/dogfood/src/components/ActiveCall.tsx @@ -15,14 +15,15 @@ import { useOrientation } from '../hooks/useOrientation'; type ActiveCallProps = CallControlsComponentProps & { onBackPressed?: () => void; + onHangupCallHandler?: () => void; onCallEnded: () => void; }; export const ActiveCall = ({ onChatOpenHandler, onBackPressed, - onCallEnded, onHangupCallHandler, + onCallEnded, unreadCountIndicator, }: ActiveCallProps) => { const [isCallParticipantsVisible, setIsCallParticipantsVisible] = @@ -62,7 +63,7 @@ export const ActiveCall = ({ diff --git a/sample-apps/react-native/dogfood/src/components/Button.tsx b/sample-apps/react-native/dogfood/src/components/Button.tsx index 21f7a7b4a1..a78699d58b 100644 --- a/sample-apps/react-native/dogfood/src/components/Button.tsx +++ b/sample-apps/react-native/dogfood/src/components/Button.tsx @@ -9,7 +9,8 @@ import { ViewStyle, } from 'react-native'; import { BUTTON_HEIGHT } from '../constants'; -import { useTheme } from '@stream-io/video-react-native-sdk'; +import { defaultTheme, useTheme } from '@stream-io/video-react-native-sdk'; +import { Theme } from 'stream-chat-react-native'; type ButtonPropTypes = Omit & { title: string; @@ -42,27 +43,28 @@ export const Button = ({ }; const useStyles = () => { - const { theme } = useTheme(); + const appTheme = useTheme()?.theme || defaultTheme; + return useMemo( () => StyleSheet.create({ button: { - backgroundColor: theme.colors.primary, + backgroundColor: appTheme.colors.primary, justifyContent: 'center', borderRadius: 8, height: BUTTON_HEIGHT, - paddingHorizontal: theme.variants.spacingSizes.md, + paddingHorizontal: appTheme.variants.spacingSizes.md, }, buttonText: { - color: theme.colors.static_white, - fontWeight: theme.typefaces.heading6.fontWeight, + color: appTheme.colors.static_white, + fontWeight: appTheme.typefaces.heading6.fontWeight, textAlign: 'center', fontSize: 17, }, disabledButtonStyle: { - backgroundColor: theme.colors.disabled, + backgroundColor: appTheme.colors.disabled, }, }), - [theme], + [appTheme], ); }; diff --git a/yarn.lock b/yarn.lock index bbf280b456..52b10d195b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -25376,17 +25376,17 @@ __metadata: "typescript@patch:typescript@4.9.5#~builtin": version: 4.9.5 - resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin::version=4.9.5&hash=23ec76" + resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin::version=4.9.5&hash=289587" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: ab417a2f398380c90a6cf5a5f74badd17866adf57f1165617d6a551f059c3ba0a3e4da0d147b3ac5681db9ac76a303c5876394b13b3de75fdd5b1eaa06181c9d + checksum: 1f8f3b6aaea19f0f67cba79057674ba580438a7db55057eb89cc06950483c5d632115c14077f6663ea76fd09fce3c190e6414bb98582ec80aa5a4eaf345d5b68 languageName: node linkType: hard "typescript@patch:typescript@^4.6.4 || ^5.0.0#~builtin, typescript@patch:typescript@^5.5.2#~builtin": version: 5.5.2 - resolution: "typescript@patch:typescript@npm%3A5.5.2#~builtin::version=5.5.2&hash=85af82" + resolution: "typescript@patch:typescript@npm%3A5.5.2#~builtin::version=5.5.2&hash=29ae49" bin: tsc: bin/tsc tsserver: bin/tsserver From ac283e7baf93cee0a621cf918686549b35d64ab1 Mon Sep 17 00:00:00 2001 From: Kristian Martinoski Date: Tue, 15 Oct 2024 13:58:23 +0200 Subject: [PATCH 02/36] feat: add icons and foundation for the top left call controls --- .../Call/CallTopView/CallTopView.tsx | 30 +------- .../Call/CallTopView/TopLeftControls.tsx | 75 +++++++++++++++++++ .../react-native-sdk/src/icons/Effects.tsx | 17 +++++ .../react-native-sdk/src/icons/FlipCamera.tsx | 17 +++++ packages/react-native-sdk/src/icons/Grid.tsx | 17 +++++ .../dogfood/src/components/Button.tsx | 14 +++- 6 files changed, 140 insertions(+), 30 deletions(-) create mode 100644 packages/react-native-sdk/src/components/Call/CallTopView/TopLeftControls.tsx create mode 100644 packages/react-native-sdk/src/icons/Effects.tsx create mode 100644 packages/react-native-sdk/src/icons/FlipCamera.tsx create mode 100644 packages/react-native-sdk/src/icons/Grid.tsx diff --git a/packages/react-native-sdk/src/components/Call/CallTopView/CallTopView.tsx b/packages/react-native-sdk/src/components/Call/CallTopView/CallTopView.tsx index f395fe39fb..04e3b57b29 100644 --- a/packages/react-native-sdk/src/components/Call/CallTopView/CallTopView.tsx +++ b/packages/react-native-sdk/src/components/Call/CallTopView/CallTopView.tsx @@ -16,22 +16,14 @@ import { useTheme } from '../../../contexts/ThemeContext'; import { HangUpCallButton } from '..'; import { colors } from '../../..'; import { DurationBadge } from './DurationBadge'; +import { TopLeftControls } from './TopLeftControls'; export type CallTopViewProps = { - /** - * Handler to be called when the back button is pressed in the CallTopView. - * @returns void - */ - onBackPressed?: () => void; /** * Handler to be called when the hangup button is pressed in the CallTopView. * @returns void */ onHangupCallHandler?: () => void; - /** - * Title to be rendered at the center of the Header. - */ - title?: string; /** * Style to override the container of the CallTopView. */ @@ -43,9 +35,7 @@ export type CallTopViewProps = { }; export const CallTopView = ({ - onBackPressed, onHangupCallHandler, - title = 'test', style: styleProp, }: CallTopViewProps) => { const [callTopViewHeight, setCallTopViewHeight] = useState(0); @@ -78,22 +68,7 @@ export const CallTopView = ({ - {onBackPressed && ( - [ - styles.backIconContainer, - { - opacity: pressed ? 0.2 : 1, - height: iconSizes.md, - width: iconSizes.md, - }, - callTopView.backIconContainer, - ]} - onPress={onBackPressed} - > - - - )} + @@ -116,6 +91,7 @@ const useStyles = () => { StyleSheet.create({ content: { position: 'absolute', + backgroundColor: 'black', top: 0, flexDirection: 'row', paddingTop: 24, diff --git a/packages/react-native-sdk/src/components/Call/CallTopView/TopLeftControls.tsx b/packages/react-native-sdk/src/components/Call/CallTopView/TopLeftControls.tsx new file mode 100644 index 0000000000..8330a0b4d9 --- /dev/null +++ b/packages/react-native-sdk/src/components/Call/CallTopView/TopLeftControls.tsx @@ -0,0 +1,75 @@ +import React, { useState, useEffect, useMemo } from 'react'; +import { + View, + StyleSheet, + Text, + Pressable, + StyleProp, + ViewStyle, +} from 'react-native'; +import { Grid } from '../../../icons/Grid'; +import { FlipCamera } from '../../../icons/FlipCamera'; +import { useTheme } from '../../..'; +import { Effects } from '../../../icons/Effects'; + +export const TopLeftControls = () => { + const styles = useStyles(); + return ( + + + + + + ); +}; + +const useStyles = () => { + const { theme } = useTheme(); + + return useMemo( + () => + StyleSheet.create({ + content: { + display: 'flex', + flexDirection: 'row', + padding: 5, + gap: 6, + }, + // backIconContainer: { + // // Added to compensate the participant badge surface area + // marginLeft: 8, + // }, + // leftElement: { + // flex: 1, + // alignItems: 'flex-start', + // }, + // centerElement: { + // flex: 1, + // alignItems: 'center', + // flexGrow: 3, + // }, + // rightElement: { + // flex: 1, + // alignItems: 'flex-end', + // }, + // centerWrapper: { + // backgroundColor: colors.buttonSecondaryDefault, + // borderRadius: 8, + // width: 60, + // display: 'flex', + // flexDirection: 'row', + // height: 32, + // padding: 6, + // justifyContent: 'center', + // alignItems: 'center', + // // gap: 4, + // }, + // timer: { + // color: colors.typePrimary, + // fontSize: 13, + // fontWeight: '600', + // }, + }), + [theme] + ); +}; diff --git a/packages/react-native-sdk/src/icons/Effects.tsx b/packages/react-native-sdk/src/icons/Effects.tsx new file mode 100644 index 0000000000..c2629712bc --- /dev/null +++ b/packages/react-native-sdk/src/icons/Effects.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 Effects = ({ color, size }: Props) => ( + + + +); diff --git a/packages/react-native-sdk/src/icons/FlipCamera.tsx b/packages/react-native-sdk/src/icons/FlipCamera.tsx new file mode 100644 index 0000000000..2dee56fd46 --- /dev/null +++ b/packages/react-native-sdk/src/icons/FlipCamera.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 FlipCamera = ({ color, size }: Props) => ( + + + +); diff --git a/packages/react-native-sdk/src/icons/Grid.tsx b/packages/react-native-sdk/src/icons/Grid.tsx new file mode 100644 index 0000000000..2f64742f5f --- /dev/null +++ b/packages/react-native-sdk/src/icons/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/components/Button.tsx b/sample-apps/react-native/dogfood/src/components/Button.tsx index a78699d58b..cd47db5fb0 100644 --- a/sample-apps/react-native/dogfood/src/components/Button.tsx +++ b/sample-apps/react-native/dogfood/src/components/Button.tsx @@ -9,8 +9,11 @@ import { ViewStyle, } from 'react-native'; import { BUTTON_HEIGHT } from '../constants'; -import { defaultTheme, useTheme } from '@stream-io/video-react-native-sdk'; -import { Theme } from 'stream-chat-react-native'; +import { + Theme, + defaultTheme, + useTheme, +} from '@stream-io/video-react-native-sdk'; type ButtonPropTypes = Omit & { title: string; @@ -43,7 +46,12 @@ export const Button = ({ }; const useStyles = () => { - const appTheme = useTheme()?.theme || defaultTheme; + let appTheme: Theme; + try { + appTheme = useTheme()?.theme; + } catch (e) { + appTheme = defaultTheme; + } return useMemo( () => From 49b6c58a5f26e6725b526e6fc47e84e9722d7a1d Mon Sep 17 00:00:00 2001 From: Kristian Martinoski Date: Tue, 15 Oct 2024 20:21:11 +0200 Subject: [PATCH 03/36] feat: call top component updates, still work in progress --- .../Call/CallContent/CallContent.tsx | 46 ++++++------------ .../Call/CallControls/FlipCameraButton.tsx | 46 ++++++++++++++++++ .../Call/CallControls/HangupCallButton.tsx | 13 ++--- .../Call/CallControls/ToggleViewButton.tsx | 46 ++++++++++++++++++ .../Call/CallControls/VideoEffectsButton.tsx | 47 +++++++++++++++++++ .../components/Call/CallControls/index.tsx | 2 + .../Call/CallLayout/CallParticipantsGrid.tsx | 6 +-- .../CallParticipantsList.tsx | 4 +- .../Call/CallTopView/CallTopView.tsx | 27 ++++++----- .../Call/CallTopView/DurationBadge.tsx | 44 +++++------------ .../Call/CallTopView/TopLeftControls.tsx | 18 +++---- .../ParticipantView/ParticipantView.tsx | 2 + .../src/icons/IconWrapper.tsx | 15 ++++++ 13 files changed, 219 insertions(+), 97 deletions(-) create mode 100644 packages/react-native-sdk/src/components/Call/CallControls/FlipCameraButton.tsx create mode 100644 packages/react-native-sdk/src/components/Call/CallControls/ToggleViewButton.tsx create mode 100644 packages/react-native-sdk/src/components/Call/CallControls/VideoEffectsButton.tsx create mode 100644 packages/react-native-sdk/src/icons/IconWrapper.tsx 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 f31f6c1725..b40120eeaf 100644 --- a/packages/react-native-sdk/src/components/Call/CallContent/CallContent.tsx +++ b/packages/react-native-sdk/src/components/Call/CallContent/CallContent.tsx @@ -71,7 +71,6 @@ export type CallContentProps = Pick< HangUpCallButtonProps, 'onHangupCallHandler' > & - Pick & CallContentComponentProps & { /** * This switches the participant's layout between the grid and the spotlight mode. @@ -97,7 +96,6 @@ export type CallContentProps = Pick< }; export const CallContent = ({ - onBackPressed, onHangupCallHandler, CallParticipantsList, CallTopView = DefaultCallTopView, @@ -209,41 +207,26 @@ export const CallContent = ({ )} + {!isInPiPMode && CallTopView && ( + + )} - - {!isInPiPMode && CallTopView && ( - - )} - {showFloatingView && FloatingParticipantView && ( - - )} - - {showSpotlightLayout ? ( - - ) : ( - + {showFloatingView && FloatingParticipantView && ( + )} {showSpotlightLayout ? ( @@ -267,6 +250,7 @@ export const CallContent = ({ const styles = StyleSheet.create({ container: { flex: 1 }, view: { + // backgroundColor: 'red', ...StyleSheet.absoluteFillObject, zIndex: Z_INDEX.IN_FRONT, }, diff --git a/packages/react-native-sdk/src/components/Call/CallControls/FlipCameraButton.tsx b/packages/react-native-sdk/src/components/Call/CallControls/FlipCameraButton.tsx new file mode 100644 index 0000000000..7a48d09c19 --- /dev/null +++ b/packages/react-native-sdk/src/components/Call/CallControls/FlipCameraButton.tsx @@ -0,0 +1,46 @@ +import React, { useState } from 'react'; +import { CallControlsButton } from '@stream-io/video-react-native-sdk'; +import { IconWrapper } from '../../../icons/IconWrapper'; +import { FlipCamera } from '../../../icons/FlipCamera'; +import { useTheme } from '../../..'; + +/** + * The props for the Flip Camera Button in the camera Controls. + */ +export type FlipCameraButtonProps = { + /** + * Handler to be called when the flip camera button is pressed. + * @returns void + */ + onPressHandler?: () => void; +}; + +/** + * The flip camera Button is used in the camera Controls component + * and allows the user to toggle camera flipping. + */ +export const FlipCameraButton = ({ onPressHandler }: FlipCameraButtonProps) => { + const { + theme: { colors, flipCameraButton, defaults, variants }, + } = useTheme(); + const [isCameraFlipped, setIsCameraFlipped] = useState(false); + const buttonColor = isCameraFlipped ? colors.base5 : colors.base5; + + return ( + { + if (onPressHandler) { + onPressHandler(); + } + setIsCameraFlipped(!isCameraFlipped); + }} + color={buttonColor} + style={flipCameraButton} + > + + + + + ); +}; diff --git a/packages/react-native-sdk/src/components/Call/CallControls/HangupCallButton.tsx b/packages/react-native-sdk/src/components/Call/CallControls/HangupCallButton.tsx index 36dab6b8e0..feceabd80d 100644 --- a/packages/react-native-sdk/src/components/Call/CallControls/HangupCallButton.tsx +++ b/packages/react-native-sdk/src/components/Call/CallControls/HangupCallButton.tsx @@ -6,6 +6,7 @@ import { ButtonTestIds } from '../../../constants/TestIds'; import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings'; import { CallingState } from '@stream-io/video-client'; import { useTheme } from '../../../contexts/ThemeContext'; +import { IconWrapper } from '../../../icons/IconWrapper'; /** * The props for the Hang up call button in the Call Controls. @@ -72,12 +73,12 @@ export const HangUpCallButton = ({ size={size} testID={ButtonTestIds.HANG_UP_CALL} > - {/* */} - - {/* */} + + + ); }; diff --git a/packages/react-native-sdk/src/components/Call/CallControls/ToggleViewButton.tsx b/packages/react-native-sdk/src/components/Call/CallControls/ToggleViewButton.tsx new file mode 100644 index 0000000000..0168497664 --- /dev/null +++ b/packages/react-native-sdk/src/components/Call/CallControls/ToggleViewButton.tsx @@ -0,0 +1,46 @@ +import React, { useState } from 'react'; +import { CallControlsButton } from '@stream-io/video-react-native-sdk'; +import { IconWrapper } from '../../../icons/IconWrapper'; +import { useTheme } from '../../..'; +import { Grid } from '../../../icons/Grid'; + +/** + * The props for the Flip Camera Button in the camera Controls. + */ +export type ToggleViewButton = { + /** + * Handler to be called when the flip camera button is pressed. + * @returns void + */ + onPressHandler?: () => void; +}; + +/** + * The flip camera Button is used in the camera Controls component + * and allows the user to toggle camera flipping. + */ +export const ToggleViewButton = ({ onPressHandler }: ToggleViewButton) => { + const { + theme: { colors, flipCameraButton, defaults, variants }, + } = useTheme(); + const [isCameraFlipped, setIsCameraFlipped] = useState(false); + const buttonColor = isCameraFlipped ? colors.base5 : colors.base5; + + return ( + { + if (onPressHandler) { + onPressHandler(); + } + setIsCameraFlipped(!isCameraFlipped); + }} + color={buttonColor} + style={flipCameraButton} + > + + + + + ); +}; diff --git a/packages/react-native-sdk/src/components/Call/CallControls/VideoEffectsButton.tsx b/packages/react-native-sdk/src/components/Call/CallControls/VideoEffectsButton.tsx new file mode 100644 index 0000000000..55bd2b4323 --- /dev/null +++ b/packages/react-native-sdk/src/components/Call/CallControls/VideoEffectsButton.tsx @@ -0,0 +1,47 @@ +import React, { useState } from 'react'; +import { IconWrapper } from '../../../icons/IconWrapper'; +import { CallControlsButton, useTheme } from '../../..'; +import { Effects } from '../../../icons/Effects'; + +/** + * The props for the Flip Camera Button in the camera Controls. + */ +export type VideoEffectsButtonProps = { + /** + * Handler to be called when the flip camera button is pressed. + * @returns void + */ + onPressHandler?: () => void; +}; + +/** + * The flip camera Button is used in the camera Controls component + * and allows the user to toggle camera flipping. + */ +export const VideoEffectsButton = ({ + onPressHandler, +}: VideoEffectsButtonProps) => { + const { + theme: { colors, flipCameraButton, defaults, variants }, + } = useTheme(); + const [isButtonPressed, setIsButtonPressed] = useState(false); + const buttonColor = isButtonPressed ? colors.base5 : colors.base5; + + return ( + { + if (onPressHandler) { + onPressHandler(); + } + setIsButtonPressed(!isButtonPressed); + }} + color={buttonColor} + style={flipCameraButton} + > + + + + + ); +}; diff --git a/packages/react-native-sdk/src/components/Call/CallControls/index.tsx b/packages/react-native-sdk/src/components/Call/CallControls/index.tsx index 1e1d5b50de..d8f1d9ca0a 100644 --- a/packages/react-native-sdk/src/components/Call/CallControls/index.tsx +++ b/packages/react-native-sdk/src/components/Call/CallControls/index.tsx @@ -14,3 +14,5 @@ export * from './LobbyControls'; export * from './IncomingCallControls'; export * from './OutgoingCallControls'; export * from './ScreenShareToggleButton'; +export * from './FlipCameraButton'; +export * from './VideoEffectsButton'; 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 521f73fc24..d71bb8c128 100644 --- a/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsGrid.tsx +++ b/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsGrid.tsx @@ -100,7 +100,7 @@ export const CallParticipantsGrid = ({ style={[ styles.container, landscapeStyles, - { backgroundColor: colors.background2 }, + { backgroundColor: colors.sheetPrimary }, callParticipantsGrid.container, ]} testID={ComponentTestIds.CALL_PARTICIPANTS_GRID} @@ -118,7 +118,5 @@ export const CallParticipantsGrid = ({ }; const styles = StyleSheet.create({ - container: { - flex: 1, - }, + container: { flex: 1 }, }); 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 2bf3a37758..afc4a53309 100644 --- a/packages/react-native-sdk/src/components/Call/CallParticipantsList/CallParticipantsList.tsx +++ b/packages/react-native-sdk/src/components/Call/CallParticipantsList/CallParticipantsList.tsx @@ -252,9 +252,7 @@ export const CallParticipantsList = ({ }; const styles = StyleSheet.create({ - flexed: { - flex: 1, - }, + flexed: { flex: 1 }, participantWrapperHorizontal: { // note: if marginHorizontal is changed, be sure to change the width calculation in calculateParticipantViewSize function marginHorizontal: 8, diff --git a/packages/react-native-sdk/src/components/Call/CallTopView/CallTopView.tsx b/packages/react-native-sdk/src/components/Call/CallTopView/CallTopView.tsx index 04e3b57b29..75888dbd0b 100644 --- a/packages/react-native-sdk/src/components/Call/CallTopView/CallTopView.tsx +++ b/packages/react-native-sdk/src/components/Call/CallTopView/CallTopView.tsx @@ -51,8 +51,6 @@ export const CallTopView = ({ const styles = useStyles(); const { useCallCallingState } = useCallStateHooks(); const callingState = useCallCallingState(); - const { t } = useI18n(); - const isCallReconnecting = callingState === CallingState.RECONNECTING; const onLayout: React.ComponentProps['onLayout'] = (event) => { const { height, width } = event.nativeEvent.layout; @@ -63,7 +61,7 @@ export const CallTopView = ({ }; return ( - + {/* Component for the background of the CallTopView. Since it has a Linear Gradient, an SVG is used to render it. */} @@ -89,22 +87,28 @@ const useStyles = () => { return useMemo( () => StyleSheet.create({ + container: { + // borderWidth: 2, + // borderColor: 'red', + }, content: { position: 'absolute', backgroundColor: 'black', top: 0, flexDirection: 'row', - paddingTop: 24, - paddingBottom: 12, + paddingTop: 2, + paddingBottom: 2, + paddingRight: 12, + paddingLeft: 12, alignItems: 'center', - }, - backIconContainer: { - // Added to compensate the participant badge surface area - marginLeft: 8, + // borderWidth: 2, + // borderColor: 'red', }, leftElement: { - flex: 1, + flex: 2, alignItems: 'flex-start', + // borderWidth: 2, + // borderColor: 'red', }, centerElement: { flex: 1, @@ -112,7 +116,7 @@ const useStyles = () => { flexGrow: 3, }, rightElement: { - flex: 1, + flex: 2, alignItems: 'flex-end', }, centerWrapper: { @@ -125,7 +129,6 @@ const useStyles = () => { padding: 6, justifyContent: 'center', alignItems: 'center', - // gap: 4, }, timer: { color: colors.typePrimary, diff --git a/packages/react-native-sdk/src/components/Call/CallTopView/DurationBadge.tsx b/packages/react-native-sdk/src/components/Call/CallTopView/DurationBadge.tsx index ba052d7780..e0abea7f1b 100644 --- a/packages/react-native-sdk/src/components/Call/CallTopView/DurationBadge.tsx +++ b/packages/react-native-sdk/src/components/Call/CallTopView/DurationBadge.tsx @@ -8,9 +8,7 @@ export const DurationBadge = () => { const { theme: { colors, - typefaces, variants: { iconSizes }, - callTopView, }, } = useTheme(); const [duration, setDuration] = useState(0); @@ -32,8 +30,10 @@ export const DurationBadge = () => { const timestamp = `${minutes}:${seconds}`; return ( - - + + + + {timestamp} ); @@ -45,47 +45,25 @@ const useStyles = () => { return useMemo( () => StyleSheet.create({ - content: { - position: 'absolute', - top: 0, - flexDirection: 'row', - paddingTop: 24, - paddingBottom: 12, - alignItems: 'center', - }, - backIconContainer: { - // Added to compensate the participant badge surface area - marginLeft: 8, - }, - leftElement: { - flex: 1, - alignItems: 'flex-start', - }, - centerElement: { - flex: 1, - alignItems: 'center', - flexGrow: 3, - }, - rightElement: { - flex: 1, - alignItems: 'flex-end', - }, - centerWrapper: { + container: { backgroundColor: theme.colors.buttonSecondaryDefault, borderRadius: 8, - width: 90, display: 'flex', flexDirection: 'row', height: 32, - padding: 6, + paddingLeft: 20, + paddingRight: 20, justifyContent: 'center', alignItems: 'center', - // gap: 4, + }, + icon: { + marginTop: 3, }, timer: { color: theme.colors.typePrimary, fontSize: 13, fontWeight: '600', + width: 40, }, }), [theme] diff --git a/packages/react-native-sdk/src/components/Call/CallTopView/TopLeftControls.tsx b/packages/react-native-sdk/src/components/Call/CallTopView/TopLeftControls.tsx index 8330a0b4d9..baf43ae4c6 100644 --- a/packages/react-native-sdk/src/components/Call/CallTopView/TopLeftControls.tsx +++ b/packages/react-native-sdk/src/components/Call/CallTopView/TopLeftControls.tsx @@ -8,17 +8,16 @@ import { ViewStyle, } from 'react-native'; import { Grid } from '../../../icons/Grid'; -import { FlipCamera } from '../../../icons/FlipCamera'; -import { useTheme } from '../../..'; -import { Effects } from '../../../icons/Effects'; +import { FlipCameraButton, VideoEffectsButton, useTheme } from '../../..'; +import { ToggleViewButton } from '../CallControls/ToggleViewButton'; export const TopLeftControls = () => { const styles = useStyles(); return ( - - - + {}} /> + {}} /> + {}} /> ); }; @@ -30,10 +29,13 @@ const useStyles = () => { () => StyleSheet.create({ content: { - display: 'flex', + // display: 'flex', flexDirection: 'row', - padding: 5, + // padding: 5, gap: 6, + // width: 200, + // borderWidth: 2, + // borderColor: 'red', }, // backIconContainer: { // // Added to compensate the participant badge surface area 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 09a4037dbd..3f8f9b13ad 100644 --- a/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantView.tsx +++ b/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantView.tsx @@ -167,6 +167,8 @@ const styles = StyleSheet.create({ overflow: 'hidden', borderWidth: 2, borderColor: 'transparent', + margin: 8, + borderRadius: 16, }, footerContainer: { flexDirection: 'row', diff --git a/packages/react-native-sdk/src/icons/IconWrapper.tsx b/packages/react-native-sdk/src/icons/IconWrapper.tsx new file mode 100644 index 0000000000..d33057eb27 --- /dev/null +++ b/packages/react-native-sdk/src/icons/IconWrapper.tsx @@ -0,0 +1,15 @@ +import React from 'react'; +import { ReactNode } from 'react'; +import { View, StyleSheet } from 'react-native'; + +export const IconWrapper = ({ children }: { children: ReactNode }) => { + return {children}; +}; + +const styles = StyleSheet.create({ + container: { + justifyContent: 'center', + alignItems: 'center', + flex: 1, + }, +}); From 3d8bdcce6142bb14a24c042163a9cf66b9f404ae Mon Sep 17 00:00:00 2001 From: Kristian Martinoski Date: Wed, 16 Oct 2024 13:53:46 +0200 Subject: [PATCH 04/36] feat: configurable duration badge --- .../Call/CallTopView/CallTopView.tsx | 10 ++--- .../Call/CallTopView/DurationBadge.tsx | 41 +++++++++++------- .../Call/CallTopView/TopLeftControls.tsx | 43 +------------------ 3 files changed, 32 insertions(+), 62 deletions(-) diff --git a/packages/react-native-sdk/src/components/Call/CallTopView/CallTopView.tsx b/packages/react-native-sdk/src/components/Call/CallTopView/CallTopView.tsx index 75888dbd0b..67f98b6a5f 100644 --- a/packages/react-native-sdk/src/components/Call/CallTopView/CallTopView.tsx +++ b/packages/react-native-sdk/src/components/Call/CallTopView/CallTopView.tsx @@ -51,6 +51,7 @@ export const CallTopView = ({ const styles = useStyles(); const { useCallCallingState } = useCallStateHooks(); const callingState = useCallCallingState(); + const [inProgress, setInProgress] = useState(true); const onLayout: React.ComponentProps['onLayout'] = (event) => { const { height, width } = event.nativeEvent.layout; @@ -66,11 +67,11 @@ export const CallTopView = ({ - + - + @@ -105,7 +106,7 @@ const useStyles = () => { // borderColor: 'red', }, leftElement: { - flex: 2, + flex: 1, alignItems: 'flex-start', // borderWidth: 2, // borderColor: 'red', @@ -113,10 +114,9 @@ const useStyles = () => { centerElement: { flex: 1, alignItems: 'center', - flexGrow: 3, }, rightElement: { - flex: 2, + flex: 1, alignItems: 'flex-end', }, centerWrapper: { diff --git a/packages/react-native-sdk/src/components/Call/CallTopView/DurationBadge.tsx b/packages/react-native-sdk/src/components/Call/CallTopView/DurationBadge.tsx index e0abea7f1b..c779ff7c32 100644 --- a/packages/react-native-sdk/src/components/Call/CallTopView/DurationBadge.tsx +++ b/packages/react-native-sdk/src/components/Call/CallTopView/DurationBadge.tsx @@ -2,9 +2,11 @@ import React, { useState, useEffect, useMemo } from 'react'; import { View, Text, StyleSheet } from 'react-native'; import { CallDuration } from '../../../icons'; import { useTheme } from '../../..'; +import RecordCall from '../../../icons/RecordCall'; +import { IconWrapper } from '../../../icons/IconWrapper'; // TODO: move to dogfood app -export const DurationBadge = () => { +export const DurationBadge = (props: any) => { const { theme: { colors, @@ -20,7 +22,7 @@ export const DurationBadge = () => { clearInterval(id); }; }, []); - const styles = useStyles(); + const styles = useStyles(props); // Format duration to MM:SS const minutes = Math.floor(duration / 60) @@ -28,18 +30,24 @@ export const DurationBadge = () => { .padStart(2, '0'); const seconds = (duration % 60).toString().padStart(2, '0'); const timestamp = `${minutes}:${seconds}`; + const text = props.inProgress ? 'Recording in progress...' : timestamp; + const icon = false ? ( + + ) : ( + + ); return ( - - - - {timestamp} + + {icon} + + {text} ); }; -const useStyles = () => { +const useStyles = (props: any) => { const { theme } = useTheme(); return useMemo( @@ -48,22 +56,23 @@ const useStyles = () => { container: { backgroundColor: theme.colors.buttonSecondaryDefault, borderRadius: 8, - display: 'flex', flexDirection: 'row', - height: 32, - paddingLeft: 20, - paddingRight: 20, + height: 36, + paddingHorizontal: 12, // Use equal padding on both sides justifyContent: 'center', alignItems: 'center', + width: props.inProgress ? 200 : 80, + gap: 7, }, - icon: { - marginTop: 3, - }, - timer: { + text: { color: theme.colors.typePrimary, fontSize: 13, fontWeight: '600', - width: 40, + marginLeft: 3, // Add some space between icon and text + flexShrink: 0, // Allow text to shrink if needed + }, + icon: { + marginTop: 3, }, }), [theme] diff --git a/packages/react-native-sdk/src/components/Call/CallTopView/TopLeftControls.tsx b/packages/react-native-sdk/src/components/Call/CallTopView/TopLeftControls.tsx index baf43ae4c6..40f39b8eca 100644 --- a/packages/react-native-sdk/src/components/Call/CallTopView/TopLeftControls.tsx +++ b/packages/react-native-sdk/src/components/Call/CallTopView/TopLeftControls.tsx @@ -11,13 +11,13 @@ import { Grid } from '../../../icons/Grid'; import { FlipCameraButton, VideoEffectsButton, useTheme } from '../../..'; import { ToggleViewButton } from '../CallControls/ToggleViewButton'; -export const TopLeftControls = () => { +export const TopLeftControls = (props: any) => { const styles = useStyles(); return ( {}} /> {}} /> - {}} /> + {!props.inProgress && {}} />} ); }; @@ -29,48 +29,9 @@ const useStyles = () => { () => StyleSheet.create({ content: { - // display: 'flex', flexDirection: 'row', - // padding: 5, gap: 6, - // width: 200, - // borderWidth: 2, - // borderColor: 'red', }, - // backIconContainer: { - // // Added to compensate the participant badge surface area - // marginLeft: 8, - // }, - // leftElement: { - // flex: 1, - // alignItems: 'flex-start', - // }, - // centerElement: { - // flex: 1, - // alignItems: 'center', - // flexGrow: 3, - // }, - // rightElement: { - // flex: 1, - // alignItems: 'flex-end', - // }, - // centerWrapper: { - // backgroundColor: colors.buttonSecondaryDefault, - // borderRadius: 8, - // width: 60, - // display: 'flex', - // flexDirection: 'row', - // height: 32, - // padding: 6, - // justifyContent: 'center', - // alignItems: 'center', - // // gap: 4, - // }, - // timer: { - // color: colors.typePrimary, - // fontSize: 13, - // fontWeight: '600', - // }, }), [theme] ); From 1282dd842949937a18c3b7f5793ba22cc69669fb Mon Sep 17 00:00:00 2001 From: Kristian Martinoski Date: Wed, 16 Oct 2024 20:15:37 +0200 Subject: [PATCH 05/36] feat: network and speech status indicators --- .../Call/CallControls/HangupCallButton.tsx | 6 +- .../Call/CallTopView/CallTopView.tsx | 9 +- .../Call/CallTopView/DurationBadge.tsx | 4 +- .../ParticipantView/ParticipantLabel.tsx | 49 ++++++----- .../ParticipantNetworkQualityIndicator.tsx | 22 +++-- .../ParticipantView/ParticipantView.tsx | 1 - .../ParticipantView/SpeechIndicator.tsx | 86 +++++++++++++++++++ .../react-native/dogfood/ios/Podfile.lock | 17 ++-- .../dogfood/src/components/ActiveCall.tsx | 17 +++- .../CallControlls/CallControlsComponent.tsx | 82 ++++++++++-------- .../dogfood/src/components/MeetingUI.tsx | 1 + yarn.lock | 2 + 12 files changed, 211 insertions(+), 85 deletions(-) create mode 100644 packages/react-native-sdk/src/components/Participant/ParticipantView/SpeechIndicator.tsx diff --git a/packages/react-native-sdk/src/components/Call/CallControls/HangupCallButton.tsx b/packages/react-native-sdk/src/components/Call/CallControls/HangupCallButton.tsx index feceabd80d..45c4463369 100644 --- a/packages/react-native-sdk/src/components/Call/CallControls/HangupCallButton.tsx +++ b/packages/react-native-sdk/src/components/Call/CallControls/HangupCallButton.tsx @@ -52,9 +52,9 @@ export const HangUpCallButton = ({ return; } try { - if (callingState === CallingState.LEFT) { - return; - } + // if (callingState === CallingState.LEFT) { + // return; + // } await call?.leave(); if (onHangupCallHandler) { onHangupCallHandler(); diff --git a/packages/react-native-sdk/src/components/Call/CallTopView/CallTopView.tsx b/packages/react-native-sdk/src/components/Call/CallTopView/CallTopView.tsx index c5d7196bff..f013140566 100644 --- a/packages/react-native-sdk/src/components/Call/CallTopView/CallTopView.tsx +++ b/packages/react-native-sdk/src/components/Call/CallTopView/CallTopView.tsx @@ -71,7 +71,7 @@ export const CallTopView = ({ - + @@ -84,13 +84,10 @@ const useStyles = () => { return useMemo( () => StyleSheet.create({ - container: { - // borderWidth: 2, - // borderColor: 'red', - }, + container: {}, content: { position: 'absolute', - backgroundColor: 'black', + backgroundColor: theme.colors.sheetPrimary, top: 0, flexDirection: 'row', paddingTop: 2, diff --git a/packages/react-native-sdk/src/components/Call/CallTopView/DurationBadge.tsx b/packages/react-native-sdk/src/components/Call/CallTopView/DurationBadge.tsx index c779ff7c32..573d3dc0b4 100644 --- a/packages/react-native-sdk/src/components/Call/CallTopView/DurationBadge.tsx +++ b/packages/react-native-sdk/src/components/Call/CallTopView/DurationBadge.tsx @@ -2,7 +2,7 @@ import React, { useState, useEffect, useMemo } from 'react'; import { View, Text, StyleSheet } from 'react-native'; import { CallDuration } from '../../../icons'; import { useTheme } from '../../..'; -import RecordCall from '../../../icons/RecordCall'; +// import RecordCall from '../../../icons/RecordCall'; import { IconWrapper } from '../../../icons/IconWrapper'; // TODO: move to dogfood app @@ -34,7 +34,7 @@ export const DurationBadge = (props: any) => { const icon = false ? ( ) : ( - + ); return ( 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 de58feced2..75b38fbd5b 100644 --- a/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantLabel.tsx +++ b/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantLabel.tsx @@ -6,6 +6,7 @@ import { ComponentTestIds } from '../../../constants/TestIds'; import { ParticipantViewProps } from './ParticipantView'; import { Z_INDEX } from '../../../constants'; import { useTheme } from '../../../contexts/ThemeContext'; +import SpeechIndicator from './SpeechIndicator'; /** * Props for the ParticipantLabel component. @@ -65,10 +66,7 @@ export const ParticipantLabel = ({ @@ -93,29 +91,31 @@ export const ParticipantLabel = ({ - - {participantLabel} - + + + {participantLabel} + + + + + {isPinningEnabled && ( { + // const { participant } = useParticipantViewContext(); + // const { isSpeaking, isDominantSpeaker } = participant; + const isSpeaking = false; + const isDominantSpeaker = true; + + const animationValues = [ + useRef(new Animated.Value(0.6)).current, + useRef(new Animated.Value(0.6)).current, + useRef(new Animated.Value(0.6)).current, + ]; + + useEffect(() => { + if (isSpeaking) { + animationValues.forEach((animatedValue, index) => { + Animated.loop( + Animated.sequence([ + Animated.timing(animatedValue, { + toValue: index % 2 === 0 ? 0.3 : 1.1, + duration: (index + 1) * 300, + useNativeDriver: true, + }), + Animated.timing(animatedValue, { + toValue: 0.6, + duration: (index + 1) * 300, + useNativeDriver: true, + }), + ]) + ).start(); + }); + } else { + animationValues.forEach((animatedValue) => { + animatedValue.setValue(0.3); // Set a smaller value for a reduced default height + }); + } + }, [isSpeaking, animationValues]); + + const barStyle = (animatedValue: any) => ({ + transform: [{ scaleY: animatedValue }], + }); + + return ( + + {animationValues.map((animatedValue, index) => ( + + ))} + + ); +}; + +const styles = StyleSheet.create({ + container: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + height: 25, + width: 25, + borderRadius: 5, + gap: 1, + backgroundColor: 'rgba(12, 13, 14, 0.65)', + padding: 5, + }, + smallBar: { + height: '30%', // Smaller default height when animation is not running + }, + bar: { + width: 3, + height: '100%', + backgroundColor: '#005fff', // Default color, can be replaced with a theme color + borderRadius: 2, + }, + dominant: { + // backgroundColor: 'rgba(0, 255, 0, 0.2)', // Optional style for dominant speaker + }, +}); + +export default SpeechIndicator; diff --git a/sample-apps/react-native/dogfood/ios/Podfile.lock b/sample-apps/react-native/dogfood/ios/Podfile.lock index fa7ea9fdbd..3ec1c72477 100644 --- a/sample-apps/react-native/dogfood/ios/Podfile.lock +++ b/sample-apps/react-native/dogfood/ios/Podfile.lock @@ -916,7 +916,7 @@ PODS: - React-Core - react-native-netinfo (11.3.0): - React-Core - - react-native-safe-area-context (4.8.2): + - react-native-safe-area-context (4.10.5): - React-Core - react-native-video (6.0.0-beta.5): - React-Core @@ -1126,15 +1126,16 @@ PODS: - React-Core - RNReactNativeHapticFeedback (2.0.3): - React-Core - - RNReanimated (3.7.0): + - RNReanimated (3.10.1): - glog - RCT-Folly (= 2022.05.16.00) - React-Core - ReactCommon/turbomodule/core - - RNScreens (3.29.0): + - RNScreens (3.31.1): - glog - RCT-Folly (= 2022.05.16.00) - React-Core + - React-RCTImage - RNSVG (13.9.0): - React-Core - RNVoipPushNotification (3.3.2): @@ -1148,7 +1149,7 @@ PODS: - stream-react-native-webrtc (118.1.0): - JitsiWebRTC (~> 118.0.0) - React-Core - - stream-video-react-native (1.1.1): + - stream-video-react-native (1.1.6): - glog - RCT-Folly (= 2022.05.16.00) - React-Core @@ -1439,7 +1440,7 @@ SPEC CHECKSUMS: react-native-image-resizer: d9fb629a867335bdc13230ac2a58702bb8c8828f react-native-mmkv: 7da5e18e55c04a9af9a7e0ab9792a1e8d33765a1 react-native-netinfo: 299dad906cdbf3b67bcc6f693c807f98bdd127cc - react-native-safe-area-context: 0ee144a6170530ccc37a0fd9388e28d06f516a89 + react-native-safe-area-context: a240ad4b683349e48b1d51fed1611138d1bdad97 react-native-video: df7d8a4c8568ed4a31b28e6cd2bfa4a98b186e36 React-nativeconfig: ca8b90c736cf3be019cb332ca42d93dd95b32e05 React-NativeModulesApple: 1fdffcce7772e274baeab33a1900f45feba86cbd @@ -1472,14 +1473,14 @@ SPEC CHECKSUMS: RNNotifee: f3c01b391dd8e98e67f539f9a35a9cbcd3bae744 RNPermissions: 4d64d2c763b20e5db08dfb8f1ef541f95c0d28c1 RNReactNativeHapticFeedback: afa5bf2794aecbb2dba2525329253da0d66656df - RNReanimated: ea7e98bf4f1167ca3cd3c348604d34dc4294e848 - RNScreens: 2b73f5eb2ac5d94fbd61fa4be0bfebd345716825 + RNReanimated: 8cd12e58bbedca2b0b62a3c3872d3629fc2e1583 + RNScreens: 35f3344904f6bba888c0e61532278436e275de3c RNSVG: 53c661b76829783cdaf9b7a57258f3d3b4c28315 RNVoipPushNotification: 543e18f83089134a35e7f1d2eba4c8b1f7776b08 SocketRocket: f32cd54efbe0f095c4d7594881e52619cfe80b17 stream-io-video-filters-react-native: 8c345e6adf5164646696d45f9962c4199b2fe3b9 stream-react-native-webrtc: 4ccf61161f77c57b9aa45f78cb7f69b7d91f3e9f - stream-video-react-native: ff287d6bc897fffdf7b07ed3828e53bbf74bf576 + stream-video-react-native: 5f9c3101cff99715e6a6139b24a8f43539421e2b TOCropViewController: 80b8985ad794298fb69d3341de183f33d1853654 Yoga: 1b901a6d6eeba4e8a2e8f308f708691cdb5db312 diff --git a/sample-apps/react-native/dogfood/src/components/ActiveCall.tsx b/sample-apps/react-native/dogfood/src/components/ActiveCall.tsx index 47af50a4e0..90c9dcf163 100644 --- a/sample-apps/react-native/dogfood/src/components/ActiveCall.tsx +++ b/sample-apps/react-native/dogfood/src/components/ActiveCall.tsx @@ -4,7 +4,7 @@ import { CallContent, useTheme, } from '@stream-io/video-react-native-sdk'; -import { ActivityIndicator, StyleSheet, View } from 'react-native'; +import { ActivityIndicator, StatusBar, StyleSheet, View } from 'react-native'; import { SafeAreaView } from 'react-native-safe-area-context'; import { ParticipantsInfoList } from './ParticipantsInfoList'; import { @@ -59,6 +59,8 @@ export const ActiveCall = ({ return ( + {/* */} + - + ); }; @@ -85,7 +87,16 @@ const useStyles = () => { container: { flex: 1 }, callContent: { flex: 1 }, safeArea: { flex: 1, paddingBottom: theme.variants.insets.bottom }, - unsafeArea: { + topUnsafeArea: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + height: theme.variants.insets.top, + backgroundColor: theme.colors.sheetPrimary, // Change to your desired color + zIndex: Z_INDEX.IN_FRONT, + }, + bottomUnsafeArea: { position: 'absolute', left: 0, right: 0, diff --git a/sample-apps/react-native/dogfood/src/components/CallControlls/CallControlsComponent.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/CallControlsComponent.tsx index add4173789..e2960e1a18 100644 --- a/sample-apps/react-native/dogfood/src/components/CallControlls/CallControlsComponent.tsx +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/CallControlsComponent.tsx @@ -3,8 +3,9 @@ import { ToggleAudioPublishingButton, ToggleVideoPublishingButton, useCallStateHooks, + useTheme, } from '@stream-io/video-react-native-sdk'; -import React from 'react'; +import React, { useMemo } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { appTheme } from '../../theme'; import { Z_INDEX } from '../../constants'; @@ -30,6 +31,7 @@ export const CallControlsComponent = ({ }: CallControlsComponentProps) => { const { useMicrophoneState } = useCallStateHooks(); const { isSpeakingWhileMuted } = useMicrophoneState(); + const styles = useStyles(); return ( @@ -58,38 +60,46 @@ export const CallControlsComponent = ({ ); }; -const styles = StyleSheet.create({ - speakingLabelContainer: { - backgroundColor: appTheme.colors.static_overlay, - paddingVertical: 10, - width: '100%', - }, - label: { - textAlign: 'center', - color: appTheme.colors.static_white, - }, - callControlsWrapper: { - display: 'flex', - flexDirection: 'row', - justifyContent: 'flex-start', - zIndex: Z_INDEX.IN_FRONT, - }, - container: { - paddingVertical: 16, - paddingHorizontal: 16, - backgroundColor: 'black', - height: 76, - }, - left: { - flex: 2.5, - flexDirection: 'row', - alignItems: 'flex-start', - gap: 6, - }, - right: { - flex: 1, - flexDirection: 'row', - justifyContent: 'flex-end', - gap: 6, - }, -}); +const useStyles = () => { + const { theme } = useTheme(); + + return useMemo( + () => + StyleSheet.create({ + speakingLabelContainer: { + backgroundColor: appTheme.colors.static_overlay, + paddingVertical: 10, + width: '100%', + }, + label: { + textAlign: 'center', + color: appTheme.colors.static_white, + }, + callControlsWrapper: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'flex-start', + zIndex: Z_INDEX.IN_FRONT, + }, + container: { + paddingVertical: 16, + paddingHorizontal: 16, + backgroundColor: theme.colors.sheetPrimary, + height: 76, + }, + left: { + flex: 2.5, + flexDirection: 'row', + alignItems: 'flex-start', + gap: 6, + }, + right: { + flex: 1, + flexDirection: 'row', + justifyContent: 'flex-end', + gap: 6, + }, + }), + [theme], + ); +}; diff --git a/sample-apps/react-native/dogfood/src/components/MeetingUI.tsx b/sample-apps/react-native/dogfood/src/components/MeetingUI.tsx index f17a03e19a..ce475a06e5 100644 --- a/sample-apps/react-native/dogfood/src/components/MeetingUI.tsx +++ b/sample-apps/react-native/dogfood/src/components/MeetingUI.tsx @@ -128,6 +128,7 @@ export const MeetingUI = ({ callId, navigation, route }: Props) => { return ( Date: Thu, 17 Oct 2024 14:14:22 +0200 Subject: [PATCH 06/36] chore: move top call components to dogfood app --- .../Call/CallContent/CallContent.tsx | 9 +--- .../Call/CallControls/CallControlsButton.tsx | 7 ++- .../Call/CallControls/FlipCameraButton.tsx | 46 ----------------- .../CallControls/ToggleCameraFaceButton.tsx | 33 ++++++++----- .../Call/CallControls/ToggleViewButton.tsx | 46 ----------------- .../Call/CallControls/VideoEffectsButton.tsx | 47 ------------------ .../components/Call/CallControls/index.tsx | 2 - .../Call/CallTopView/TopLeftControls.tsx | 38 -------------- .../src/components/Call/CallTopView/index.ts | 1 - .../Call/RingingCallContent/IncomingCall.tsx | 8 +-- .../Call/RingingCallContent/OutgoingCall.tsx | 8 +-- .../ParticipantView/ParticipantLabel.tsx | 6 ++- .../ParticipantView/SpeechIndicator.tsx | 27 ++++++---- .../src/icons/CameraSwitch.tsx | 7 +-- .../react-native-sdk/src/icons/FlipCamera.tsx | 17 ------- .../react-native-sdk/src/icons/RecordCall.tsx | 21 ++++++++ packages/react-native-sdk/src/icons/index.tsx | 1 - .../dogfood/src/assets}/CallDuration.tsx | 0 .../dogfood/src/assets/RecordCall.tsx | 23 --------- .../dogfood/src/components/ActiveCall.tsx | 8 ++- .../components/CallControlls}/CallTopView.tsx | 20 ++++---- .../CallControlls}/DurationBadge.tsx | 27 +++++----- .../CallControlls/LayoutSwitcherButton.tsx | 49 +++++++++++++++++++ .../CallControlls/RecordCallButton.tsx | 2 +- .../CallControlls/TopLeftControls.tsx | 34 +++++++++++++ .../components/VideoEffectsButton/index.tsx | 17 ++++++- 26 files changed, 210 insertions(+), 294 deletions(-) delete mode 100644 packages/react-native-sdk/src/components/Call/CallControls/FlipCameraButton.tsx delete mode 100644 packages/react-native-sdk/src/components/Call/CallControls/ToggleViewButton.tsx delete mode 100644 packages/react-native-sdk/src/components/Call/CallControls/VideoEffectsButton.tsx delete mode 100644 packages/react-native-sdk/src/components/Call/CallTopView/TopLeftControls.tsx delete mode 100644 packages/react-native-sdk/src/components/Call/CallTopView/index.ts delete mode 100644 packages/react-native-sdk/src/icons/FlipCamera.tsx create mode 100644 packages/react-native-sdk/src/icons/RecordCall.tsx rename {packages/react-native-sdk/src/icons => sample-apps/react-native/dogfood/src/assets}/CallDuration.tsx (100%) delete mode 100644 sample-apps/react-native/dogfood/src/assets/RecordCall.tsx rename {packages/react-native-sdk/src/components/Call/CallTopView => sample-apps/react-native/dogfood/src/components/CallControlls}/CallTopView.tsx (87%) rename {packages/react-native-sdk/src/components/Call/CallTopView => sample-apps/react-native/dogfood/src/components/CallControlls}/DurationBadge.tsx (74%) create mode 100644 sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherButton.tsx create mode 100644 sample-apps/react-native/dogfood/src/components/CallControlls/TopLeftControls.tsx 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 ffb44c1609..62827a9b9d 100644 --- a/packages/react-native-sdk/src/components/Call/CallContent/CallContent.tsx +++ b/packages/react-native-sdk/src/components/Call/CallContent/CallContent.tsx @@ -1,11 +1,7 @@ import React, { useEffect, useState } from 'react'; import { StyleSheet, View } from 'react-native'; import InCallManager from 'react-native-incall-manager'; - -import { - CallTopView as DefaultCallTopView, - CallTopViewProps, -} from '../CallTopView'; +import { CallTopViewProps } from '../CallTopView'; import { CallParticipantsGrid, CallParticipantsGridProps, @@ -98,7 +94,7 @@ export type CallContentProps = Pick< export const CallContent = ({ onHangupCallHandler, CallParticipantsList, - CallTopView = DefaultCallTopView, + CallTopView, CallControls = DefaultCallControls, FloatingParticipantView = DefaultFloatingParticipantView, ScreenShareOverlay = DefaultScreenShareOverlay, @@ -247,7 +243,6 @@ const styles = StyleSheet.create({ container: { flex: 1 }, content: { flex: 1 }, view: { - // backgroundColor: 'red', ...StyleSheet.absoluteFillObject, zIndex: Z_INDEX.IN_FRONT, }, diff --git a/packages/react-native-sdk/src/components/Call/CallControls/CallControlsButton.tsx b/packages/react-native-sdk/src/components/Call/CallControls/CallControlsButton.tsx index d7d9b814da..2574e70bd5 100644 --- a/packages/react-native-sdk/src/components/Call/CallControls/CallControlsButton.tsx +++ b/packages/react-native-sdk/src/components/Call/CallControls/CallControlsButton.tsx @@ -18,6 +18,10 @@ interface CallControlsButtonProps { * The background color of the button rendered. */ color?: ColorValue; + /** + * The background color of the disabled button. + */ + disabledColor?: ColorValue; /** * Boolean to enable/disable the button */ @@ -49,6 +53,7 @@ export const CallControlsButton = ( children, disabled, color: colorProp, + disabledColor: disabledColorProp, style: styleProp, size, testID, @@ -68,7 +73,7 @@ export const CallControlsButton = ( styles.container, { backgroundColor: disabled - ? colors.buttonPrimaryDisabled + ? disabledColorProp || colors.buttonPrimaryDisabled : colorProp || colors.buttonSecondaryDefault, opacity: pressed ? 0.2 : 1, height: size || roundButtonSizes.lg, diff --git a/packages/react-native-sdk/src/components/Call/CallControls/FlipCameraButton.tsx b/packages/react-native-sdk/src/components/Call/CallControls/FlipCameraButton.tsx deleted file mode 100644 index 7a48d09c19..0000000000 --- a/packages/react-native-sdk/src/components/Call/CallControls/FlipCameraButton.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React, { useState } from 'react'; -import { CallControlsButton } from '@stream-io/video-react-native-sdk'; -import { IconWrapper } from '../../../icons/IconWrapper'; -import { FlipCamera } from '../../../icons/FlipCamera'; -import { useTheme } from '../../..'; - -/** - * The props for the Flip Camera Button in the camera Controls. - */ -export type FlipCameraButtonProps = { - /** - * Handler to be called when the flip camera button is pressed. - * @returns void - */ - onPressHandler?: () => void; -}; - -/** - * The flip camera Button is used in the camera Controls component - * and allows the user to toggle camera flipping. - */ -export const FlipCameraButton = ({ onPressHandler }: FlipCameraButtonProps) => { - const { - theme: { colors, flipCameraButton, defaults, variants }, - } = useTheme(); - const [isCameraFlipped, setIsCameraFlipped] = useState(false); - const buttonColor = isCameraFlipped ? colors.base5 : colors.base5; - - return ( - { - if (onPressHandler) { - onPressHandler(); - } - setIsCameraFlipped(!isCameraFlipped); - }} - color={buttonColor} - style={flipCameraButton} - > - - - - - ); -}; 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 fa5652f665..f8f635b744 100644 --- a/packages/react-native-sdk/src/components/Call/CallControls/ToggleCameraFaceButton.tsx +++ b/packages/react-native-sdk/src/components/Call/CallControls/ToggleCameraFaceButton.tsx @@ -2,7 +2,7 @@ import { OwnCapability } from '@stream-io/video-client'; import { Restricted, useCallStateHooks } from '@stream-io/video-react-bindings'; import React from 'react'; import { CallControlsButton } from './CallControlsButton'; -import { CameraSwitch } from '../../../icons'; +import { CameraSwitch, IconWrapper } from '../../../icons'; import { useTheme } from '../../../contexts/ThemeContext'; /** @@ -28,7 +28,7 @@ export const ToggleCameraFaceButton = ({ const isVideoEnabledInCall = callSettings?.video.enabled; const { - theme: { colors, toggleCameraFaceButton }, + theme: { colors, toggleCameraFaceButton, defaults }, } = useTheme(); const onPress = async () => { if (onPressHandler) { @@ -36,7 +36,12 @@ export const ToggleCameraFaceButton = ({ return; } - await camera.flip(); + // TODO: investigate camera on off issues and flip operation not working + try { + await camera.flip(); + } catch (e) { + console.error(e); + } }; if (!isVideoEnabledInCall) { @@ -47,17 +52,23 @@ export const ToggleCameraFaceButton = ({ - + + + ); diff --git a/packages/react-native-sdk/src/components/Call/CallControls/ToggleViewButton.tsx b/packages/react-native-sdk/src/components/Call/CallControls/ToggleViewButton.tsx deleted file mode 100644 index 0168497664..0000000000 --- a/packages/react-native-sdk/src/components/Call/CallControls/ToggleViewButton.tsx +++ /dev/null @@ -1,46 +0,0 @@ -import React, { useState } from 'react'; -import { CallControlsButton } from '@stream-io/video-react-native-sdk'; -import { IconWrapper } from '../../../icons/IconWrapper'; -import { useTheme } from '../../..'; -import { Grid } from '../../../icons/Grid'; - -/** - * The props for the Flip Camera Button in the camera Controls. - */ -export type ToggleViewButton = { - /** - * Handler to be called when the flip camera button is pressed. - * @returns void - */ - onPressHandler?: () => void; -}; - -/** - * The flip camera Button is used in the camera Controls component - * and allows the user to toggle camera flipping. - */ -export const ToggleViewButton = ({ onPressHandler }: ToggleViewButton) => { - const { - theme: { colors, flipCameraButton, defaults, variants }, - } = useTheme(); - const [isCameraFlipped, setIsCameraFlipped] = useState(false); - const buttonColor = isCameraFlipped ? colors.base5 : colors.base5; - - return ( - { - if (onPressHandler) { - onPressHandler(); - } - setIsCameraFlipped(!isCameraFlipped); - }} - color={buttonColor} - style={flipCameraButton} - > - - - - - ); -}; diff --git a/packages/react-native-sdk/src/components/Call/CallControls/VideoEffectsButton.tsx b/packages/react-native-sdk/src/components/Call/CallControls/VideoEffectsButton.tsx deleted file mode 100644 index 55bd2b4323..0000000000 --- a/packages/react-native-sdk/src/components/Call/CallControls/VideoEffectsButton.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import React, { useState } from 'react'; -import { IconWrapper } from '../../../icons/IconWrapper'; -import { CallControlsButton, useTheme } from '../../..'; -import { Effects } from '../../../icons/Effects'; - -/** - * The props for the Flip Camera Button in the camera Controls. - */ -export type VideoEffectsButtonProps = { - /** - * Handler to be called when the flip camera button is pressed. - * @returns void - */ - onPressHandler?: () => void; -}; - -/** - * The flip camera Button is used in the camera Controls component - * and allows the user to toggle camera flipping. - */ -export const VideoEffectsButton = ({ - onPressHandler, -}: VideoEffectsButtonProps) => { - const { - theme: { colors, flipCameraButton, defaults, variants }, - } = useTheme(); - const [isButtonPressed, setIsButtonPressed] = useState(false); - const buttonColor = isButtonPressed ? colors.base5 : colors.base5; - - return ( - { - if (onPressHandler) { - onPressHandler(); - } - setIsButtonPressed(!isButtonPressed); - }} - color={buttonColor} - style={flipCameraButton} - > - - - - - ); -}; diff --git a/packages/react-native-sdk/src/components/Call/CallControls/index.tsx b/packages/react-native-sdk/src/components/Call/CallControls/index.tsx index f921e3da75..1fa2a0543c 100644 --- a/packages/react-native-sdk/src/components/Call/CallControls/index.tsx +++ b/packages/react-native-sdk/src/components/Call/CallControls/index.tsx @@ -13,5 +13,3 @@ export * from './LobbyControls'; export * from './IncomingCallControls'; export * from './OutgoingCallControls'; export * from './ScreenShareToggleButton'; -export * from './FlipCameraButton'; -export * from './VideoEffectsButton'; diff --git a/packages/react-native-sdk/src/components/Call/CallTopView/TopLeftControls.tsx b/packages/react-native-sdk/src/components/Call/CallTopView/TopLeftControls.tsx deleted file mode 100644 index 40f39b8eca..0000000000 --- a/packages/react-native-sdk/src/components/Call/CallTopView/TopLeftControls.tsx +++ /dev/null @@ -1,38 +0,0 @@ -import React, { useState, useEffect, useMemo } from 'react'; -import { - View, - StyleSheet, - Text, - Pressable, - StyleProp, - ViewStyle, -} from 'react-native'; -import { Grid } from '../../../icons/Grid'; -import { FlipCameraButton, VideoEffectsButton, useTheme } from '../../..'; -import { ToggleViewButton } from '../CallControls/ToggleViewButton'; - -export const TopLeftControls = (props: any) => { - const styles = useStyles(); - return ( - - {}} /> - {}} /> - {!props.inProgress && {}} />} - - ); -}; - -const useStyles = () => { - const { theme } = useTheme(); - - return useMemo( - () => - StyleSheet.create({ - content: { - flexDirection: 'row', - gap: 6, - }, - }), - [theme] - ); -}; diff --git a/packages/react-native-sdk/src/components/Call/CallTopView/index.ts b/packages/react-native-sdk/src/components/Call/CallTopView/index.ts deleted file mode 100644 index bb5d521f5b..0000000000 --- a/packages/react-native-sdk/src/components/Call/CallTopView/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './CallTopView'; diff --git a/packages/react-native-sdk/src/components/Call/RingingCallContent/IncomingCall.tsx b/packages/react-native-sdk/src/components/Call/RingingCallContent/IncomingCall.tsx index 3dca12bedb..074768c24e 100644 --- a/packages/react-native-sdk/src/components/Call/RingingCallContent/IncomingCall.tsx +++ b/packages/react-native-sdk/src/components/Call/RingingCallContent/IncomingCall.tsx @@ -12,10 +12,6 @@ import { useI18n, } from '@stream-io/video-react-bindings'; import { UserInfo } from './UserInfo'; -import { - CallTopView as DefaultCallTopView, - CallTopViewProps, -} from '../CallTopView'; import { IncomingCallControls as DefaultIncomingCallControls, IncomingCallControlsProps, @@ -30,7 +26,7 @@ export type IncomingCallProps = IncomingCallControlsProps & { /** * Prop to customize the CallTopView component in the IncomingCall component. */ - CallTopView?: React.ComponentType | null; + CallTopView?: React.ComponentType | null; /** * Prop to customize the IncomingCall controls. */ @@ -49,7 +45,7 @@ export type IncomingCallProps = IncomingCallControlsProps & { export const IncomingCall = ({ onAcceptCallHandler, onRejectCallHandler, - CallTopView = DefaultCallTopView, + CallTopView, IncomingCallControls = DefaultIncomingCallControls, landscape, }: IncomingCallProps) => { diff --git a/packages/react-native-sdk/src/components/Call/RingingCallContent/OutgoingCall.tsx b/packages/react-native-sdk/src/components/Call/RingingCallContent/OutgoingCall.tsx index ce70a5ab94..fe441098d4 100644 --- a/packages/react-native-sdk/src/components/Call/RingingCallContent/OutgoingCall.tsx +++ b/packages/react-native-sdk/src/components/Call/RingingCallContent/OutgoingCall.tsx @@ -9,10 +9,6 @@ import { OutgoingCallControls as DefaultOutgoingCallControls, OutgoingCallControlsProps, } from '../CallControls'; -import { - CallTopView as DefaultCallTopView, - CallTopViewProps, -} from '../CallTopView'; import { useCallMediaStreamCleanup } from '../../../hooks/internal/useCallMediaStreamCleanup'; import { useApplyDefaultMediaStreamSettings } from '../../../hooks/useApplyDefaultMediaStreamSettings'; @@ -23,7 +19,7 @@ export type OutgoingCallProps = OutgoingCallControlsProps & { /** * Prop to customize the CallTopView component in the IncomingCall component. */ - CallTopView?: React.ComponentType | null; + CallTopView?: React.ComponentType | null; /** * Prop to customize the OutgoingCall controls. */ @@ -40,7 +36,7 @@ export type OutgoingCallProps = OutgoingCallControlsProps & { * Used after the user has initiated a call. */ export const OutgoingCall = ({ - CallTopView = DefaultCallTopView, + CallTopView, OutgoingCallControls = DefaultOutgoingCallControls, landscape, }: OutgoingCallProps) => { 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 75b38fbd5b..f947333ce9 100644 --- a/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantLabel.tsx +++ b/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantLabel.tsx @@ -108,7 +108,7 @@ export const ParticipantLabel = ({ {participantLabel} - + {isPinningEnabled && ( @@ -137,7 +137,8 @@ const styles = StyleSheet.create({ container: { flexDirection: 'row', alignItems: 'center', - padding: 8, + padding: 5, + maxHeight: 30, borderTopRightRadius: 5, marginBottom: -2, flexShrink: 1, @@ -145,6 +146,7 @@ const styles = StyleSheet.create({ }, userNameLabel: { flexShrink: 1, + marginTop: 2, }, screenShareIconContainer: { marginRight: 8, diff --git a/packages/react-native-sdk/src/components/Participant/ParticipantView/SpeechIndicator.tsx b/packages/react-native-sdk/src/components/Participant/ParticipantView/SpeechIndicator.tsx index c91328976d..f35d5c4b71 100644 --- a/packages/react-native-sdk/src/components/Participant/ParticipantView/SpeechIndicator.tsx +++ b/packages/react-native-sdk/src/components/Participant/ParticipantView/SpeechIndicator.tsx @@ -1,12 +1,22 @@ import React, { useEffect, useRef } from 'react'; import { View, Animated, StyleSheet } from 'react-native'; -export const SpeechIndicator = () => { - // const { participant } = useParticipantViewContext(); - // const { isSpeaking, isDominantSpeaker } = participant; - const isSpeaking = false; - const isDominantSpeaker = true; +/** + * Props for the SpeechIndicator component. + */ +export type SpeechIndicatorProps = { + /** + * Indicates whether the participant is speaking. + * If true, the animation will run, otherwise the bars will remain static. + */ + isSpeaking: boolean; +}; +/** + * The SpeechIndicator component displays animated bars to indicate speech activity. + * The bars animate when `isSpeaking` is true, mimicking a sound meter. + */ +export const SpeechIndicator = ({ isSpeaking }: SpeechIndicatorProps) => { const animationValues = [ useRef(new Animated.Value(0.6)).current, useRef(new Animated.Value(0.6)).current, @@ -38,12 +48,12 @@ export const SpeechIndicator = () => { } }, [isSpeaking, animationValues]); - const barStyle = (animatedValue: any) => ({ + const barStyle = (animatedValue: Animated.Value) => ({ transform: [{ scaleY: animatedValue }], }); return ( - + {animationValues.map((animatedValue, index) => ( ( - +export const CameraSwitch = ({ color, size }: Props) => ( + diff --git a/packages/react-native-sdk/src/icons/FlipCamera.tsx b/packages/react-native-sdk/src/icons/FlipCamera.tsx deleted file mode 100644 index 2dee56fd46..0000000000 --- a/packages/react-native-sdk/src/icons/FlipCamera.tsx +++ /dev/null @@ -1,17 +0,0 @@ -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 FlipCamera = ({ color, size }: Props) => ( - - - -); diff --git a/packages/react-native-sdk/src/icons/RecordCall.tsx b/packages/react-native-sdk/src/icons/RecordCall.tsx new file mode 100644 index 0000000000..c1b9853f42 --- /dev/null +++ b/packages/react-native-sdk/src/icons/RecordCall.tsx @@ -0,0 +1,21 @@ +import React from 'react'; +import { ColorValue } from 'react-native'; +import { Svg, Path } from 'react-native-svg'; + +type Props = { + color: ColorValue; + size: number; +}; + +export const RecordCall = ({ color, size }: Props) => ( + + + + +); diff --git a/packages/react-native-sdk/src/icons/index.tsx b/packages/react-native-sdk/src/icons/index.tsx index 4da04dcd0f..f3b3ee722f 100644 --- a/packages/react-native-sdk/src/icons/index.tsx +++ b/packages/react-native-sdk/src/icons/index.tsx @@ -19,5 +19,4 @@ export * from './StartStreamIcon'; export * from './StopScreenShare'; export * from './EndStreamIcon'; export * from './LeaveStreamIcon'; -export * from './CallDuration'; export * from './IconWrapper'; diff --git a/packages/react-native-sdk/src/icons/CallDuration.tsx b/sample-apps/react-native/dogfood/src/assets/CallDuration.tsx similarity index 100% rename from packages/react-native-sdk/src/icons/CallDuration.tsx rename to sample-apps/react-native/dogfood/src/assets/CallDuration.tsx diff --git a/sample-apps/react-native/dogfood/src/assets/RecordCall.tsx b/sample-apps/react-native/dogfood/src/assets/RecordCall.tsx deleted file mode 100644 index c07c995b43..0000000000 --- a/sample-apps/react-native/dogfood/src/assets/RecordCall.tsx +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import { ColorValue } from 'react-native'; -import { Svg, Path } from 'react-native-svg'; - -type Props = { - color: ColorValue; - size: number; -}; - -const RecordCall = ({ color, size }: Props) => ( - - - - -); - -export default RecordCall; diff --git a/sample-apps/react-native/dogfood/src/components/ActiveCall.tsx b/sample-apps/react-native/dogfood/src/components/ActiveCall.tsx index 90c9dcf163..e1651ab9ec 100644 --- a/sample-apps/react-native/dogfood/src/components/ActiveCall.tsx +++ b/sample-apps/react-native/dogfood/src/components/ActiveCall.tsx @@ -13,6 +13,7 @@ import { } from './CallControlls/CallControlsComponent'; import { useOrientation } from '../hooks/useOrientation'; import { Z_INDEX } from '../constants'; +import { CustomCallTopView } from './CallControlls/CallTopView'; type ActiveCallProps = CallControlsComponentProps & { onBackPressed?: () => void; @@ -32,6 +33,7 @@ export const ActiveCall = ({ const call = useCall(); const currentOrientation = useOrientation(); const styles = useStyles(); + const { theme: colors } = useTheme(); const onOpenCallParticipantsInfo = useCallback(() => { setIsCallParticipantsVisible(true); @@ -59,12 +61,16 @@ export const ActiveCall = ({ return ( - {/* */} + diff --git a/packages/react-native-sdk/src/components/Call/CallTopView/CallTopView.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/CallTopView.tsx similarity index 87% rename from packages/react-native-sdk/src/components/Call/CallTopView/CallTopView.tsx rename to sample-apps/react-native/dogfood/src/components/CallControlls/CallTopView.tsx index f013140566..112401c1a8 100644 --- a/packages/react-native-sdk/src/components/Call/CallTopView/CallTopView.tsx +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/CallTopView.tsx @@ -7,14 +7,14 @@ import { StyleProp, ViewStyle, } from 'react-native'; -import { ParticipantsInfoBadgeProps } from './ParticipantsInfoBadge'; -import { Back } from '../../../icons/Back'; -import { CallDuration, TopViewBackground } from '../../../icons'; -import { useCallStateHooks, useI18n } from '@stream-io/video-react-bindings'; +import { TopViewBackground } from '@stream-io/video-react-native-sdk/src/icons'; import { CallingState } from '@stream-io/video-client'; -import { useTheme } from '../../../contexts/ThemeContext'; -import { HangUpCallButton } from '..'; -import { colors } from '../../..'; +import { HangUpCallButton } from '@stream-io/video-react-native-sdk/src/components/Call'; +import { + colors, + useCallStateHooks, +} from '@stream-io/video-react-native-sdk/src'; +import { useTheme } from '@stream-io/video-react-native-sdk'; import { DurationBadge } from './DurationBadge'; import { TopLeftControls } from './TopLeftControls'; @@ -30,7 +30,7 @@ export type CallTopViewProps = { style?: StyleProp; }; -export const CallTopView = ({ +export const CustomCallTopView = ({ onHangupCallHandler, style: styleProp, }: CallTopViewProps) => { @@ -47,7 +47,7 @@ export const CallTopView = ({ const styles = useStyles(); const { useCallCallingState } = useCallStateHooks(); const callingState = useCallCallingState(); - const [inProgress, setInProgress] = useState(true); + const [inProgress, setInProgress] = useState(false); const onLayout: React.ComponentProps['onLayout'] = (event) => { const { height, width } = event.nativeEvent.layout; @@ -129,6 +129,6 @@ const useStyles = () => { fontWeight: '600', }, }), - [theme] + [theme], ); }; diff --git a/packages/react-native-sdk/src/components/Call/CallTopView/DurationBadge.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/DurationBadge.tsx similarity index 74% rename from packages/react-native-sdk/src/components/Call/CallTopView/DurationBadge.tsx rename to sample-apps/react-native/dogfood/src/components/CallControlls/DurationBadge.tsx index 573d3dc0b4..da183fd97a 100644 --- a/packages/react-native-sdk/src/components/Call/CallTopView/DurationBadge.tsx +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/DurationBadge.tsx @@ -1,11 +1,10 @@ import React, { useState, useEffect, useMemo } from 'react'; import { View, Text, StyleSheet } from 'react-native'; -import { CallDuration } from '../../../icons'; -import { useTheme } from '../../..'; -// import RecordCall from '../../../icons/RecordCall'; -import { IconWrapper } from '../../../icons/IconWrapper'; +import { CallDuration } from '../../assets/CallDuration'; +import { RecordCall } from '@stream-io/video-react-native-sdk/src/icons/RecordCall'; +import { IconWrapper } from '@stream-io/video-react-native-sdk/src/icons'; +import { useTheme } from '@stream-io/video-react-native-sdk'; -// TODO: move to dogfood app export const DurationBadge = (props: any) => { const { theme: { @@ -31,10 +30,10 @@ export const DurationBadge = (props: any) => { const seconds = (duration % 60).toString().padStart(2, '0'); const timestamp = `${minutes}:${seconds}`; const text = props.inProgress ? 'Recording in progress...' : timestamp; - const icon = false ? ( + const icon = true ? ( ) : ( - + ); return ( @@ -58,23 +57,25 @@ const useStyles = (props: any) => { borderRadius: 8, flexDirection: 'row', height: 36, - paddingHorizontal: 12, // Use equal padding on both sides + paddingLeft: 17, + paddingRight: 5, justifyContent: 'center', alignItems: 'center', width: props.inProgress ? 200 : 80, - gap: 7, }, text: { color: theme.colors.typePrimary, fontSize: 13, fontWeight: '600', - marginLeft: 3, // Add some space between icon and text - flexShrink: 0, // Allow text to shrink if needed + flexShrink: 0, + marginLeft: 10, + minWidth: 41, }, icon: { - marginTop: 3, + marginTop: 2, + marginRight: 5, }, }), - [theme] + [theme], ); }; diff --git a/sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherButton.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherButton.tsx new file mode 100644 index 0000000000..57f25d54a1 --- /dev/null +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherButton.tsx @@ -0,0 +1,49 @@ +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'; + +export type LayoutSwitcherButtonProps = { + /** + * Handler to be called when the layout switcher button is pressed. + * @returns void + */ + onPressHandler?: () => void; +}; + +/** + * The layout switcher Button can be used to switch different layout arrangements + * of the call participants. + */ +export const LayoutSwitcherButton = ({ + onPressHandler, +}: LayoutSwitcherButtonProps) => { + const { + theme: { colors, LayoutSwitcherButton, defaults, variants }, + } = useTheme(); + const [toggleLayoutMenu, setToggleLayoutMenu] = useState(false); + const buttonColor = toggleLayoutMenu + ? colors.iconPrimaryAccent + : colors.iconPrimaryDefault; + + return ( + { + if (onPressHandler) { + onPressHandler(); + } + setToggleLayoutMenu(!toggleLayoutMenu); + }} + color={colors.sheetPrimary} + style={LayoutSwitcherButton} + > + + + + + ); +}; diff --git a/sample-apps/react-native/dogfood/src/components/CallControlls/RecordCallButton.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/RecordCallButton.tsx index 7f80e3e406..8accc949fb 100644 --- a/sample-apps/react-native/dogfood/src/components/CallControlls/RecordCallButton.tsx +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/RecordCallButton.tsx @@ -3,7 +3,7 @@ import { CallControlsButton, useTheme, } from '@stream-io/video-react-native-sdk'; -import RecordCall from '../../assets/RecordCall'; +import { RecordCall } from '@stream-io/video-react-native-sdk/src/icons/RecordCall'; import { IconWrapper } from '@stream-io/video-react-native-sdk/src/icons'; /** diff --git a/sample-apps/react-native/dogfood/src/components/CallControlls/TopLeftControls.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/TopLeftControls.tsx new file mode 100644 index 0000000000..47b0e8d35f --- /dev/null +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/TopLeftControls.tsx @@ -0,0 +1,34 @@ +import React, { useMemo } from 'react'; +import { View, StyleSheet } from 'react-native'; +import { useTheme } from '@stream-io/video-react-native-sdk/src'; +import { VideoEffectsButton } from '../VideoEffectsButton'; +import { ToggleCameraFaceButton } from '@stream-io/video-react-native-sdk'; +import { LayoutSwitcherButton } from './LayoutSwitcherButton'; + +export const TopLeftControls = (props: any) => { + const styles = useStyles(); + return ( + + {}} /> + + {!props.inProgress && } + + ); +}; + +const useStyles = () => { + const { theme } = useTheme(); + + return useMemo( + () => + StyleSheet.create({ + content: { + flexDirection: 'row', + // gap: 2, + alignItems: 'center', + justifyContent: 'center', + }, + }), + [theme], + ); +}; 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 7ebee4cedc..3a591a6970 100644 --- a/sample-apps/react-native/dogfood/src/components/VideoEffectsButton/index.tsx +++ b/sample-apps/react-native/dogfood/src/components/VideoEffectsButton/index.tsx @@ -3,6 +3,7 @@ import { useBackgroundFilters, BlurIntensity, BackgroundFiltersProvider, + useTheme, } from '@stream-io/video-react-native-sdk'; import React, { useState } from 'react'; import { AutoAwesome } from '../../assets/AutoAwesome'; @@ -18,6 +19,8 @@ import { import { appTheme } from '../../theme'; import { Button } from '../Button'; import { useCustomVideoFilters } from './CustomFilters'; +import { IconWrapper } from '@stream-io/video-react-native-sdk/src/icons'; +import { Effects } from '@stream-io/video-react-native-sdk/src/icons/Effects'; type ImageSourceType = ImageURISource | number; @@ -42,6 +45,7 @@ export const VideoEffectsButton = () => ( const FilterButton = () => { const [modalVisible, setModalVisible] = useState(false); const closeModal = () => setModalVisible(false); + const { theme } = useTheme(); const { disableCustomFilter } = useCustomVideoFilters(); const { isSupported, disableAllFilters } = useBackgroundFilters(); @@ -74,8 +78,17 @@ const FilterButton = () => { - setModalVisible((prev) => !prev)}> - + setModalVisible((prev) => !prev)} + > + + + ); From 81d0241ae3a36fda9971ca7ac38b7c4bf8db8f91 Mon Sep 17 00:00:00 2001 From: Kristian Martinoski Date: Thu, 17 Oct 2024 16:23:38 +0200 Subject: [PATCH 07/36] chore: code cleanup --- packages/client/src/Call.ts | 1 - .../Call/CallControls/HangupCallButton.tsx | 6 +- .../Call/CallControls/RejectCallButton.tsx | 4 +- .../CallControls/ToggleCameraFaceButton.tsx | 8 +- .../src/components/Call/index.ts | 1 - .../ParticipantView/ParticipantLabel.tsx | 90 ++++++------ .../ParticipantNetworkQualityIndicator.tsx | 2 +- .../ParticipantView/ParticipantView.tsx | 46 +++--- .../ParticipantView/SpeechIndicator.tsx | 55 ++++--- packages/react-native-sdk/src/theme/colors.ts | 2 +- .../dogfood/src/components/ActiveCall.tsx | 14 +- ...ntrolsComponent.tsx => BottomControls.tsx} | 14 +- ...{DurationBadge.tsx => CallStatusBadge.tsx} | 49 +++++-- .../components/CallControlls/CallTopView.tsx | 134 ------------------ .../CallControlls/RecordCallButton.tsx | 2 +- .../components/CallControlls/TopControls.tsx | 110 ++++++++++++++ .../CallControlls/TopLeftControls.tsx | 34 ----- .../components/VideoEffectsButton/index.tsx | 1 - 18 files changed, 278 insertions(+), 295 deletions(-) rename sample-apps/react-native/dogfood/src/components/CallControlls/{CallControlsComponent.tsx => BottomControls.tsx} (89%) rename sample-apps/react-native/dogfood/src/components/CallControlls/{DurationBadge.tsx => CallStatusBadge.tsx} (65%) delete mode 100644 sample-apps/react-native/dogfood/src/components/CallControlls/CallTopView.tsx create mode 100644 sample-apps/react-native/dogfood/src/components/CallControlls/TopControls.tsx delete mode 100644 sample-apps/react-native/dogfood/src/components/CallControlls/TopLeftControls.tsx diff --git a/packages/client/src/Call.ts b/packages/client/src/Call.ts index 6cba3020d0..31e5cf08ac 100644 --- a/packages/client/src/Call.ts +++ b/packages/client/src/Call.ts @@ -504,7 +504,6 @@ export class Call { await withoutConcurrency(this.joinLeaveConcurrencyTag, async () => { const callingState = this.state.callingState; if (callingState === CallingState.LEFT) { - // TODO: check why this error is happening throw new Error('Cannot leave call that has already been left.'); } diff --git a/packages/react-native-sdk/src/components/Call/CallControls/HangupCallButton.tsx b/packages/react-native-sdk/src/components/Call/CallControls/HangupCallButton.tsx index 45c4463369..feceabd80d 100644 --- a/packages/react-native-sdk/src/components/Call/CallControls/HangupCallButton.tsx +++ b/packages/react-native-sdk/src/components/Call/CallControls/HangupCallButton.tsx @@ -52,9 +52,9 @@ export const HangUpCallButton = ({ return; } try { - // if (callingState === CallingState.LEFT) { - // return; - // } + if (callingState === CallingState.LEFT) { + return; + } await call?.leave(); if (onHangupCallHandler) { onHangupCallHandler(); diff --git a/packages/react-native-sdk/src/components/Call/CallControls/RejectCallButton.tsx b/packages/react-native-sdk/src/components/Call/CallControls/RejectCallButton.tsx index f1a3c5372b..5d2878d5f6 100644 --- a/packages/react-native-sdk/src/components/Call/CallControls/RejectCallButton.tsx +++ b/packages/react-native-sdk/src/components/Call/CallControls/RejectCallButton.tsx @@ -38,7 +38,7 @@ export const RejectCallButton = ({ theme: { colors, rejectCallButton, - variants: { buttonSizes }, + variants: { buttonSizes, iconSizes }, }, } = useTheme(); const rejectCallHandler = async () => { @@ -69,7 +69,7 @@ export const RejectCallButton = ({ // svgContainerStyle={theme.icon.lg} style={rejectCallButton} > - + ); }; 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 f8f635b744..ae11ea6860 100644 --- a/packages/react-native-sdk/src/components/Call/CallControls/ToggleCameraFaceButton.tsx +++ b/packages/react-native-sdk/src/components/Call/CallControls/ToggleCameraFaceButton.tsx @@ -36,12 +36,8 @@ export const ToggleCameraFaceButton = ({ return; } - // TODO: investigate camera on off issues and flip operation not working - try { - await camera.flip(); - } catch (e) { - console.error(e); - } + // TODO: investigate issue with the camera flip + await camera.flip(); }; if (!isVideoEnabledInCall) { diff --git a/packages/react-native-sdk/src/components/Call/index.ts b/packages/react-native-sdk/src/components/Call/index.ts index 2b099259ff..b86bcd1797 100644 --- a/packages/react-native-sdk/src/components/Call/index.ts +++ b/packages/react-native-sdk/src/components/Call/index.ts @@ -1,5 +1,4 @@ export * from './CallContent'; -export * from './CallTopView'; export * from './CallLayout'; export * from './CallControls'; export * from './CallParticipantsList'; 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 f947333ce9..5d4ee2ad81 100644 --- a/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantLabel.tsx +++ b/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantLabel.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { Pressable, StyleSheet, Text, View } from 'react-native'; import { PinVertical, ScreenShareIndicator } from '../../../icons'; import { useCall, useI18n } from '@stream-io/video-react-bindings'; @@ -36,6 +36,7 @@ export const ParticipantLabel = ({ }, }, } = useTheme(); + const styles = useStyles(); const { name, userId, pin, sessionId, isLocalParticipant } = participant; const call = useCall(); const { t } = useI18n(); @@ -58,7 +59,7 @@ export const ParticipantLabel = ({ - + @@ -99,7 +100,7 @@ export const ParticipantLabel = ({ - + )} ); }; -const styles = StyleSheet.create({ - indicatorWrapper: { - marginLeft: 7, - }, - wrapper: { - flexDirection: 'row', - }, - container: { - flexDirection: 'row', - alignItems: 'center', - padding: 5, - maxHeight: 30, - borderTopRightRadius: 5, - marginBottom: -2, - flexShrink: 1, - zIndex: Z_INDEX.IN_FRONT, - }, - userNameLabel: { - flexShrink: 1, - marginTop: 2, - }, - screenShareIconContainer: { - marginRight: 8, - }, - audioMutedIconContainer: { - marginLeft: 4, - }, - videoMutedIconContainer: { - marginLeft: 4, - }, - pinIconContainer: { - marginLeft: 4, - }, -}); +const useStyles = () => { + const { theme } = useTheme(); + return useMemo( + () => + StyleSheet.create({ + indicatorWrapper: { + marginLeft: theme.variants.spacingSizes.sm, + }, + wrapper: { + flexDirection: 'row', + }, + container: { + flexDirection: 'row', + alignItems: 'center', + padding: theme.variants.spacingSizes.sm, + maxHeight: 30, + borderTopRightRadius: 5, + marginBottom: -2, + flexShrink: 1, + zIndex: Z_INDEX.IN_FRONT, + }, + userNameLabel: { + flexShrink: 1, + marginTop: 2, + }, + screenShareIconContainer: { + marginRight: theme.variants.spacingSizes.sm, + }, + audioMutedIconContainer: { + marginLeft: theme.variants.spacingSizes.xs, + }, + videoMutedIconContainer: { + marginLeft: theme.variants.spacingSizes.xs, + }, + pinIconContainer: { + marginLeft: theme.variants.spacingSizes.xs, + }, + }), + [theme] + ); +}; diff --git a/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantNetworkQualityIndicator.tsx b/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantNetworkQualityIndicator.tsx index f8c2a03dcf..fd39d74f47 100644 --- a/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantNetworkQualityIndicator.tsx +++ b/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantNetworkQualityIndicator.tsx @@ -69,7 +69,7 @@ export const ParticipantNetworkQualityIndicator = ({ style={[ styles.container, { - backgroundColor: 'rgba(12, 13, 14, 0.65)', + backgroundColor: colors.sheetOverlay, height: iconSizes.lg, width: iconSizes.lg, }, 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 ee485239a3..76b6e74921 100644 --- a/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantView.tsx +++ b/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantView.tsx @@ -1,4 +1,4 @@ -import React, { ComponentType } from 'react'; +import React, { ComponentType, useMemo } from 'react'; import { StyleProp, StyleSheet, View, ViewStyle } from 'react-native'; import { StreamVideoParticipant, @@ -112,6 +112,7 @@ export const ParticipantView = ({ const { theme: { colors, participantView }, } = useTheme(); + const styles = useStyles(); const { isSpeaking, userId } = participant; const isScreenSharing = trackType === 'screenShareTrack'; const applySpeakerStyle = isSpeaking && !isScreenSharing; @@ -160,21 +161,28 @@ export const ParticipantView = ({ ); }; -const styles = StyleSheet.create({ - container: { - justifyContent: 'space-between', - overflow: 'hidden', - borderWidth: 2, - borderColor: 'transparent', - margin: 8, - borderRadius: 16, - }, - footerContainer: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - }, - highligtedContainer: { - borderWidth: 2, - }, -}); +const useStyles = () => { + const { theme } = useTheme(); + return useMemo( + () => + StyleSheet.create({ + container: { + justifyContent: 'space-between', + overflow: 'hidden', + borderWidth: 2, + borderColor: 'transparent', + margin: theme.variants.spacingSizes.sm, + borderRadius: 16, + }, + footerContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + highligtedContainer: { + borderWidth: 2, + }, + }), + [theme] + ); +}; diff --git a/packages/react-native-sdk/src/components/Participant/ParticipantView/SpeechIndicator.tsx b/packages/react-native-sdk/src/components/Participant/ParticipantView/SpeechIndicator.tsx index f35d5c4b71..b02c9dd2c3 100644 --- a/packages/react-native-sdk/src/components/Participant/ParticipantView/SpeechIndicator.tsx +++ b/packages/react-native-sdk/src/components/Participant/ParticipantView/SpeechIndicator.tsx @@ -1,5 +1,6 @@ -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useRef, useMemo } from 'react'; import { View, Animated, StyleSheet } from 'react-native'; +import { useTheme } from '../../..'; /** * Props for the SpeechIndicator component. @@ -17,6 +18,7 @@ export type SpeechIndicatorProps = { * The bars animate when `isSpeaking` is true, mimicking a sound meter. */ export const SpeechIndicator = ({ isSpeaking }: SpeechIndicatorProps) => { + const styles = useStyles(); const animationValues = [ useRef(new Animated.Value(0.6)).current, useRef(new Animated.Value(0.6)).current, @@ -67,27 +69,34 @@ export const SpeechIndicator = ({ isSpeaking }: SpeechIndicatorProps) => { ); }; -const styles = StyleSheet.create({ - container: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - height: 25, - width: 25, - borderRadius: 5, - gap: 1, - backgroundColor: 'rgba(12, 13, 14, 0.65)', - padding: 5, - }, - smallBar: { - height: '30%', // Smaller default height when animation is not running - }, - bar: { - width: 3, - height: '100%', - backgroundColor: '#005fff', // Default color, can be replaced with a theme color - borderRadius: 2, - }, -}); +const useStyles = () => { + const { theme } = useTheme(); + return useMemo( + () => + StyleSheet.create({ + container: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + height: theme.variants.roundButtonSizes.sm, + width: theme.variants.roundButtonSizes.sm, + borderRadius: 5, + gap: 1, + backgroundColor: theme.colors.sheetOverlay, + padding: 5, + }, + smallBar: { + height: '30%', // Smaller default height when animation is not running + }, + bar: { + width: 3, + height: '100%', + backgroundColor: theme.colors.iconPrimaryAccent, + borderRadius: 2, + }, + }), + [theme] + ); +}; export default SpeechIndicator; diff --git a/packages/react-native-sdk/src/theme/colors.ts b/packages/react-native-sdk/src/theme/colors.ts index 39c3beb0d3..0c78ae81db 100644 --- a/packages/react-native-sdk/src/theme/colors.ts +++ b/packages/react-native-sdk/src/theme/colors.ts @@ -35,6 +35,7 @@ const colors: ColorScheme = { iconPrimaryDefault: '#eff0f1', iconAlertSuccess: '#00e2a1', sheetPrimary: '#000000', + sheetOverlay: '#0c0d0ea6', buttonPrimaryDisabled: '#1b2c43', buttonPrimaryHover: '#4c8fff', @@ -71,7 +72,6 @@ const colors: ColorScheme = { iconPrimaryHover: '#e3e4e5', iconPrimaryOnAccent: '#eff0f1', iconPrimaryPressed: '#656b72', - sheetOverlay: '#0c0d0ea6', sheetSecondary: '#101213', sheetTertiary: '#19232d', typeAccent: '#00e2a1', diff --git a/sample-apps/react-native/dogfood/src/components/ActiveCall.tsx b/sample-apps/react-native/dogfood/src/components/ActiveCall.tsx index e1651ab9ec..11eb695a0b 100644 --- a/sample-apps/react-native/dogfood/src/components/ActiveCall.tsx +++ b/sample-apps/react-native/dogfood/src/components/ActiveCall.tsx @@ -9,13 +9,13 @@ import { SafeAreaView } from 'react-native-safe-area-context'; import { ParticipantsInfoList } from './ParticipantsInfoList'; import { CallControlsComponent, - CallControlsComponentProps, -} from './CallControlls/CallControlsComponent'; + BottomControlsProps, +} from './CallControlls/BottomControls'; import { useOrientation } from '../hooks/useOrientation'; import { Z_INDEX } from '../constants'; -import { CustomCallTopView } from './CallControlls/CallTopView'; +import { TopControls } from './CallControlls/TopControls'; -type ActiveCallProps = CallControlsComponentProps & { +type ActiveCallProps = BottomControlsProps & { onBackPressed?: () => void; onHangupCallHandler?: () => void; onCallEnded: () => void; @@ -62,7 +62,7 @@ export const ActiveCall = ({ return ( @@ -70,7 +70,7 @@ export const ActiveCall = ({ @@ -99,7 +99,7 @@ const useStyles = () => { left: 0, right: 0, height: theme.variants.insets.top, - backgroundColor: theme.colors.sheetPrimary, // Change to your desired color + backgroundColor: theme.colors.sheetPrimary, zIndex: Z_INDEX.IN_FRONT, }, bottomUnsafeArea: { diff --git a/sample-apps/react-native/dogfood/src/components/CallControlls/CallControlsComponent.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/BottomControls.tsx similarity index 89% rename from sample-apps/react-native/dogfood/src/components/CallControlls/CallControlsComponent.tsx rename to sample-apps/react-native/dogfood/src/components/CallControlls/BottomControls.tsx index e2960e1a18..9adc2e2168 100644 --- a/sample-apps/react-native/dogfood/src/components/CallControlls/CallControlsComponent.tsx +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/BottomControls.tsx @@ -15,7 +15,7 @@ import { ChatButton } from './ChatButton'; import { RecordCallButton } from './RecordCallButton'; import { AudioButton } from './AudioButton'; -export type CallControlsComponentProps = Pick< +export type BottomControlsProps = Pick< CallContentProps, 'supportedReactions' > & { @@ -28,7 +28,7 @@ export const CallControlsComponent = ({ onChatOpenHandler, unreadCountIndicator, onParticipantInfoPress, -}: CallControlsComponentProps) => { +}: BottomControlsProps) => { const { useMicrophoneState } = useCallStateHooks(); const { isSpeakingWhileMuted } = useMicrophoneState(); const styles = useStyles(); @@ -68,7 +68,7 @@ const useStyles = () => { StyleSheet.create({ speakingLabelContainer: { backgroundColor: appTheme.colors.static_overlay, - paddingVertical: 10, + // paddingVertical: 10, width: '100%', }, label: { @@ -82,8 +82,8 @@ const useStyles = () => { zIndex: Z_INDEX.IN_FRONT, }, container: { - paddingVertical: 16, - paddingHorizontal: 16, + paddingVertical: theme.variants.spacingSizes.md, + paddingHorizontal: theme.variants.spacingSizes.md, backgroundColor: theme.colors.sheetPrimary, height: 76, }, @@ -91,13 +91,13 @@ const useStyles = () => { flex: 2.5, flexDirection: 'row', alignItems: 'flex-start', - gap: 6, + gap: theme.variants.spacingSizes.xs, }, right: { flex: 1, flexDirection: 'row', justifyContent: 'flex-end', - gap: 6, + gap: theme.variants.spacingSizes.xs, }, }), [theme], diff --git a/sample-apps/react-native/dogfood/src/components/CallControlls/DurationBadge.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/CallStatusBadge.tsx similarity index 65% rename from sample-apps/react-native/dogfood/src/components/CallControlls/DurationBadge.tsx rename to sample-apps/react-native/dogfood/src/components/CallControlls/CallStatusBadge.tsx index da183fd97a..2032d59f21 100644 --- a/sample-apps/react-native/dogfood/src/components/CallControlls/DurationBadge.tsx +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/CallStatusBadge.tsx @@ -5,23 +5,45 @@ import { RecordCall } from '@stream-io/video-react-native-sdk/src/icons/RecordCa import { IconWrapper } from '@stream-io/video-react-native-sdk/src/icons'; import { useTheme } from '@stream-io/video-react-native-sdk'; -export const DurationBadge = (props: any) => { +/** + * Props for the CallStatusBadge component. + */ +interface CallStatusBadgeProps { + /** + * Indicates if the call is currently being recorded. + * When true, it shows "Recording in progress..." and a specific icon. + * When false, it shows the elapsed time. + */ + isCallRecorded: boolean; +} + +/** + * CallStatusBadge component to display the current status of a call. + * It shows either a recording message or the elapsed time, with an accompanying icon. + * + * @param {CallStatusBadgeProps} props - The props for the component. + */ +export const CallStatusBadge: React.FC = ({ + isCallRecorded, +}) => { const { theme: { colors, variants: { iconSizes }, }, } = useTheme(); + const [duration, setDuration] = useState(0); + + // Increment the duration every second useEffect(() => { const id = setInterval(() => { setDuration((d) => d + 1); }, 1000); - return () => { - clearInterval(id); - }; + return () => clearInterval(id); }, []); - const styles = useStyles(props); + + const styles = useStyles(isCallRecorded); // Format duration to MM:SS const minutes = Math.floor(duration / 60) @@ -29,11 +51,12 @@ export const DurationBadge = (props: any) => { .padStart(2, '0'); const seconds = (duration % 60).toString().padStart(2, '0'); const timestamp = `${minutes}:${seconds}`; - const text = props.inProgress ? 'Recording in progress...' : timestamp; - const icon = true ? ( - - ) : ( + const text = isCallRecorded ? 'Recording in progress...' : timestamp; + + const icon = isCallRecorded ? ( + ) : ( + ); return ( @@ -46,7 +69,7 @@ export const DurationBadge = (props: any) => { ); }; -const useStyles = (props: any) => { +const useStyles = (isCallRecorded: boolean) => { const { theme } = useTheme(); return useMemo( @@ -61,11 +84,11 @@ const useStyles = (props: any) => { paddingRight: 5, justifyContent: 'center', alignItems: 'center', - width: props.inProgress ? 200 : 80, + width: isCallRecorded ? 200 : 80, }, text: { color: theme.colors.typePrimary, - fontSize: 13, + fontSize: 14, fontWeight: '600', flexShrink: 0, marginLeft: 10, @@ -76,6 +99,6 @@ const useStyles = (props: any) => { marginRight: 5, }, }), - [theme], + [theme, isCallRecorded], ); }; diff --git a/sample-apps/react-native/dogfood/src/components/CallControlls/CallTopView.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/CallTopView.tsx deleted file mode 100644 index 112401c1a8..0000000000 --- a/sample-apps/react-native/dogfood/src/components/CallControlls/CallTopView.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import React, { useState, useMemo } from 'react'; -import { - View, - StyleSheet, - Text, - Pressable, - StyleProp, - ViewStyle, -} from 'react-native'; -import { TopViewBackground } from '@stream-io/video-react-native-sdk/src/icons'; -import { CallingState } from '@stream-io/video-client'; -import { HangUpCallButton } from '@stream-io/video-react-native-sdk/src/components/Call'; -import { - colors, - useCallStateHooks, -} from '@stream-io/video-react-native-sdk/src'; -import { useTheme } from '@stream-io/video-react-native-sdk'; -import { DurationBadge } from './DurationBadge'; -import { TopLeftControls } from './TopLeftControls'; - -export type CallTopViewProps = { - /** - * Handler to be called when the hangup button is pressed in the CallTopView. - * @returns void - */ - onHangupCallHandler?: () => void; - /** - * Style to override the container of the CallTopView. - */ - style?: StyleProp; -}; - -export const CustomCallTopView = ({ - onHangupCallHandler, - style: styleProp, -}: CallTopViewProps) => { - const [callTopViewHeight, setCallTopViewHeight] = useState(0); - const [callTopViewWidth, setCallTopViewWidth] = useState(0); - const { - theme: { - colors, - typefaces, - variants: { iconSizes }, - callTopView, - }, - } = useTheme(); - const styles = useStyles(); - const { useCallCallingState } = useCallStateHooks(); - const callingState = useCallCallingState(); - const [inProgress, setInProgress] = useState(false); - - const onLayout: React.ComponentProps['onLayout'] = (event) => { - const { height, width } = event.nativeEvent.layout; - if (setCallTopViewHeight) { - setCallTopViewHeight(height); - setCallTopViewWidth(width); - } - }; - - return ( - - {/* Component for the background of the CallTopView. Since it has a Linear Gradient, an SVG is used to render it. */} - - - - - - - - - - - - - - - - ); -}; - -const useStyles = () => { - const { theme } = useTheme(); - - return useMemo( - () => - StyleSheet.create({ - container: {}, - content: { - position: 'absolute', - backgroundColor: theme.colors.sheetPrimary, - top: 0, - flexDirection: 'row', - paddingTop: 2, - paddingBottom: 2, - paddingRight: 12, - paddingLeft: 12, - alignItems: 'center', - // borderWidth: 2, - // borderColor: 'red', - }, - leftElement: { - flex: 1, - alignItems: 'flex-start', - // borderWidth: 2, - // borderColor: 'red', - }, - centerElement: { - flex: 1, - alignItems: 'center', - }, - rightElement: { - flex: 1, - alignItems: 'flex-end', - }, - centerWrapper: { - backgroundColor: colors.buttonSecondaryDefault, - borderRadius: 8, - width: 60, - display: 'flex', - flexDirection: 'row', - height: 32, - padding: 6, - justifyContent: 'center', - alignItems: 'center', - }, - timer: { - color: colors.typePrimary, - fontSize: 13, - fontWeight: '600', - }, - }), - [theme], - ); -}; diff --git a/sample-apps/react-native/dogfood/src/components/CallControlls/RecordCallButton.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/RecordCallButton.tsx index 8accc949fb..8ae8840651 100644 --- a/sample-apps/react-native/dogfood/src/components/CallControlls/RecordCallButton.tsx +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/RecordCallButton.tsx @@ -46,7 +46,7 @@ export const RecordCallButton = ({ onPressHandler }: RecordCallButtonProps) => { diff --git a/sample-apps/react-native/dogfood/src/components/CallControlls/TopControls.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/TopControls.tsx new file mode 100644 index 0000000000..36873fa79c --- /dev/null +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/TopControls.tsx @@ -0,0 +1,110 @@ +import React, { useState, useMemo } from 'react'; +import { View, StyleSheet, StyleProp, ViewStyle } from 'react-native'; +import { TopViewBackground } from '@stream-io/video-react-native-sdk/src/icons'; +import { + HangUpCallButton, + useTheme, + ToggleCameraFaceButton, +} from '@stream-io/video-react-native-sdk'; +import { CallStatusBadge } from './CallStatusBadge'; +import { VideoEffectsButton } from '../VideoEffectsButton'; +import { LayoutSwitcherButton } from './LayoutSwitcherButton'; + +export type TopControlsProps = { + /** + * Handler to be called when the hangup button is pressed in the TopControls. + * @returns void + */ + onHangupCallHandler?: () => void; + /** + * Style to override the container of the TopControls. + */ + style?: StyleProp; +}; + +export const TopControls = ({ + onHangupCallHandler, + style: styleProp, +}: TopControlsProps) => { + const [topControlsHeight, setTopControlsHeight] = useState(0); + const [topControlsWidth, setTopControlsWidth] = useState(0); + const { + theme: { + colors, + typefaces, + variants: { iconSizes }, + callTopView, + }, + } = useTheme(); + const styles = useStyles(); + + // TODO: replace this with real data implement PBE-5871 [Demo App] Call Recording flow + const [isCallRecorded, setIsCallRecorded] = useState(false); + + const onLayout: React.ComponentProps['onLayout'] = (event) => { + const { height, width } = event.nativeEvent.layout; + if (setTopControlsHeight) { + setTopControlsHeight(height); + setTopControlsWidth(width); + } + }; + + return ( + + {/* Component for the background of the TopControls. Since it has a Linear Gradient, an SVG is used to render it. */} + + + + + {}} /> + + {!isCallRecorded && } + + + + + + + + + + + ); +}; + +const useStyles = () => { + const { theme } = useTheme(); + + return useMemo( + () => + StyleSheet.create({ + content: { + position: 'absolute', + backgroundColor: theme.colors.sheetPrimary, + top: 0, + flexDirection: 'row', + paddingVertical: 2, + paddingHorizontal: theme.variants.spacingSizes.md, + alignItems: 'center', + }, + leftElement: { + flex: 1, + alignItems: 'flex-start', + }, + leftContent: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + }, + centerElement: { + flex: 1, + alignItems: 'center', + }, + rightElement: { + flex: 1, + alignItems: 'flex-end', + }, + }), + [theme], + ); +}; diff --git a/sample-apps/react-native/dogfood/src/components/CallControlls/TopLeftControls.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/TopLeftControls.tsx deleted file mode 100644 index 47b0e8d35f..0000000000 --- a/sample-apps/react-native/dogfood/src/components/CallControlls/TopLeftControls.tsx +++ /dev/null @@ -1,34 +0,0 @@ -import React, { useMemo } from 'react'; -import { View, StyleSheet } from 'react-native'; -import { useTheme } from '@stream-io/video-react-native-sdk/src'; -import { VideoEffectsButton } from '../VideoEffectsButton'; -import { ToggleCameraFaceButton } from '@stream-io/video-react-native-sdk'; -import { LayoutSwitcherButton } from './LayoutSwitcherButton'; - -export const TopLeftControls = (props: any) => { - const styles = useStyles(); - return ( - - {}} /> - - {!props.inProgress && } - - ); -}; - -const useStyles = () => { - const { theme } = useTheme(); - - return useMemo( - () => - StyleSheet.create({ - content: { - flexDirection: 'row', - // gap: 2, - alignItems: 'center', - justifyContent: 'center', - }, - }), - [theme], - ); -}; 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 3a591a6970..d83be3c82d 100644 --- a/sample-apps/react-native/dogfood/src/components/VideoEffectsButton/index.tsx +++ b/sample-apps/react-native/dogfood/src/components/VideoEffectsButton/index.tsx @@ -6,7 +6,6 @@ import { useTheme, } from '@stream-io/video-react-native-sdk'; import React, { useState } from 'react'; -import { AutoAwesome } from '../../assets/AutoAwesome'; import { Image, Modal, From 7dfa4738cbf01ddaf5707bd90f55dec9188761f3 Mon Sep 17 00:00:00 2001 From: Kristian Martinoski Date: Thu, 17 Oct 2024 16:32:26 +0200 Subject: [PATCH 08/36] chore: additional clean up --- .../src/components/Call/CallControls/RejectCallButton.tsx | 2 +- .../components/Call/CallControls/ToggleCameraFaceButton.tsx | 1 - yarn.lock | 6 ++---- 3 files changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/react-native-sdk/src/components/Call/CallControls/RejectCallButton.tsx b/packages/react-native-sdk/src/components/Call/CallControls/RejectCallButton.tsx index 5d2878d5f6..8073c6a7f8 100644 --- a/packages/react-native-sdk/src/components/Call/CallControls/RejectCallButton.tsx +++ b/packages/react-native-sdk/src/components/Call/CallControls/RejectCallButton.tsx @@ -69,7 +69,7 @@ export const RejectCallButton = ({ // svgContainerStyle={theme.icon.lg} style={rejectCallButton} > - + ); }; 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 ae11ea6860..ac27479f69 100644 --- a/packages/react-native-sdk/src/components/Call/CallControls/ToggleCameraFaceButton.tsx +++ b/packages/react-native-sdk/src/components/Call/CallControls/ToggleCameraFaceButton.tsx @@ -36,7 +36,6 @@ export const ToggleCameraFaceButton = ({ return; } - // TODO: investigate issue with the camera flip await camera.flip(); }; diff --git a/yarn.lock b/yarn.lock index 557f9a207a..95c8cb70c6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -26173,21 +26173,19 @@ __metadata: "typescript@patch:typescript@4.9.5#~builtin": version: 4.9.5 - resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin::version=4.9.5&hash=289587" + resolution: "typescript@patch:typescript@npm%3A4.9.5#~builtin::version=4.9.5&hash=23ec76" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 1f8f3b6aaea19f0f67cba79057674ba580438a7db55057eb89cc06950483c5d632115c14077f6663ea76fd09fce3c190e6414bb98582ec80aa5a4eaf345d5b68 languageName: node linkType: hard "typescript@patch:typescript@^4.6.4 || ^5.0.0#~builtin, typescript@patch:typescript@^5.5.2#~builtin": version: 5.5.2 - resolution: "typescript@patch:typescript@npm%3A5.5.2#~builtin::version=5.5.2&hash=29ae49" + resolution: "typescript@patch:typescript@npm%3A5.5.2#~builtin::version=5.5.2&hash=85af82" bin: tsc: bin/tsc tsserver: bin/tsserver - checksum: 9d89bac0de650e15d6846485f238d1e65f1013f2c260d9e53e86a1da6ecf8109d9fad9402575c5c36a6592dc5d4370db090e12971c8630ae84453654baabb6b4 languageName: node linkType: hard From e77d38c80b89fa07bc569ae231c374e2fcb7c6e8 Mon Sep 17 00:00:00 2001 From: Kristian Martinoski Date: Thu, 17 Oct 2024 17:02:40 +0200 Subject: [PATCH 09/36] lint: fix reported issues --- .../src/components/Call/CallContent/CallContent.tsx | 3 +-- .../Call/RingingCallContent/RingingCallContent.tsx | 3 +-- .../Participant/ParticipantView/SpeechIndicator.tsx | 13 ++++++++----- .../react-native/dogfood/src/components/Button.tsx | 1 + .../CallControlls/LayoutSwitcherButton.tsx | 3 +-- .../components/CallControlls/RecordCallButton.tsx | 2 +- .../src/components/CallControlls/TopControls.tsx | 9 ++------- .../src/components/ChannelHeader/ChannelHeader.tsx | 5 +---- .../src/components/QuickDial/QuickDial.tsx | 6 +----- 9 files changed, 17 insertions(+), 28 deletions(-) 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 62827a9b9d..0ec66f8df2 100644 --- a/packages/react-native-sdk/src/components/Call/CallContent/CallContent.tsx +++ b/packages/react-native-sdk/src/components/Call/CallContent/CallContent.tsx @@ -1,7 +1,6 @@ import React, { useEffect, useState } from 'react'; import { StyleSheet, View } from 'react-native'; import InCallManager from 'react-native-incall-manager'; -import { CallTopViewProps } from '../CallTopView'; import { CallParticipantsGrid, CallParticipantsGridProps, @@ -44,7 +43,7 @@ type CallContentComponentProps = ParticipantViewComponentProps & /** * Component to customize the CallTopView component. */ - CallTopView?: React.ComponentType | null; + CallTopView?: React.ComponentType | null; /** * Component to customize the CallControls component. */ diff --git a/packages/react-native-sdk/src/components/Call/RingingCallContent/RingingCallContent.tsx b/packages/react-native-sdk/src/components/Call/RingingCallContent/RingingCallContent.tsx index 18d0524013..f84f222ad0 100644 --- a/packages/react-native-sdk/src/components/Call/RingingCallContent/RingingCallContent.tsx +++ b/packages/react-native-sdk/src/components/Call/RingingCallContent/RingingCallContent.tsx @@ -6,7 +6,6 @@ import { CallContent as DefaultCallContent, CallContentProps, } from '../CallContent'; -import { CallTopViewProps } from '../CallTopView'; import { IncomingCall as DefaultIncomingCall, IncomingCallProps, @@ -44,7 +43,7 @@ export type RingingCallContentProps = { /** * Prop to customize the CallTopView component in the RingingCallContent. */ - CallTopView?: React.ComponentType | null; + CallTopView?: React.ComponentType | null; /** * Prop to override the component shown when the call is left. */ diff --git a/packages/react-native-sdk/src/components/Participant/ParticipantView/SpeechIndicator.tsx b/packages/react-native-sdk/src/components/Participant/ParticipantView/SpeechIndicator.tsx index b02c9dd2c3..d7d78633aa 100644 --- a/packages/react-native-sdk/src/components/Participant/ParticipantView/SpeechIndicator.tsx +++ b/packages/react-native-sdk/src/components/Participant/ParticipantView/SpeechIndicator.tsx @@ -19,11 +19,14 @@ export type SpeechIndicatorProps = { */ export const SpeechIndicator = ({ isSpeaking }: SpeechIndicatorProps) => { const styles = useStyles(); - const animationValues = [ - useRef(new Animated.Value(0.6)).current, - useRef(new Animated.Value(0.6)).current, - useRef(new Animated.Value(0.6)).current, - ]; + const animatedValue1 = useRef(new Animated.Value(0.6)).current; + const animatedValue2 = useRef(new Animated.Value(0.6)).current; + const animatedValue3 = useRef(new Animated.Value(0.6)).current; + + const animationValues = useMemo( + () => [animatedValue1, animatedValue2, animatedValue3], + [animatedValue1, animatedValue2, animatedValue3] + ); useEffect(() => { if (isSpeaking) { diff --git a/sample-apps/react-native/dogfood/src/components/Button.tsx b/sample-apps/react-native/dogfood/src/components/Button.tsx index cd47db5fb0..889a1dd01a 100644 --- a/sample-apps/react-native/dogfood/src/components/Button.tsx +++ b/sample-apps/react-native/dogfood/src/components/Button.tsx @@ -48,6 +48,7 @@ export const Button = ({ const useStyles = () => { let appTheme: Theme; try { + /* eslint-disable react-hooks/rules-of-hooks */ appTheme = useTheme()?.theme; } catch (e) { appTheme = defaultTheme; 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 57f25d54a1..05f1324a16 100644 --- a/sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherButton.tsx +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherButton.tsx @@ -22,7 +22,7 @@ export const LayoutSwitcherButton = ({ onPressHandler, }: LayoutSwitcherButtonProps) => { const { - theme: { colors, LayoutSwitcherButton, defaults, variants }, + theme: { colors, defaults, variants }, } = useTheme(); const [toggleLayoutMenu, setToggleLayoutMenu] = useState(false); const buttonColor = toggleLayoutMenu @@ -39,7 +39,6 @@ export const LayoutSwitcherButton = ({ setToggleLayoutMenu(!toggleLayoutMenu); }} color={colors.sheetPrimary} - style={LayoutSwitcherButton} > diff --git a/sample-apps/react-native/dogfood/src/components/CallControlls/RecordCallButton.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/RecordCallButton.tsx index 8ae8840651..a3484758a2 100644 --- a/sample-apps/react-native/dogfood/src/components/CallControlls/RecordCallButton.tsx +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/RecordCallButton.tsx @@ -23,7 +23,7 @@ export type RecordCallButtonProps = { */ export const RecordCallButton = ({ onPressHandler }: RecordCallButtonProps) => { const { - theme: { colors, recordCallButton, defaults, variants }, + theme: { colors, recordCallButton, variants }, } = useTheme(); const [isRecording, setIsRecording] = useState(false); const buttonColor = isRecording 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 36873fa79c..b89be3464d 100644 --- a/sample-apps/react-native/dogfood/src/components/CallControlls/TopControls.tsx +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/TopControls.tsx @@ -29,17 +29,12 @@ export const TopControls = ({ const [topControlsHeight, setTopControlsHeight] = useState(0); const [topControlsWidth, setTopControlsWidth] = useState(0); const { - theme: { - colors, - typefaces, - variants: { iconSizes }, - callTopView, - }, + theme: { callTopView }, } = useTheme(); const styles = useStyles(); // TODO: replace this with real data implement PBE-5871 [Demo App] Call Recording flow - const [isCallRecorded, setIsCallRecorded] = useState(false); + const [isCallRecorded] = useState(false); const onLayout: React.ComponentProps['onLayout'] = (event) => { const { height, width } = event.nativeEvent.layout; diff --git a/sample-apps/react/messenger-clone/src/components/ChannelHeader/ChannelHeader.tsx b/sample-apps/react/messenger-clone/src/components/ChannelHeader/ChannelHeader.tsx index c6085ab2df..7afbca6bae 100644 --- a/sample-apps/react/messenger-clone/src/components/ChannelHeader/ChannelHeader.tsx +++ b/sample-apps/react/messenger-clone/src/components/ChannelHeader/ChannelHeader.tsx @@ -44,10 +44,7 @@ const UnMemoizedChannelHeader = (props: ChannelHeaderProps) => { > - +

{displayTitle}{' '} diff --git a/sample-apps/react/messenger-clone/src/components/QuickDial/QuickDial.tsx b/sample-apps/react/messenger-clone/src/components/QuickDial/QuickDial.tsx index 11c04d7128..47e6527b6e 100644 --- a/sample-apps/react/messenger-clone/src/components/QuickDial/QuickDial.tsx +++ b/sample-apps/react/messenger-clone/src/components/QuickDial/QuickDial.tsx @@ -120,11 +120,7 @@ const QuickDialButton = ({ away: !user.online, })} > - + ); }; From 98e742eb3e90412015f458967c2685f0a387b0da Mon Sep 17 00:00:00 2001 From: Kristian Martinoski Date: Fri, 18 Oct 2024 14:47:02 +0200 Subject: [PATCH 10/36] feat: add call duration observable in the call state --- packages/client/src/store/CallState.ts | 28 +++++++++++++++++++ .../src/hooks/callStateHooks.ts | 10 +++++++ .../CallControlls/CallStatusBadge.tsx | 13 ++------- 3 files changed, 41 insertions(+), 10 deletions(-) diff --git a/packages/client/src/store/CallState.ts b/packages/client/src/store/CallState.ts index 5a379387f8..cd1d55b8cc 100644 --- a/packages/client/src/store/CallState.ts +++ b/packages/client/src/store/CallState.ts @@ -117,6 +117,7 @@ export class CallState { private callStatsReportSubject = new BehaviorSubject< CallStatsReport | undefined >(undefined); + private durationSubject = new BehaviorSubject(0); // These are tracks that were delivered to the Subscriber's onTrack event // that we couldn't associate with a participant yet. @@ -285,6 +286,11 @@ export class CallState { */ thumbnails$: Observable; + /** + * Will provide the count of seconds since the call started. + */ + duration$: Observable; + readonly logger = getLogger(['CallState']); /** @@ -391,6 +397,11 @@ export class CallState { this.recording$ = duc(this.recordingSubject); this.transcribing$ = duc(this.transcribingSubject); + this.duration$ = duc(this.durationSubject); + setInterval(() => { + this.setDuration((d) => d + 1); + }, 1000); + this.eventHandlers = { // these events are not updating the call state: 'call.closed_caption': undefined, @@ -515,6 +526,23 @@ export class CallState { return this.setCurrentValue(this.participantCountSubject, count); }; + /** + * The number of seconds since the start of the call. + */ + get duration() { + return this.getCurrentValue(this.duration$); + } + + /** + * Sets the number of seconds since the start of the call. + * + * @internal + * @param duration the duration of the call in seconds. + */ + setDuration = (duration: Patch) => { + return this.setCurrentValue(this.durationSubject, duration); + }; + /** * The time the call session actually started. * Useful for displaying the call duration. diff --git a/packages/react-bindings/src/hooks/callStateHooks.ts b/packages/react-bindings/src/hooks/callStateHooks.ts index e4e7a825ae..b147451384 100644 --- a/packages/react-bindings/src/hooks/callStateHooks.ts +++ b/packages/react-bindings/src/hooks/callStateHooks.ts @@ -306,6 +306,16 @@ export const useParticipantCount = () => { return useObservableValue(participantCount$); }; +/** + * Returns the duration of the call in seconds. + * + * @category Call State + */ +export const useCallDuration = () => { + const { duration$ } = useCallState(); + return useObservableValue(duration$); +}; + /** * Returns the approximate anonymous participant count of the active call. * The regular participants are not included in this count. It is computed on the server. diff --git a/sample-apps/react-native/dogfood/src/components/CallControlls/CallStatusBadge.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/CallStatusBadge.tsx index 2032d59f21..a98955820f 100644 --- a/sample-apps/react-native/dogfood/src/components/CallControlls/CallStatusBadge.tsx +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/CallStatusBadge.tsx @@ -3,7 +3,7 @@ import { View, Text, StyleSheet } from 'react-native'; import { CallDuration } from '../../assets/CallDuration'; import { RecordCall } from '@stream-io/video-react-native-sdk/src/icons/RecordCall'; import { IconWrapper } from '@stream-io/video-react-native-sdk/src/icons'; -import { useTheme } from '@stream-io/video-react-native-sdk'; +import { useCallStateHooks, useTheme } from '@stream-io/video-react-native-sdk'; /** * Props for the CallStatusBadge component. @@ -33,15 +33,8 @@ export const CallStatusBadge: React.FC = ({ }, } = useTheme(); - const [duration, setDuration] = useState(0); - - // Increment the duration every second - useEffect(() => { - const id = setInterval(() => { - setDuration((d) => d + 1); - }, 1000); - return () => clearInterval(id); - }, []); + const { useCallDuration } = useCallStateHooks(); + const duration = useCallDuration(); const styles = useStyles(isCallRecorded); From c7399e6d963d5261ffeaf42584e57dd044b72634 Mon Sep 17 00:00:00 2001 From: Kristian Martinoski Date: Sat, 19 Oct 2024 13:32:57 +0200 Subject: [PATCH 11/36] feat(design-v2): adds layout switcher components --- .../src/components/Call/Lobby/Lobby.tsx | 111 ++++++------ packages/react-native-sdk/src/icons/Grid.tsx | 17 -- .../react-native-sdk/src/icons/Spotlight.tsx | 18 -- packages/react-native-sdk/src/icons/index.tsx | 1 - packages/react-native-sdk/src/theme/theme.ts | 8 + .../dogfood/src/assets/FullScreen.tsx | 17 ++ .../react-native/dogfood/src/assets/Grid.tsx | 17 ++ .../dogfood/src/assets/Spotlight.tsx | 17 ++ .../dogfood/src/components/ActiveCall.tsx | 22 ++- .../CallControlls/LayoutSwitcherButton.tsx | 57 +++++- .../CallControlls/LayoutSwitcherModal.tsx | 163 ++++++++++++++++++ .../components/CallControlls/TopControls.tsx | 19 +- .../src/components/ParticipantsInfoList.tsx | 1 + .../src/screens/Meeting/JoinMeetingScreen.tsx | 114 ++++++------ 14 files changed, 423 insertions(+), 159 deletions(-) delete mode 100644 packages/react-native-sdk/src/icons/Grid.tsx delete mode 100644 packages/react-native-sdk/src/icons/Spotlight.tsx create mode 100644 sample-apps/react-native/dogfood/src/assets/FullScreen.tsx create mode 100644 sample-apps/react-native/dogfood/src/assets/Grid.tsx create mode 100644 sample-apps/react-native/dogfood/src/assets/Spotlight.tsx create mode 100644 sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherModal.tsx 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/icons/Grid.tsx b/packages/react-native-sdk/src/icons/Grid.tsx deleted file mode 100644 index 2f64742f5f..0000000000 --- a/packages/react-native-sdk/src/icons/Grid.tsx +++ /dev/null @@ -1,17 +0,0 @@ -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/packages/react-native-sdk/src/icons/Spotlight.tsx b/packages/react-native-sdk/src/icons/Spotlight.tsx deleted file mode 100644 index 30a39acb21..0000000000 --- a/packages/react-native-sdk/src/icons/Spotlight.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import React from 'react'; -import { Svg, Path } from 'react-native-svg'; -import { ColorValue } from 'react-native/types'; - -type Props = { - color: ColorValue; -}; - -export const SpotLight = ({ color }: 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 5b7eead4f3..0e35318c9b 100644 --- a/packages/react-native-sdk/src/theme/theme.ts +++ b/packages/react-native-sdk/src/theme/theme.ts @@ -17,6 +17,7 @@ export type Theme = { avatarSizes: DimensionType; fontSizes: DimensionType; spacingSizes: DimensionType; + borderRadiusSizes: DimensionType; insets: Insets; }; typefaces: Record; @@ -298,6 +299,13 @@ export const defaultTheme: Theme = { lg: 44, xl: 56, }, + borderRadiusSizes: { + xs: 4, + sm: 8, + md: 16, + lg: 24, + xl: 32, + }, buttonSizes: { xs: 40, sm: 50, 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..8ecaaa7384 --- /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..5f42a14956 --- /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..d1ecff1aa9 --- /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 11eb695a0b..073ae083d8 100644 --- a/sample-apps/react-native/dogfood/src/components/ActiveCall.tsx +++ b/sample-apps/react-native/dogfood/src/components/ActiveCall.tsx @@ -16,14 +16,12 @@ import { Z_INDEX } from '../constants'; import { TopControls } from './CallControlls/TopControls'; type ActiveCallProps = BottomControlsProps & { - onBackPressed?: () => void; onHangupCallHandler?: () => void; onCallEnded: () => void; }; export const ActiveCall = ({ onChatOpenHandler, - onBackPressed, onHangupCallHandler, onCallEnded, unreadCountIndicator, @@ -66,9 +64,10 @@ export const ActiveCall = ({ backgroundColor={colors.sheetPrimary} /> + + { const { theme } = useTheme(); - return useMemo( () => StyleSheet.create({ @@ -110,6 +108,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/LayoutSwitcherButton.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherButton.tsx index 05f1324a16..b40ebe39af 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,14 @@ 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, LayoutChangeEvent } from 'react-native'; +import { Grid } from '../../assets/Grid'; +import { SpotLight } from '../../assets/Spotlight'; +import { FullScreen } from '../../assets/FullScreen'; export type LayoutSwitcherButtonProps = { /** @@ -14,6 +18,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. @@ -24,25 +41,55 @@ export const LayoutSwitcherButton = ({ const { theme: { colors, defaults, variants }, } = useTheme(); - const [toggleLayoutMenu, setToggleLayoutMenu] = useState(false); - const buttonColor = toggleLayoutMenu + + const [selectedButton, setSelectedButton] = useState('grid'); + const [isPopupVisible, setIsPopupVisible] = useState(false); + const [anchorPosition, setAnchorPosition] = useState<{ + x: number; + y: number; + width: number; + height: number; + } | null>(null); + + const buttonColor = isPopupVisible ? colors.iconPrimaryAccent : colors.iconPrimaryDefault; + const handlePress = () => setIsPopupVisible(true); + const handleClosePopup = () => setIsPopupVisible(false); + + const handleButtonSelection = (buttonName: string) => { + setSelectedButton(buttonName); + }; + + const handleLayout = (event: LayoutChangeEvent) => { + const { x, y, width, height } = event.nativeEvent.layout; + setAnchorPosition({ x, y: y + height, width, height }); + }; + return ( { + handlePress(); if (onPressHandler) { onPressHandler(); } - setToggleLayoutMenu(!toggleLayoutMenu); + setIsPopupVisible(!isPopupVisible); }} color={colors.sheetPrimary} > - + {getIcon(selectedButton, 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..9041e2a5e3 --- /dev/null +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherModal.tsx @@ -0,0 +1,163 @@ +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'; + +interface AnchorPosition { + x: number; + y: number; + height: number; +} + +interface PopupComponentProps { + anchorPosition?: AnchorPosition | null; + isVisible: boolean; + onClose: () => void; + selectedButton: string; + onSelectButton: (buttonName: string) => void; +} + +const LayoutSwitcherModal: React.FC = ({ + isVisible, + onClose, + anchorPosition, + onSelectButton, + selectedButton, +}) => { + const { theme } = useTheme(); + const styles = useStyles(); + const [popupPosition, setPopupPosition] = useState({ top: 0, left: 0 }); + + useEffect(() => { + if (isVisible && anchorPosition) { + const windowHeight = Dimensions.get('window').height; + const windowWidth = Dimensions.get('window').width; + + let top = + anchorPosition.y + + anchorPosition.height / 2 + + theme.variants.insets.top; + let left = anchorPosition.x + theme.variants.insets.left; + + // 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]); + + if (!isVisible || !anchorPosition) return null; + + return ( + + + + onSelectButton('grid')} + > + + Grid + + onSelectButton('spotlight')} + > + + Spotlight + + onSelectButton('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', + 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 b89be3464d..0813680a4e 100644 --- a/sample-apps/react-native/dogfood/src/components/CallControlls/TopControls.tsx +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/TopControls.tsx @@ -1,5 +1,5 @@ import React, { useState, useMemo } from 'react'; -import { View, StyleSheet, StyleProp, ViewStyle } from 'react-native'; +import { View, StyleSheet } from 'react-native'; import { TopViewBackground } from '@stream-io/video-react-native-sdk/src/icons'; import { HangUpCallButton, @@ -16,16 +16,9 @@ export type TopControlsProps = { * @returns void */ onHangupCallHandler?: () => void; - /** - * Style to override the container of the TopControls. - */ - style?: StyleProp; }; -export const TopControls = ({ - onHangupCallHandler, - style: styleProp, -}: TopControlsProps) => { +export const TopControls = ({ onHangupCallHandler }: TopControlsProps) => { const [topControlsHeight, setTopControlsHeight] = useState(0); const [topControlsWidth, setTopControlsWidth] = useState(0); const { @@ -45,18 +38,18 @@ export const TopControls = ({ }; return ( - + {/* Component for the background of the TopControls. Since it has a Linear Gradient, an SVG is used to render it. */} - + - {}} /> + {!isCallRecorded && } - + diff --git a/sample-apps/react-native/dogfood/src/components/ParticipantsInfoList.tsx b/sample-apps/react-native/dogfood/src/components/ParticipantsInfoList.tsx index 8fe4964efa..4391629c99 100644 --- a/sample-apps/react-native/dogfood/src/components/ParticipantsInfoList.tsx +++ b/sample-apps/react-native/dogfood/src/components/ParticipantsInfoList.tsx @@ -109,6 +109,7 @@ export const ParticipantsInfoList = ({ transparent visible={isCallParticipantsInfoVisible} onRequestClose={onCloseCallParticipantsVisible} + supportedOrientations={['portrait', 'landscape']} > <> {/*independent background, needed due to desired opacity only 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; From 1b773c01ae6c6bc585855ab5d563f3bfd7d8d6e1 Mon Sep 17 00:00:00 2001 From: Kristian Martinoski Date: Sat, 19 Oct 2024 13:45:07 +0200 Subject: [PATCH 12/36] fix: self code review improvements --- .../CallControlls/LayoutSwitcherButton.tsx | 16 ++++++++-------- .../CallControlls/LayoutSwitcherModal.tsx | 2 ++ 2 files changed, 10 insertions(+), 8 deletions(-) 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 b40ebe39af..277ed4ccef 100644 --- a/sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherButton.tsx +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherButton.tsx @@ -43,7 +43,7 @@ export const LayoutSwitcherButton = ({ } = useTheme(); const [selectedButton, setSelectedButton] = useState('grid'); - const [isPopupVisible, setIsPopupVisible] = useState(false); + const [isModalVisible, setIsModalVisible] = useState(false); const [anchorPosition, setAnchorPosition] = useState<{ x: number; y: number; @@ -51,12 +51,12 @@ export const LayoutSwitcherButton = ({ height: number; } | null>(null); - const buttonColor = isPopupVisible + const buttonColor = isModalVisible ? colors.iconPrimaryAccent : colors.iconPrimaryDefault; - const handlePress = () => setIsPopupVisible(true); - const handleClosePopup = () => setIsPopupVisible(false); + const handleOpenModal = () => setIsModalVisible(true); + const handleCloseModal = () => setIsModalVisible(false); const handleButtonSelection = (buttonName: string) => { setSelectedButton(buttonName); @@ -72,11 +72,11 @@ export const LayoutSwitcherButton = ({ size={variants.iconSizes.lg} onLayout={handleLayout} onPress={() => { - handlePress(); + handleOpenModal(); if (onPressHandler) { onPressHandler(); } - setIsPopupVisible(!isPopupVisible); + setIsModalVisible(!isModalVisible); }} color={colors.sheetPrimary} > @@ -84,9 +84,9 @@ export const LayoutSwitcherButton = ({ {getIcon(selectedButton, 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 index 9041e2a5e3..285824438b 100644 --- a/sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherModal.tsx +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherModal.tsx @@ -153,6 +153,8 @@ const useStyles = () => { buttonText: { color: 'white', textAlign: 'center', + fontWeight: '600', + marginTop: 2, marginLeft: theme.variants.spacingSizes.xs, }, }), From 8220903bc3cfc77941d54512adc9ed41eef0a49f Mon Sep 17 00:00:00 2001 From: Kristian Martinoski Date: Mon, 21 Oct 2024 15:11:50 +0200 Subject: [PATCH 13/36] feat: add layout context and fix layout icons --- .../Call/CallContent/CallContent.tsx | 4 +- .../CallControls/ToggleCameraFaceButton.tsx | 4 +- .../src/icons/CameraSwitch.tsx | 2 +- .../react-native-sdk/src/icons/Effects.tsx | 2 +- .../dogfood/src/components/ActiveCall.tsx | 5 +- .../CallControlls/LayoutSwitcherButton.tsx | 11 +- .../CallControlls/LayoutSwitcherModal.tsx | 18 ++- .../components/CallControlls/TopControls.tsx | 4 - .../dogfood/src/components/MeetingUI.tsx | 17 ++- .../components/ParticipantsLayoutButton.tsx | 133 ------------------ .../components/VideoEffectsButton/index.tsx | 2 +- .../dogfood/src/contexts/LayoutContext.tsx | 44 ++++++ 12 files changed, 76 insertions(+), 170 deletions(-) delete mode 100644 sample-apps/react-native/dogfood/src/components/ParticipantsLayoutButton.tsx create mode 100644 sample-apps/react-native/dogfood/src/contexts/LayoutContext.tsx 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..2fc7faec02 100644 --- a/packages/react-native-sdk/src/components/Call/CallContent/CallContent.tsx +++ b/packages/react-native-sdk/src/components/Call/CallContent/CallContent.tsx @@ -68,9 +68,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 */ 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 = ({ > ( - + ( - + void; @@ -32,6 +33,7 @@ export const ActiveCall = ({ const currentOrientation = useOrientation(); const styles = useStyles(); const { theme: colors } = useTheme(); + const { selectedLayout } = useLayout(); const onOpenCallParticipantsInfo = useCallback(() => { setIsCallParticipantsVisible(true); @@ -67,11 +69,12 @@ export const ActiveCall = ({ + ('grid'); + const { selectedLayout } = useLayout(); const [isModalVisible, setIsModalVisible] = useState(false); const [anchorPosition, setAnchorPosition] = useState<{ x: number; @@ -58,10 +59,6 @@ export const LayoutSwitcherButton = ({ const handleOpenModal = () => setIsModalVisible(true); const handleCloseModal = () => setIsModalVisible(false); - const handleButtonSelection = (buttonName: string) => { - setSelectedButton(buttonName); - }; - const handleLayout = (event: LayoutChangeEvent) => { const { x, y, width, height } = event.nativeEvent.layout; setAnchorPosition({ x, y: y + height, width, height }); @@ -81,14 +78,12 @@ export const LayoutSwitcherButton = ({ color={colors.sheetPrimary} > - {getIcon(selectedButton, buttonColor, variants.iconSizes.lg)} + {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 index 285824438b..b77e321f3a 100644 --- a/sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherModal.tsx +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherModal.tsx @@ -11,6 +11,7 @@ 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 { useLayout } from '../../contexts/LayoutContext'; interface AnchorPosition { x: number; @@ -22,20 +23,17 @@ interface PopupComponentProps { anchorPosition?: AnchorPosition | null; isVisible: boolean; onClose: () => void; - selectedButton: string; - onSelectButton: (buttonName: string) => void; } const LayoutSwitcherModal: React.FC = ({ isVisible, onClose, anchorPosition, - onSelectButton, - selectedButton, }) => { const { theme } = useTheme(); const styles = useStyles(); const [popupPosition, setPopupPosition] = useState({ top: 0, left: 0 }); + const { selectedLayout, onLayoutSelection } = useLayout(); useEffect(() => { if (isVisible && anchorPosition) { @@ -79,9 +77,9 @@ const LayoutSwitcherModal: React.FC = ({ onSelectButton('grid')} + onPress={() => onLayoutSelection('grid')} > = ({ onSelectButton('spotlight')} + onPress={() => onLayoutSelection('spotlight')} > = ({ onSelectButton('fullscreen')} + onPress={() => onLayoutSelection('fullscreen')} > { const [topControlsHeight, setTopControlsHeight] = useState(0); const [topControlsWidth, setTopControlsWidth] = useState(0); - const { - theme: { callTopView }, - } = useTheme(); const styles = useStyles(); // TODO: replace this with real data implement PBE-5871 [Demo App] Call Recording flow @@ -62,7 +59,6 @@ export const TopControls = ({ onHangupCallHandler }: TopControlsProps) => { 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 ce475a06e5..5533667430 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, @@ -126,13 +127,15 @@ 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 4eb228c75b..0000000000 --- a/sample-apps/react-native/dogfood/src/components/ParticipantsLayoutButton.tsx +++ /dev/null @@ -1,133 +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 d83be3c82d..16a276fcb8 100644 --- a/sample-apps/react-native/dogfood/src/components/VideoEffectsButton/index.tsx +++ b/sample-apps/react-native/dogfood/src/components/VideoEffectsButton/index.tsx @@ -85,7 +85,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 }; From 32f94a39715fb42dd01564ef5317554625ccf74e Mon Sep 17 00:00:00 2001 From: Kristian Martinoski Date: Tue, 22 Oct 2024 16:11:37 +0200 Subject: [PATCH 14/36] fix: lint reported issues --- .../dogfood/src/assets/FullScreen.tsx | 2 +- .../react-native/dogfood/src/assets/Grid.tsx | 2 +- .../react-native/dogfood/src/assets/Spotlight.tsx | 2 +- .../components/CallControlls/CallStatusBadge.tsx | 2 +- .../CallControlls/LayoutSwitcherButton.tsx | 2 +- .../CallControlls/LayoutSwitcherModal.tsx | 15 ++++++++------- 6 files changed, 13 insertions(+), 12 deletions(-) diff --git a/sample-apps/react-native/dogfood/src/assets/FullScreen.tsx b/sample-apps/react-native/dogfood/src/assets/FullScreen.tsx index 8ecaaa7384..553b889f9f 100644 --- a/sample-apps/react-native/dogfood/src/assets/FullScreen.tsx +++ b/sample-apps/react-native/dogfood/src/assets/FullScreen.tsx @@ -8,7 +8,7 @@ type Props = { }; export const FullScreen = ({ color, size }: Props) => ( - + ( - + ( - + { const { - theme: { colors, defaults, variants }, + theme: { colors, variants }, } = useTheme(); const { selectedLayout } = useLayout(); diff --git a/sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherModal.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherModal.tsx index b77e321f3a..3a9c55657b 100644 --- a/sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherModal.tsx +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherModal.tsx @@ -34,17 +34,16 @@ const LayoutSwitcherModal: React.FC = ({ 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 + - theme.variants.insets.top; - let left = anchorPosition.x + theme.variants.insets.left; + 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) { @@ -56,9 +55,11 @@ const LayoutSwitcherModal: React.FC = ({ setPopupPosition({ top, left }); } - }, [isVisible, anchorPosition]); + }, [isVisible, anchorPosition, topInset, leftInset]); - if (!isVisible || !anchorPosition) return null; + if (!isVisible || !anchorPosition) { + return null; + } return ( Date: Tue, 22 Oct 2024 16:12:35 +0200 Subject: [PATCH 15/36] fix: lint reported issues --- .../dogfood/src/components/CallControlls/CallStatusBadge.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sample-apps/react-native/dogfood/src/components/CallControlls/CallStatusBadge.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/CallStatusBadge.tsx index a98955820f..a869719f88 100644 --- a/sample-apps/react-native/dogfood/src/components/CallControlls/CallStatusBadge.tsx +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/CallStatusBadge.tsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect, useMemo } from 'react'; +import React, { useMemo } from 'react'; import { View, Text, StyleSheet } from 'react-native'; import { CallDuration } from '../../assets/CallDuration'; import { RecordCall } from '@stream-io/video-react-native-sdk/src/icons/RecordCall'; From 62b613709922950138204b6e11f30ed2ec143bbf Mon Sep 17 00:00:00 2001 From: Kristian Martinoski Date: Tue, 22 Oct 2024 16:18:33 +0200 Subject: [PATCH 16/36] remove duration changes --- packages/client/src/store/CallState.ts | 28 ------------------- .../src/hooks/callStateHooks.ts | 10 ------- .../CallControlls/CallStatusBadge.tsx | 18 ++++++++---- 3 files changed, 13 insertions(+), 43 deletions(-) diff --git a/packages/client/src/store/CallState.ts b/packages/client/src/store/CallState.ts index cd1d55b8cc..5a379387f8 100644 --- a/packages/client/src/store/CallState.ts +++ b/packages/client/src/store/CallState.ts @@ -117,7 +117,6 @@ export class CallState { private callStatsReportSubject = new BehaviorSubject< CallStatsReport | undefined >(undefined); - private durationSubject = new BehaviorSubject(0); // These are tracks that were delivered to the Subscriber's onTrack event // that we couldn't associate with a participant yet. @@ -286,11 +285,6 @@ export class CallState { */ thumbnails$: Observable; - /** - * Will provide the count of seconds since the call started. - */ - duration$: Observable; - readonly logger = getLogger(['CallState']); /** @@ -397,11 +391,6 @@ export class CallState { this.recording$ = duc(this.recordingSubject); this.transcribing$ = duc(this.transcribingSubject); - this.duration$ = duc(this.durationSubject); - setInterval(() => { - this.setDuration((d) => d + 1); - }, 1000); - this.eventHandlers = { // these events are not updating the call state: 'call.closed_caption': undefined, @@ -526,23 +515,6 @@ export class CallState { return this.setCurrentValue(this.participantCountSubject, count); }; - /** - * The number of seconds since the start of the call. - */ - get duration() { - return this.getCurrentValue(this.duration$); - } - - /** - * Sets the number of seconds since the start of the call. - * - * @internal - * @param duration the duration of the call in seconds. - */ - setDuration = (duration: Patch) => { - return this.setCurrentValue(this.durationSubject, duration); - }; - /** * The time the call session actually started. * Useful for displaying the call duration. diff --git a/packages/react-bindings/src/hooks/callStateHooks.ts b/packages/react-bindings/src/hooks/callStateHooks.ts index b147451384..e4e7a825ae 100644 --- a/packages/react-bindings/src/hooks/callStateHooks.ts +++ b/packages/react-bindings/src/hooks/callStateHooks.ts @@ -306,16 +306,6 @@ export const useParticipantCount = () => { return useObservableValue(participantCount$); }; -/** - * Returns the duration of the call in seconds. - * - * @category Call State - */ -export const useCallDuration = () => { - const { duration$ } = useCallState(); - return useObservableValue(duration$); -}; - /** * Returns the approximate anonymous participant count of the active call. * The regular participants are not included in this count. It is computed on the server. diff --git a/sample-apps/react-native/dogfood/src/components/CallControlls/CallStatusBadge.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/CallStatusBadge.tsx index a869719f88..2e6bcf3f65 100644 --- a/sample-apps/react-native/dogfood/src/components/CallControlls/CallStatusBadge.tsx +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/CallStatusBadge.tsx @@ -1,4 +1,4 @@ -import React, { useMemo } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { View, Text, StyleSheet } from 'react-native'; import { CallDuration } from '../../assets/CallDuration'; import { RecordCall } from '@stream-io/video-react-native-sdk/src/icons/RecordCall'; @@ -33,16 +33,24 @@ export const CallStatusBadge: React.FC = ({ }, } = useTheme(); - const { useCallDuration } = useCallStateHooks(); - const duration = useCallDuration(); + // TODO: replace this with useDuration when that https://github.com/GetStream/stream-video-js/pull/1528 is merged + const [elapsedSeconds, setElapsedSeconds] = useState(0); + useEffect(() => { + const startedAt = new Date().getTime(); + setElapsedSeconds(Math.floor((Date.now() - startedAt) / 1000)); + const intervalId = setInterval(() => { + setElapsedSeconds((prevSeconds) => prevSeconds + 1); + }, 1000); + return () => clearInterval(intervalId); + }, []); const styles = useStyles(isCallRecorded); // Format duration to MM:SS - const minutes = Math.floor(duration / 60) + const minutes = Math.floor(elapsedSeconds / 60) .toString() .padStart(2, '0'); - const seconds = (duration % 60).toString().padStart(2, '0'); + const seconds = (elapsedSeconds % 60).toString().padStart(2, '0'); const timestamp = `${minutes}:${seconds}`; const text = isCallRecorded ? 'Recording in progress...' : timestamp; From 5de19206f58df23b65066a443d1f3ddb24f75c66 Mon Sep 17 00:00:00 2001 From: Kristian Martinoski Date: Tue, 22 Oct 2024 16:28:38 +0200 Subject: [PATCH 17/36] fix: lint issue --- .../dogfood/src/components/CallControlls/CallStatusBadge.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sample-apps/react-native/dogfood/src/components/CallControlls/CallStatusBadge.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/CallStatusBadge.tsx index 2e6bcf3f65..7698b8f397 100644 --- a/sample-apps/react-native/dogfood/src/components/CallControlls/CallStatusBadge.tsx +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/CallStatusBadge.tsx @@ -3,7 +3,7 @@ import { View, Text, StyleSheet } from 'react-native'; import { CallDuration } from '../../assets/CallDuration'; import { RecordCall } from '@stream-io/video-react-native-sdk/src/icons/RecordCall'; import { IconWrapper } from '@stream-io/video-react-native-sdk/src/icons'; -import { useCallStateHooks, useTheme } from '@stream-io/video-react-native-sdk'; +import { useTheme } from '@stream-io/video-react-native-sdk'; /** * Props for the CallStatusBadge component. From 38141b9d53c084a75c29ad134e74a320c3a124b7 Mon Sep 17 00:00:00 2001 From: Kristian Martinoski Date: Tue, 22 Oct 2024 18:12:15 +0200 Subject: [PATCH 18/36] fix: lint issues --- .../dogfood/src/components/ActiveCall.tsx | 3 --- .../components/CallControlls/TopControls.tsx | 23 ++++++++++--------- .../dogfood/src/components/MeetingUI.tsx | 1 - 3 files changed, 12 insertions(+), 15 deletions(-) diff --git a/sample-apps/react-native/dogfood/src/components/ActiveCall.tsx b/sample-apps/react-native/dogfood/src/components/ActiveCall.tsx index 11eb695a0b..0ed9194ab4 100644 --- a/sample-apps/react-native/dogfood/src/components/ActiveCall.tsx +++ b/sample-apps/react-native/dogfood/src/components/ActiveCall.tsx @@ -16,14 +16,12 @@ import { Z_INDEX } from '../constants'; import { TopControls } from './CallControlls/TopControls'; type ActiveCallProps = BottomControlsProps & { - onBackPressed?: () => void; onHangupCallHandler?: () => void; onCallEnded: () => void; }; export const ActiveCall = ({ onChatOpenHandler, - onBackPressed, onHangupCallHandler, onCallEnded, unreadCountIndicator, @@ -68,7 +66,6 @@ export const ActiveCall = ({ void; - /** - * Style to override the container of the TopControls. - */ - style?: StyleProp; }; -export const TopControls = ({ - onHangupCallHandler, - style: styleProp, -}: TopControlsProps) => { +export const TopControls = ({ onHangupCallHandler }: TopControlsProps) => { const [topControlsHeight, setTopControlsHeight] = useState(0); const [topControlsWidth, setTopControlsWidth] = useState(0); const { @@ -45,10 +38,13 @@ export const TopControls = ({ }; return ( - + }> {/* Component for the background of the TopControls. Since it has a Linear Gradient, an SVG is used to render it. */} - + ]} + onLayout={onLayout} + > {}} /> @@ -56,7 +52,12 @@ export const TopControls = ({ {!isCallRecorded && } - + , + ]} + > diff --git a/sample-apps/react-native/dogfood/src/components/MeetingUI.tsx b/sample-apps/react-native/dogfood/src/components/MeetingUI.tsx index 766d9ea3af..b5765d0749 100644 --- a/sample-apps/react-native/dogfood/src/components/MeetingUI.tsx +++ b/sample-apps/react-native/dogfood/src/components/MeetingUI.tsx @@ -134,7 +134,6 @@ export const MeetingUI = ({ callId, navigation, route }: Props) => { onHangupCallHandler={onHangupCallHandler} onChatOpenHandler={onChatOpenHandler} unreadCountIndicator={unreadCountIndicator} - onBackPressed={onHangupCallHandler} /> ); } From 0903b303cb5511ef3d6d1b7e4d6992da6b151fb3 Mon Sep 17 00:00:00 2001 From: Kristian Martinoski Date: Tue, 22 Oct 2024 21:24:20 +0200 Subject: [PATCH 19/36] fix: lint issues --- .../components/CallControlls/TopControls.tsx | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) 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 e84a18e4ce..105bd0a5cf 100644 --- a/sample-apps/react-native/dogfood/src/components/CallControlls/TopControls.tsx +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/TopControls.tsx @@ -1,5 +1,5 @@ import React, { useState, useMemo } from 'react'; -import { View, StyleSheet, StyleProp, ViewStyle } from 'react-native'; +import { View, StyleSheet } from 'react-native'; import { TopViewBackground } from '@stream-io/video-react-native-sdk/src/icons'; import { HangUpCallButton, @@ -21,9 +21,6 @@ export type TopControlsProps = { export const TopControls = ({ onHangupCallHandler }: TopControlsProps) => { const [topControlsHeight, setTopControlsHeight] = useState(0); const [topControlsWidth, setTopControlsWidth] = useState(0); - const { - theme: { callTopView }, - } = useTheme(); const styles = useStyles(); // TODO: replace this with real data implement PBE-5871 [Demo App] Call Recording flow @@ -38,13 +35,10 @@ export const TopControls = ({ onHangupCallHandler }: TopControlsProps) => { }; return ( - }> + {/* Component for the background of the TopControls. Since it has a Linear Gradient, an SVG is used to render it. */} - ]} - onLayout={onLayout} - > + {}} /> @@ -52,12 +46,7 @@ export const TopControls = ({ onHangupCallHandler }: TopControlsProps) => { {!isCallRecorded && } - , - ]} - > + From 53b908a6a4bc6b8f53c7e99b7ed7cb4b10034d58 Mon Sep 17 00:00:00 2001 From: Kristian Martinoski Date: Tue, 22 Oct 2024 21:40:12 +0200 Subject: [PATCH 20/36] fix: lint reported issues --- .../src/components/CallControlls/LayoutSwitcherButton.tsx | 4 ++-- sample-apps/react-native/dogfood/src/components/MeetingUI.tsx | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) 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 89e518a917..3d9d9987b9 100644 --- a/sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherButton.tsx +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherButton.tsx @@ -5,7 +5,7 @@ import { } from '@stream-io/video-react-native-sdk'; import { IconWrapper } from '@stream-io/video-react-native-sdk/src/icons'; import LayoutSwitcherModal from './LayoutSwitcherModal'; -import { ColorValue, LayoutChangeEvent } from 'react-native'; +import { ColorValue } from 'react-native'; import { Grid } from '../../assets/Grid'; import { SpotLight } from '../../assets/Spotlight'; import { FullScreen } from '../../assets/FullScreen'; @@ -59,7 +59,7 @@ export const LayoutSwitcherButton = ({ const handleOpenModal = () => setIsModalVisible(true); const handleCloseModal = () => setIsModalVisible(false); - const handleLayout = (event: LayoutChangeEvent) => { + const handleLayout = (event: any) => { const { x, y, width, height } = event.nativeEvent.layout; setAnchorPosition({ x, y: y + height, width, height }); }; diff --git a/sample-apps/react-native/dogfood/src/components/MeetingUI.tsx b/sample-apps/react-native/dogfood/src/components/MeetingUI.tsx index 6d3d6715db..1c64338a0c 100644 --- a/sample-apps/react-native/dogfood/src/components/MeetingUI.tsx +++ b/sample-apps/react-native/dogfood/src/components/MeetingUI.tsx @@ -136,7 +136,6 @@ export const MeetingUI = ({ callId, navigation, route }: Props) => { onHangupCallHandler={onHangupCallHandler} onChatOpenHandler={onChatOpenHandler} unreadCountIndicator={unreadCountIndicator} - onBackPressed={onHangupCallHandler} /> ); From 7539bc64552a6d89c57cc451b399e375057477a6 Mon Sep 17 00:00:00 2001 From: Kristian Martinoski Date: Thu, 24 Oct 2024 14:10:28 +0200 Subject: [PATCH 21/36] fix: styling fixes for grid and spotlight layouts --- .../Call/CallContent/CallContent.tsx | 28 ++- .../Call/CallLayout/CallParticipantsGrid.tsx | 13 +- .../CallLayout/CallParticipantsSpotlight.tsx | 61 +++-- .../src/components/Call/CallLayout/index.ts | 218 ++++++++++++++++++ .../CallParticipantsList.tsx | 60 +++-- .../ParticipantView/ParticipantLabel.tsx | 13 +- .../ParticipantView/ParticipantView.tsx | 5 +- .../CallControlls/LayoutSwitcherModal.tsx | 13 +- 8 files changed, 334 insertions(+), 77 deletions(-) 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 2fc7faec02..91273fc604 100644 --- a/packages/react-native-sdk/src/components/Call/CallContent/CallContent.tsx +++ b/packages/react-native-sdk/src/components/Call/CallContent/CallContent.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useMemo, useState } from 'react'; import { StyleSheet, View } from 'react-native'; import InCallManager from 'react-native-incall-manager'; import { @@ -116,6 +116,7 @@ export const CallContent = ({ const { theme: { callContent }, } = useTheme(); + const styles = useStyles(); const { useCallSettings, useHasOngoingScreenShare, @@ -197,7 +198,7 @@ export const CallContent = ({ /> )} - + {!isInPiPMode && CallTopView && ( )} @@ -238,11 +239,18 @@ export const CallContent = ({ ); }; -const styles = StyleSheet.create({ - container: { flex: 1 }, - content: { flex: 1 }, - view: { - ...StyleSheet.absoluteFillObject, - zIndex: Z_INDEX.IN_FRONT, - }, -}); +const useStyles = () => { + const { theme } = useTheme(); + return useMemo( + () => + StyleSheet.create({ + container: { flex: 1 }, + content: { flex: 1 }, + view: { + ...StyleSheet.absoluteFillObject, + zIndex: Z_INDEX.IN_FRONT, + }, + }), + [theme] + ); +}; 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..2886d87454 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. @@ -71,11 +72,13 @@ export const CallParticipantsGrid = ({ remoteParticipants.length > 0 && remoteParticipants.length < 3; - let participants = showFloatingView - ? showLocalParticipant && localParticipant - ? [localParticipant] - : remoteParticipants - : allParticipants; + // let participants = showFloatingView + // ? showLocalParticipant && localParticipant + // ? [localParticipant] + // : remoteParticipants + // : allParticipants; + + let participants = generateMockParticipants(6); if (isInPiPMode) { participants = 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..7b18a4f7c5 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 }, } = 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 + // const allParticipants = useDebouncedValue(_allParticipants, 300); // we debounce the participants to avoid unnecessary rerenders that happen when participant tracks are all subscribed simultaneously + const allParticipants = generateMockParticipants(5); const [participantInSpotlight, ...otherParticipants] = allParticipants; const isScreenShareOnSpotlight = participantInSpotlight && hasScreenShare(participantInSpotlight); - const isUserAloneInCall = _allParticipants?.length === 1; + const isUserAloneInCall = allParticipants?.length === 1; const isInPiP = useIsInPiPMode(disablePictureInPicture); @@ -97,9 +100,6 @@ export const CallParticipantsSpotlight = ({ style={[ styles.container, landscapeStyles, - { - backgroundColor: colors.background2, - }, callParticipantsSpotlight.container, ]} > @@ -155,20 +155,33 @@ export const CallParticipantsSpotlight = ({ ); }; -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - fullScreenSpotlightContainer: { - flex: 1, - }, - spotlightContainer: { - flex: 2, - overflow: 'hidden', - borderRadius: 10, - marginHorizontal: 8, - }, - callParticipantsListContainer: { - flex: 1, - }, -}); +const useStyles = (landscape: boolean | undefined) => { + const { theme } = useTheme(); + return useMemo( + () => + StyleSheet.create({ + container: { + flex: 1, + display: 'flex', + 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, + // marginTop: !landscape ? theme.variants.spacingSizes.sm : 0, + }, + }), + [theme] + ); +}; 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..6b24290ba0 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,220 @@ export * from './CallParticipantsGrid'; export * from './CallParticipantsSpotlight'; + +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..d03a9a63f0 100644 --- a/packages/react-native-sdk/src/components/Call/CallParticipantsList/CallParticipantsList.tsx +++ b/packages/react-native-sdk/src/components/Call/CallParticipantsList/CallParticipantsList.tsx @@ -5,7 +5,7 @@ import React, { useRef, useState, } from 'react'; -import { FlatList, StyleProp, StyleSheet, ViewStyle } from 'react-native'; +import { FlatList, StyleProp, StyleSheet, View, ViewStyle } from 'react-native'; import { StreamVideoParticipant, @@ -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,9 @@ export const CallParticipantsList = ({ VideoRenderer, supportedReactions, landscape, + numberOfColumns = landscape ? 3 : 2, }: CallParticipantsListProps) => { + const styles = useStyles(); const [containerLayout, setContainerLayout] = useState({ width: 0, height: 0, @@ -166,7 +168,12 @@ export const CallParticipantsList = ({ }); const itemContainerStyle = useMemo>(() => { - const style = { width: itemWidth, height: itemHeight }; + const style = { + width: itemWidth, + height: itemHeight, + marginHorizontal: 4, + marginVertical: 4, + }; if (horizontal) { return [styles.participantWrapperHorizontal, style]; } @@ -208,9 +215,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 row on the screen + const shouldWrapByColumns = !!horizontal || participants.length > 3; if (!shouldWrapByColumns) { return ( @@ -251,17 +258,25 @@ 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: 4 }, + 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, + }, + landScapeStyle: { + marginVertical: theme.variants.spacingSizes.xs, + borderRadius: theme.variants.borderRadiusSizes.md, + }, + }), + [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 +307,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/Participant/ParticipantView/ParticipantLabel.tsx b/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantLabel.tsx index 5d4ee2ad81..a226b41b1b 100644 --- a/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantLabel.tsx +++ b/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantLabel.tsx @@ -97,15 +97,7 @@ export const ParticipantLabel = ({ ]} > - + {participantLabel} @@ -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..c9a955570e 100644 --- a/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantView.tsx +++ b/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantView.tsx @@ -169,9 +169,8 @@ const useStyles = () => { container: { justifyContent: 'space-between', overflow: 'hidden', - borderWidth: 2, - borderColor: 'transparent', - margin: theme.variants.spacingSizes.sm, + // borderWidth: 2, + // borderColor: 'transparent', borderRadius: 16, }, footerContainer: { diff --git a/sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherModal.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherModal.tsx index 3a9c55657b..d9c6b84bcd 100644 --- a/sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherModal.tsx +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherModal.tsx @@ -11,7 +11,7 @@ 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 { useLayout } from '../../contexts/LayoutContext'; +import { Layout, useLayout } from '../../contexts/LayoutContext'; interface AnchorPosition { x: number; @@ -61,6 +61,11 @@ const LayoutSwitcherModal: React.FC = ({ return null; } + const onPressHandler = (layout: Layout) => { + onLayoutSelection(layout); + onClose(); + }; + return ( = ({ styles.button, selectedLayout === 'grid' && styles.selectedButton, ]} - onPress={() => onLayoutSelection('grid')} + onPress={() => onPressHandler('grid')} > = ({ styles.button, selectedLayout === 'spotlight' && styles.selectedButton, ]} - onPress={() => onLayoutSelection('spotlight')} + onPress={() => onPressHandler('spotlight')} > = ({ styles.button, selectedLayout === 'fullscreen' && styles.selectedButton, ]} - onPress={() => onLayoutSelection('fullscreen')} + onPress={() => onPressHandler('fullscreen')} > Date: Fri, 25 Oct 2024 15:58:08 +0200 Subject: [PATCH 22/36] feat: add fullscreen layout --- .../Call/CallContent/CallContent.tsx | 31 ++++--- .../CallLayout/CallParticipantsFullscreen.tsx | 86 +++++++++++++++++++ .../Call/CallLayout/CallParticipantsGrid.tsx | 23 +---- .../CallLayout/CallParticipantsSpotlight.tsx | 9 +- .../CallParticipantsList.tsx | 2 +- .../ParticipantView/ParticipantView.tsx | 8 +- .../react-native-sdk/src/constants/TestIds.ts | 1 + .../react-native-sdk/src/constants/index.ts | 6 +- 8 files changed, 127 insertions(+), 39 deletions(-) create mode 100644 packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsFullscreen.tsx 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 91273fc604..9218685eb4 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; @@ -93,7 +97,6 @@ export type CallContentProps = Pick< export const CallContent = ({ onHangupCallHandler, CallParticipantsList, - CallTopView, CallControls = DefaultCallControls, FloatingParticipantView = DefaultFloatingParticipantView, ScreenShareOverlay = DefaultScreenShareOverlay, @@ -133,14 +136,12 @@ 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 hasScreenShare = useHasOngoingScreenShare(); + const isFullScreen = layout === 'fullscreen'; + const isSpotlight = layout === 'spotlight'; + + const showFloatingView = isFullScreen && remoteParticipants.length === 1; - const showFloatingView = - !showSpotlightLayout && - !isInPiPMode && - remoteParticipants.length > 0 && - remoteParticipants.length < 3; const isRemoteParticipantInFloatingView = showFloatingView && showRemoteParticipantInFloatingView && @@ -175,6 +176,13 @@ export const CallContent = ({ const callParticipantsGridProps: CallParticipantsGridProps = { ...participantViewProps, landscape, + ParticipantView, + CallParticipantsList, + supportedReactions, + }; + + const callParticipantsFullscreenProps: CallParticipantsFullscreenProps = { + ...participantViewProps, showLocalParticipant: isRemoteParticipantInFloatingView, ParticipantView, CallParticipantsList, @@ -199,9 +207,6 @@ export const CallContent = ({ )} - {!isInPiPMode && CallTopView && ( - - )} )} - {showSpotlightLayout ? ( + {isFullScreen ? ( + + ) : isSpotlight ? ( ) : ( diff --git a/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsFullscreen.tsx b/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsFullscreen.tsx new file mode 100644 index 0000000000..5c48eb97be --- /dev/null +++ b/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsFullscreen.tsx @@ -0,0 +1,86 @@ +import React from 'react'; +import { StyleSheet, View } from 'react-native'; +import { useCallStateHooks } from '@stream-io/video-react-bindings'; +import { useDebouncedValue } from '../../../utils/hooks/useDebouncedValue'; +import { + CallParticipantsList as DefaultCallParticipantsList, + CallParticipantsListComponentProps, +} from '../CallParticipantsList/CallParticipantsList'; +import { ComponentTestIds } from '../../../constants/TestIds'; +import { useTheme } from '../../../contexts/ThemeContext'; +import { CallContentProps } from '../CallContent'; +import { ParticipantViewComponentProps } from '../../Participant'; + +/** + * Props for the CallParticipantsFullscreen component. + */ +export type CallParticipantsFullscreenProps = ParticipantViewComponentProps & + Pick< + CallContentProps, + 'supportedReactions' | 'CallParticipantsList' | 'disablePictureInPicture' + > & + 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; + + 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 2886d87454..4d68560ccf 100644 --- a/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsGrid.tsx +++ b/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsGrid.tsx @@ -23,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. @@ -45,7 +41,6 @@ export const CallParticipantsGrid = ({ ParticipantVideoFallback, ParticipantView, VideoRenderer, - showLocalParticipant = false, supportedReactions, landscape, disablePictureInPicture, @@ -65,21 +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 = generateMockParticipants(6); + let participants = allParticipants; + // console.log('🚀 ~ participants:', participants); + // let participants = generateMockParticipants(4); + 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 7b18a4f7c5..f8259ed870 100644 --- a/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsSpotlight.tsx +++ b/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsSpotlight.tsx @@ -64,8 +64,8 @@ export const CallParticipantsSpotlight = ({ 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 - const allParticipants = generateMockParticipants(5); + const allParticipants = useDebouncedValue(_allParticipants, 300); // we debounce the participants to avoid unnecessary rerenders that happen when participant tracks are all subscribed simultaneously + // const allParticipants = generateMockParticipants(5); const [participantInSpotlight, ...otherParticipants] = allParticipants; const isScreenShareOnSpotlight = participantInSpotlight && hasScreenShare(participantInSpotlight); @@ -94,6 +94,9 @@ export const CallParticipantsSpotlight = ({ marginHorizontal: landscape ? 0 : 8, }; + // TODO: implement screen sharing + const showShareScreen = false; + return ( {participantInSpotlight && ParticipantView && - (participantInSpotlight.isLocalParticipant && ScreenShareOverlay ? ( + (showShareScreen && ScreenShareOverlay ? ( ) : ( )} - + {ParticipantLabel && ( )} 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; From af0c28c15e3965293eab048f7debe95cd3d248db Mon Sep 17 00:00:00 2001 From: Kristian Martinoski Date: Fri, 25 Oct 2024 22:10:44 +0200 Subject: [PATCH 23/36] fix: layout improvements --- .../Call/CallContent/CallContent.tsx | 58 +++++++++---------- .../CallLayout/CallParticipantsFullscreen.tsx | 4 ++ .../Call/CallLayout/CallParticipantsGrid.tsx | 2 +- .../CallLayout/CallParticipantsSpotlight.tsx | 20 +++---- .../src/components/Call/CallLayout/index.ts | 1 + .../CallParticipantsList.tsx | 34 ++++++----- .../ParticipantView/ParticipantLabel.tsx | 2 +- .../ParticipantView/ParticipantView.tsx | 7 +-- 8 files changed, 64 insertions(+), 64 deletions(-) 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 9218685eb4..498e2cef62 100644 --- a/packages/react-native-sdk/src/components/Call/CallContent/CallContent.tsx +++ b/packages/react-native-sdk/src/components/Call/CallContent/CallContent.tsx @@ -119,13 +119,8 @@ export const CallContent = ({ const { theme: { callContent }, } = useTheme(); - const styles = useStyles(); - const { - useCallSettings, - useHasOngoingScreenShare, - useRemoteParticipants, - useLocalParticipant, - } = useCallStateHooks(); + const { useCallSettings, useRemoteParticipants, useLocalParticipant } = + useCallStateHooks(); useAutoEnterPiPEffect(disablePictureInPicture); @@ -136,10 +131,7 @@ 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 isFullScreen = layout === 'fullscreen'; - const isSpotlight = layout === 'spotlight'; - const showFloatingView = isFullScreen && remoteParticipants.length === 1; const isRemoteParticipantInFloatingView = @@ -198,6 +190,21 @@ export const CallContent = ({ supportedReactions, }; + const renderCallParticipants = (layout: string) => { + switch (layout) { + case 'fullscreen': + return ( + + ); + case 'spotlight': + return ( + + ); + default: + return ; + } + }; + return ( <> {!disablePictureInPicture && ( @@ -226,13 +233,7 @@ export const CallContent = ({ /> )} - {isFullScreen ? ( - - ) : isSpotlight ? ( - - ) : ( - - )} + {renderCallParticipants(layout)} {!isInPiPMode && CallControls && ( @@ -246,18 +247,11 @@ export const CallContent = ({ ); }; -const useStyles = () => { - const { theme } = useTheme(); - return useMemo( - () => - StyleSheet.create({ - container: { flex: 1 }, - content: { flex: 1 }, - view: { - ...StyleSheet.absoluteFillObject, - zIndex: Z_INDEX.IN_FRONT, - }, - }), - [theme] - ); -}; +const styles = StyleSheet.create({ + container: { flex: 1 }, + content: { flex: 1 }, + view: { + ...StyleSheet.absoluteFillObject, + zIndex: Z_INDEX.IN_FRONT, + }, +}); diff --git a/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsFullscreen.tsx b/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsFullscreen.tsx index 5c48eb97be..44ec8c6fe3 100644 --- a/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsFullscreen.tsx +++ b/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsFullscreen.tsx @@ -52,6 +52,10 @@ export const CallParticipantsFullscreen = ({ ? [localParticipant] : remoteParticipants; + if (remoteParticipants.length === 0 && localParticipant) { + participants = [localParticipant]; + } + const participantViewProps: CallParticipantsListComponentProps = { ParticipantView, ParticipantLabel, 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 4d68560ccf..1205fd4d95 100644 --- a/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsGrid.tsx +++ b/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsGrid.tsx @@ -62,7 +62,7 @@ export const CallParticipantsGrid = ({ let participants = allParticipants; // console.log('🚀 ~ participants:', participants); - // let participants = generateMockParticipants(4); + // let participants = generateMockParticipants(9); const isInPiPMode = useIsInPiPMode(disablePictureInPicture); if (isInPiPMode) { 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 f8259ed870..66140c86b8 100644 --- a/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsSpotlight.tsx +++ b/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsSpotlight.tsx @@ -57,15 +57,15 @@ export const CallParticipantsSpotlight = ({ disablePictureInPicture, }: CallParticipantsSpotlightProps) => { const { - theme: { 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 - // const allParticipants = generateMockParticipants(5); + 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); @@ -91,12 +91,9 @@ export const CallParticipantsSpotlight = ({ }; const spotlightContainerLandscapeStyles: ViewStyle = { - marginHorizontal: landscape ? 0 : 8, + marginHorizontal: landscape ? 0 : variants.spacingSizes.xs, }; - // TODO: implement screen sharing - const showShareScreen = false; - return ( {participantInSpotlight && ParticipantView && - (showShareScreen && ScreenShareOverlay ? ( + (isScreenShareOnSpotlight && ScreenShareOverlay ? ( ) : ( { StyleSheet.create({ container: { flex: 1, - display: 'flex', + padding: theme.variants.spacingSizes.xs, backgroundColor: theme.colors.sheetPrimary, }, fullScreenSpotlightContainer: { @@ -175,16 +172,15 @@ const useStyles = (landscape: boolean | undefined) => { flex: landscape ? 3 : 4, overflow: 'hidden', borderRadius: theme.variants.borderRadiusSizes.md, - // marginHorizontal: theme.variants.spacingSizes.xs, + marginHorizontal: theme.variants.spacingSizes.xs, }, callParticipantsListContainer: { flex: 1, flexDirection: 'row', backgroundColor: theme.colors.sheetPrimary, marginLeft: landscape ? theme.variants.spacingSizes.sm : 0, - // marginTop: !landscape ? theme.variants.spacingSizes.sm : 0, }, }), - [theme] + [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 6b24290ba0..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,6 +1,7 @@ export * from './CallParticipantsGrid'; export * from './CallParticipantsSpotlight'; +// TODO: before merge remove this export const generateMockParticipants = (count: number) => { const mockParticipants = [ { 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 f10334889f..a26dd11a80 100644 --- a/packages/react-native-sdk/src/components/Call/CallParticipantsList/CallParticipantsList.tsx +++ b/packages/react-native-sdk/src/components/Call/CallParticipantsList/CallParticipantsList.tsx @@ -85,6 +85,7 @@ export const CallParticipantsList = ({ numberOfColumns = landscape ? 3 : 2, }: CallParticipantsListProps) => { const styles = useStyles(); + const { theme } = useTheme(); const [containerLayout, setContainerLayout] = useState({ width: 0, height: 0, @@ -171,14 +172,25 @@ export const CallParticipantsList = ({ const style = { width: itemWidth, height: itemHeight, - marginHorizontal: 4, - marginVertical: 4, + 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]); @@ -216,7 +228,7 @@ export const CallParticipantsList = ({ ); // 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 row on the screen + // else we display them in a stretched rows on the screen const shouldWrapByColumns = !!horizontal || participants.length > 3; if (!shouldWrapByColumns) { @@ -263,15 +275,9 @@ const useStyles = () => { return useMemo( () => StyleSheet.create({ - flexed: { flex: 1, margin: 4 }, - 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, - }, - landScapeStyle: { - marginVertical: theme.variants.spacingSizes.xs, - borderRadius: theme.variants.borderRadiusSizes.md, + flexed: { + flex: 1, + margin: theme.variants.spacingSizes.xs, }, }), [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 a226b41b1b..36be9835c3 100644 --- a/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantLabel.tsx +++ b/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantLabel.tsx @@ -101,7 +101,7 @@ export const ParticipantLabel = ({ {participantLabel} - + {isPinningEnabled && ( 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 869880c6e5..758dd7538b 100644 --- a/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantView.tsx +++ b/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantView.tsx @@ -153,7 +153,7 @@ export const ParticipantView = ({ style={[ styles.footerContainer, participantView.footerContainer, - !ParticipantLabel && { justifyContent: 'flex-end' }, + !ParticipantLabel && styles.networkIndicatorOnly, ]} > {ParticipantLabel && ( @@ -175,9 +175,7 @@ const useStyles = () => { container: { justifyContent: 'space-between', overflow: 'hidden', - // borderWidth: 2, - // borderColor: 'transparent', - borderRadius: 16, + borderRadius: theme.variants.borderRadiusSizes.md, }, footerContainer: { flexDirection: 'row', @@ -187,6 +185,7 @@ const useStyles = () => { highligtedContainer: { borderWidth: 2, }, + networkIndicatorOnly: { justifyContent: 'flex-end' }, }), [theme] ); From cdd2fddadb48d34596f33b66bd604c4797a32706 Mon Sep 17 00:00:00 2001 From: Kristian Martinoski Date: Fri, 25 Oct 2024 22:22:50 +0200 Subject: [PATCH 24/36] fix: top controls not sent to activeCall --- .../src/components/Call/CallContent/CallContent.tsx | 6 +++++- .../src/components/Call/CallLayout/CallParticipantsGrid.tsx | 2 +- .../Call/CallLayout/CallParticipantsSpotlight.tsx | 2 +- .../react-native/dogfood/src/components/ActiveCall.tsx | 2 +- 4 files changed, 8 insertions(+), 4 deletions(-) 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 498e2cef62..2da297257c 100644 --- a/packages/react-native-sdk/src/components/Call/CallContent/CallContent.tsx +++ b/packages/react-native-sdk/src/components/Call/CallContent/CallContent.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useMemo, useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { StyleSheet, View } from 'react-native'; import InCallManager from 'react-native-incall-manager'; import { @@ -97,6 +97,7 @@ export type CallContentProps = Pick< export const CallContent = ({ onHangupCallHandler, CallParticipantsList, + CallTopView, CallControls = DefaultCallControls, FloatingParticipantView = DefaultFloatingParticipantView, ScreenShareOverlay = DefaultScreenShareOverlay, @@ -214,6 +215,9 @@ export const CallContent = ({ )} + {!isInPiPMode && CallTopView && ( + + )} - Date: Fri, 25 Oct 2024 22:37:10 +0200 Subject: [PATCH 25/36] fix: lint issues --- .../Call/CallContent/CallContent.tsx | 4 ++-- .../CallParticipantsList.tsx | 4 ++-- .../CallControlls/BottomControls.tsx | 19 +++++++++---------- 3 files changed, 13 insertions(+), 14 deletions(-) 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 2da297257c..7aaadc3b34 100644 --- a/packages/react-native-sdk/src/components/Call/CallContent/CallContent.tsx +++ b/packages/react-native-sdk/src/components/Call/CallContent/CallContent.tsx @@ -191,8 +191,8 @@ export const CallContent = ({ supportedReactions, }; - const renderCallParticipants = (layout: string) => { - switch (layout) { + const renderCallParticipants = (selectedLayout: string) => { + switch (selectedLayout) { case 'fullscreen': return ( 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 a26dd11a80..f1a2304d83 100644 --- a/packages/react-native-sdk/src/components/Call/CallParticipantsList/CallParticipantsList.tsx +++ b/packages/react-native-sdk/src/components/Call/CallParticipantsList/CallParticipantsList.tsx @@ -184,7 +184,7 @@ export const CallParticipantsList = ({ }; return [participantWrapperHorizontal, style]; } - + if (landscape) { const landscapeStyle = { marginVertical: theme.variants.spacingSizes.xs, @@ -193,7 +193,7 @@ export const CallParticipantsList = ({ return [landscapeStyle, style]; } return style; - }, [itemWidth, itemHeight, horizontal, landscape]); + }, [itemWidth, itemHeight, horizontal, landscape, theme]); const participantProps: ParticipantViewComponentProps = { ParticipantLabel, 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 9adc2e2168..8079d2c233 100644 --- a/sample-apps/react-native/dogfood/src/components/CallControlls/BottomControls.tsx +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/BottomControls.tsx @@ -31,7 +31,7 @@ export const CallControlsComponent = ({ }: BottomControlsProps) => { const { useMicrophoneState } = useCallStateHooks(); const { isSpeakingWhileMuted } = useMicrophoneState(); - const styles = useStyles(); + const styles = useStyles(isSpeakingWhileMuted); return ( @@ -60,15 +60,20 @@ export const CallControlsComponent = ({ ); }; -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: { @@ -81,12 +86,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', @@ -100,6 +99,6 @@ const useStyles = () => { gap: theme.variants.spacingSizes.xs, }, }), - [theme], + [theme, showMicLabel], ); }; From 787e958d36c89e99fa5729b2441d95a84b469ed8 Mon Sep 17 00:00:00 2001 From: Kristian Martinoski Date: Mon, 28 Oct 2024 19:14:23 +0100 Subject: [PATCH 26/36] feat: add bottom controls drawer component with reactions flow --- .../ParticipantView/ParticipantReaction.tsx | 33 ++- .../react-native-sdk/src/constants/index.ts | 30 +- .../dogfood/src/assets/ClosedCaptions.tsx | 19 ++ .../dogfood/src/assets/Feedback.tsx | 19 ++ .../dogfood/src/assets/NoiseCancelation.tsx | 19 ++ .../react-native/dogfood/src/assets/Stats.tsx | 19 ++ .../CallControlls/MoreActionsButton.tsx | 74 ++++- .../dogfood/src/components/Drawer.tsx | 276 ++++++++++++++++++ .../dogfood/src/constants/index.ts | 47 +++ 9 files changed, 512 insertions(+), 24 deletions(-) create mode 100644 sample-apps/react-native/dogfood/src/assets/ClosedCaptions.tsx create mode 100644 sample-apps/react-native/dogfood/src/assets/Feedback.tsx create mode 100644 sample-apps/react-native/dogfood/src/assets/NoiseCancelation.tsx create mode 100644 sample-apps/react-native/dogfood/src/assets/Stats.tsx create mode 100644 sample-apps/react-native/dogfood/src/components/Drawer.tsx diff --git a/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantReaction.tsx b/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantReaction.tsx index 1d8e7c52d4..0e8233dcff 100644 --- a/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantReaction.tsx +++ b/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantReaction.tsx @@ -33,11 +33,7 @@ export const ParticipantReaction = ({ const { reaction, sessionId } = participant; const call = useCall(); const { - theme: { - typefaces, - variants: { iconSizes }, - participantReaction, - }, + theme: { typefaces, participantReaction }, } = useTheme(); useEffect(() => { @@ -52,6 +48,12 @@ export const ParticipantReaction = ({ }; }, [call, hideAfterTimeoutInMs, sessionId, reaction]); + // const currentReaction = { + // type: 'reaction', + // emoji_code: ':rolling_on_the_floor_laughing:', + // custom: {}, + // icon: '🤣', + // }; const currentReaction = reaction && supportedReactions.find( @@ -60,16 +62,7 @@ export const ParticipantReaction = ({ ); return ( - + {currentReaction?.icon} @@ -79,7 +72,15 @@ export const ParticipantReaction = ({ const styles = StyleSheet.create({ container: { - alignSelf: 'flex-start', + alignSelf: 'flex-end', + marginRight: 10, + marginTop: 10, + height: 44, + width: 44, + borderRadius: 8, + backgroundColor: 'gray', + alignItems: 'center', + justifyContent: 'center', zIndex: Z_INDEX.IN_FRONT, }, }); diff --git a/packages/react-native-sdk/src/constants/index.ts b/packages/react-native-sdk/src/constants/index.ts index 3e893573b6..3e8db57542 100644 --- a/packages/react-native-sdk/src/constants/index.ts +++ b/packages/react-native-sdk/src/constants/index.ts @@ -9,6 +9,12 @@ export const FLOATING_VIDEO_VIEW_STYLE = { export const LOBBY_VIDEO_VIEW_HEIGHT = 240; export const defaultEmojiReactions: StreamReactionType[] = [ + { + type: 'reaction', + emoji_code: ':rolling_on_the_floor_laughing:', + custom: {}, + icon: '🤣', + }, { type: 'reaction', emoji_code: ':like:', @@ -16,10 +22,16 @@ export const defaultEmojiReactions: StreamReactionType[] = [ icon: '👍', }, { - type: 'raised-hand', - emoji_code: ':raise-hand:', + type: 'reaction', + emoji_code: ':rocket:', custom: {}, - icon: '✋', + icon: '🚀', + }, + { + type: 'reaction', + emoji_code: ':dislike:', + custom: {}, + icon: '👎', }, { type: 'reaction', @@ -27,6 +39,18 @@ export const defaultEmojiReactions: StreamReactionType[] = [ custom: {}, icon: '🎉', }, + { + type: 'reaction', + emoji_code: ':raised-hands:', + custom: {}, + icon: '🙌', + }, + { + type: 'raised-hand', + emoji_code: ':raised-hand:', + custom: {}, + icon: '✋', + }, ]; export const Z_INDEX = { diff --git a/sample-apps/react-native/dogfood/src/assets/ClosedCaptions.tsx b/sample-apps/react-native/dogfood/src/assets/ClosedCaptions.tsx new file mode 100644 index 0000000000..1a95b04337 --- /dev/null +++ b/sample-apps/react-native/dogfood/src/assets/ClosedCaptions.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { Svg, Path } from 'react-native-svg'; +import { ColorValue } from 'react-native/types'; + +type Props = { + color: ColorValue; + size: number; +}; + +const ClosedCaptions = ({ color, size }: Props) => ( + + + +); + +export default ClosedCaptions; diff --git a/sample-apps/react-native/dogfood/src/assets/Feedback.tsx b/sample-apps/react-native/dogfood/src/assets/Feedback.tsx new file mode 100644 index 0000000000..b35bdefb71 --- /dev/null +++ b/sample-apps/react-native/dogfood/src/assets/Feedback.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { Svg, Path } from 'react-native-svg'; +import { ColorValue } from 'react-native/types'; + +type Props = { + color: ColorValue; + size: number; +}; + +const Feedback = ({ color, size }: Props) => ( + + + +); + +export default Feedback; diff --git a/sample-apps/react-native/dogfood/src/assets/NoiseCancelation.tsx b/sample-apps/react-native/dogfood/src/assets/NoiseCancelation.tsx new file mode 100644 index 0000000000..1af5777f00 --- /dev/null +++ b/sample-apps/react-native/dogfood/src/assets/NoiseCancelation.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { Svg, Path } from 'react-native-svg'; +import { ColorValue } from 'react-native/types'; + +type Props = { + color: ColorValue; + size: number; +}; + +const NoiseCancelation = ({ color, size }: Props) => ( + + + +); + +export default NoiseCancelation; diff --git a/sample-apps/react-native/dogfood/src/assets/Stats.tsx b/sample-apps/react-native/dogfood/src/assets/Stats.tsx new file mode 100644 index 0000000000..6c063c255b --- /dev/null +++ b/sample-apps/react-native/dogfood/src/assets/Stats.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { Svg, Path } from 'react-native-svg'; +import { ColorValue } from 'react-native/types'; + +type Props = { + color: ColorValue; + size: number; +}; + +const Stats = ({ color, size }: Props) => ( + + + +); + +export default Stats; diff --git a/sample-apps/react-native/dogfood/src/components/CallControlls/MoreActionsButton.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/MoreActionsButton.tsx index a884cadd37..71e0543001 100644 --- a/sample-apps/react-native/dogfood/src/components/CallControlls/MoreActionsButton.tsx +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/MoreActionsButton.tsx @@ -5,6 +5,11 @@ import { } from '@stream-io/video-react-native-sdk'; import { IconWrapper } from '@stream-io/video-react-native-sdk/src/icons'; import MoreActions from '../../assets/MoreActions'; +import { Drawer, DrawerOption } from '../Drawer'; +import NoiseCancelation from '../../assets/NoiseCancelation'; +import ClosedCaptions from '../../assets/ClosedCaptions'; +import Stats from '../../assets/Stats'; +import Feedback from '../../assets/Feedback'; /** * The props for the More Actions Button in the Call Controls. @@ -25,26 +30,85 @@ export const MoreActionsButton = ({ onPressHandler, }: MoreActionsButtonProps) => { const { - theme: { colors, moreActionsButton, defaults }, + theme: { colors, moreActionsButton, defaults, variants }, } = useTheme(); + const [isDrawerVisible, setIsDrawerVisible] = useState(false); + const options: DrawerOption[] = [ + { + id: '2', + label: 'Noise Cancellation On', + icon: ( + + + + ), + onPress: () => console.log('Noise Cancellation'), + }, + { + id: '3', + label: 'Start Closed Captions', + icon: ( + + + + ), + onPress: () => console.log('Closed Captions'), + }, + { + id: '4', + label: 'Stats', + icon: ( + + + + ), + onPress: () => console.log('Stats'), + }, + { + id: '5', + label: 'Feedback', + icon: ( + + + + ), + onPress: () => console.log('Feedback'), + }, + ]; - const [isPressed, setIsPressed] = useState(false); - const buttonColor = isPressed + const buttonColor = isDrawerVisible ? colors.buttonPrimaryDefault : colors.buttonSecondaryDefault; return ( { - // TODO: Implement PBE-5870 [Demo App] Component for "More" menu items if (onPressHandler) { onPressHandler(); } - setIsPressed(!isPressed); + setIsDrawerVisible(!isDrawerVisible); }} style={moreActionsButton} color={buttonColor} > + setIsDrawerVisible(false)} + options={options} + /> void; +}; + +type DrawerProps = { + isVisible: boolean; + onClose: () => void; + options: DrawerOption[]; +}; + +export const Drawer: React.FC = ({ + isVisible, + onClose, + options, +}) => { + const screenHeight = Dimensions.get('window').height; + const drawerHeight = screenHeight * 0.8; + const styles = useStyles(); + const offset = -70; + const call = useCall(); + + const translateY = useRef( + new Animated.Value(drawerHeight + offset), + ).current; + + const SNAP_TOP = offset; + const SNAP_BOTTOM = drawerHeight + offset; + const SNAP_MIDDLE = drawerHeight * 0.5; + + const getClosestSnapPoint = (y: number) => { + const points = [SNAP_TOP, SNAP_MIDDLE, SNAP_BOTTOM]; + return points.reduce((prev, curr) => + Math.abs(curr - y) < Math.abs(prev - y) ? curr : prev, + ); + }; + + const panResponder = useRef( + PanResponder.create({ + onStartShouldSetPanResponder: () => true, + onMoveShouldSetPanResponder: () => true, + onPanResponderGrant: () => { + translateY.setOffset(translateY._value); + translateY.setValue(0); + }, + onPanResponderMove: (_, gestureState) => { + translateY.setValue(gestureState.dy); + }, + onPanResponderRelease: () => { + translateY.flattenOffset(); + const currentPosition = translateY._value; + const snapPoint = getClosestSnapPoint(currentPosition); + + if (snapPoint === SNAP_BOTTOM) { + onClose(); + } else { + Animated.spring(translateY, { + toValue: snapPoint, + useNativeDriver: true, + bounciness: 4, + }).start(); + } + }, + }), + ).current; + + useEffect(() => { + if (isVisible) { + Animated.spring(translateY, { + toValue: SNAP_TOP, + useNativeDriver: true, + bounciness: 4, + }).start(); + } else { + Animated.timing(translateY, { + toValue: SNAP_BOTTOM, + duration: 300, + useNativeDriver: true, + }).start(); + } + }, [isVisible]); + + const dragIndicator = ( + + + + ); + + const elasticAnimRef = useRef(new Animated.Value(0.5)); + + const onCloseReaction = (reaction?: SendReactionRequest) => { + if (reaction) { + call?.sendReaction(reaction).catch((e) => { + const logger = getLogger(['ReactionsPicker']); + logger('error', 'Error on onClose-sendReaction', e, reaction); + }); + } + Animated.timing(elasticAnimRef.current, { + toValue: 0.2, + duration: 150, + useNativeDriver: true, + easing: Easing.linear, + }).start(onClose); + }; + + return ( + + + + + + {dragIndicator} + + {reactions.map((item) => ( + + { + onCloseReaction({ + type: item.type, + custom: item.custom, + emoji_code: item.emoji_code, + }); + }} + > + {item.icon} + + + ))} + + item.id} + renderItem={({ item }) => ( + + {item.icon && ( + {item.icon} + )} + {item.label} + + )} + /> + + + + + + ); +}; + +const useStyles = () => { + const { theme } = useTheme(); + return useMemo( + () => + StyleSheet.create({ + overlay: { + flex: 1, + justifyContent: 'flex-end', + }, + safeArea: { + flex: 1, + justifyContent: 'flex-end', + }, + container: { + backgroundColor: theme.colors.sheetPrimary, + borderTopLeftRadius: 20, + borderTopRightRadius: 20, + padding: 20, + maxHeight: '80%', + marginBottom: 0, + gap: 10, + }, + dragIndicator: { + width: '100%', + height: 24, + alignItems: 'center', + justifyContent: 'center', + marginBottom: 8, + }, + dragIndicatorBar: { + width: 40, + height: 4, + backgroundColor: '#FFFFFF40', + borderRadius: 2, + }, + option: { + flexDirection: 'row', + alignItems: 'center', + borderWidth: 1, + borderRadius: 32, + paddingHorizontal: theme.variants.spacingSizes.md, + height: 44, + backgroundColor: theme.colors.buttonSecondaryDefault, + }, + iconContainer: { + marginRight: 10, + }, + label: { + fontSize: 17, + color: theme.colors.iconPrimaryDefault, + fontWeight: '600', + }, + screen: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + openDrawerButton: { + padding: 10, + backgroundColor: '#007AFF', + borderRadius: 5, + }, + openDrawerText: { + color: '#FFF', + fontWeight: 'bold', + }, + emojiContainer: { + width: 42, + height: 42, + padding: theme.variants.spacingSizes.xs, + borderRadius: theme.variants.borderRadiusSizes.lg, + backgroundColor: theme.colors.buttonSecondaryDefault, + alignItems: 'center', + justifyContent: 'center', + }, + emojiRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + flexWrap: 'wrap', + }, + emojiText: { + fontSize: 25, + }, + }), + [theme], + ); +}; diff --git a/sample-apps/react-native/dogfood/src/constants/index.ts b/sample-apps/react-native/dogfood/src/constants/index.ts index 600c02c18a..0c69c9f159 100644 --- a/sample-apps/react-native/dogfood/src/constants/index.ts +++ b/sample-apps/react-native/dogfood/src/constants/index.ts @@ -1,3 +1,5 @@ +import { StreamReactionType } from '@stream-io/video-react-native-sdk'; + export const BUTTON_HEIGHT = 50; export const INPUT_HEIGHT = 50; export const AVATAR_SIZE = 50; @@ -7,3 +9,48 @@ export const Z_INDEX = { IN_MIDDLE: 1, IN_FRONT: 2, }; + +export const reactions: StreamReactionType[] = [ + { + type: 'reaction', + emoji_code: ':rolling_on_the_floor_laughing:', + custom: {}, + icon: '🤣', + }, + { + type: 'reaction', + emoji_code: ':like:', + custom: {}, + icon: '👍', + }, + { + type: 'reaction', + emoji_code: ':rocket:', + custom: {}, + icon: '🚀', + }, + { + type: 'reaction', + emoji_code: ':dislike:', + custom: {}, + icon: '👎', + }, + { + type: 'reaction', + emoji_code: ':fireworks:', + custom: {}, + icon: '🎉', + }, + { + type: 'reaction', + emoji_code: ':raised-hands:', + custom: {}, + icon: '🙌', + }, + { + type: 'raised-hand', + emoji_code: ':raised-hand:', + custom: {}, + icon: '✋', + }, +]; From 7eaa09214d59d4ad1e47d8f4b37af9d594cc5dc4 Mon Sep 17 00:00:00 2001 From: Kristian Martinoski Date: Tue, 29 Oct 2024 06:12:43 +0100 Subject: [PATCH 27/36] chore: revert mocks --- .../Call/CallLayout/CallParticipantsGrid.tsx | 4 - .../CallLayout/CallParticipantsSpotlight.tsx | 4 +- .../src/components/Call/CallLayout/index.ts | 219 ------------------ 3 files changed, 1 insertion(+), 226 deletions(-) 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 ea9ca6e627..a8f1b4c4e0 100644 --- a/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsGrid.tsx +++ b/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsGrid.tsx @@ -12,7 +12,6 @@ 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. @@ -61,9 +60,6 @@ export const CallParticipantsGrid = ({ }; let participants = allParticipants; - // console.log('🚀 ~ participants:', participants); - // let participants = generateMockParticipants(9); - const isInPiPMode = useIsInPiPMode(disablePictureInPicture); if (isInPiPMode) { participants = 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 9f13d2fb17..f746d10ec6 100644 --- a/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsSpotlight.tsx +++ b/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsSpotlight.tsx @@ -18,7 +18,6 @@ import { import { useTheme } from '../../../contexts/ThemeContext'; import { CallContentProps } from '../CallContent'; import { useIsInPiPMode } from '../../../hooks/useIsInPiPMode'; -// import { generateMockParticipants } from '.'; /** * Props for the CallParticipantsSpotlight component. @@ -65,11 +64,10 @@ export const CallParticipantsSpotlight = ({ sortBy: speakerLayoutSortPreset, }); 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); 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 abcb8fec49..7a8274996f 100644 --- a/packages/react-native-sdk/src/components/Call/CallLayout/index.ts +++ b/packages/react-native-sdk/src/components/Call/CallLayout/index.ts @@ -1,221 +1,2 @@ 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); -}; From 7cbf2a536fa62491656090df7ca6379655a3446c Mon Sep 17 00:00:00 2001 From: Kristian Martinoski Date: Tue, 29 Oct 2024 14:25:10 +0100 Subject: [PATCH 28/36] feat: final changes for the drawer component --- .../FloatingView/common.ts | 7 +- .../ParticipantView/ParticipantReaction.tsx | 56 ++--- .../react-native-sdk/src/constants/index.ts | 2 +- packages/react-native-sdk/src/theme/theme.ts | 2 + .../dogfood/src/assets/RaiseHand.tsx | 19 ++ .../{Drawer.tsx => BottomControlsDrawer.tsx} | 221 ++++++++++-------- .../CallControlls/MoreActionsButton.tsx | 21 +- .../dogfood/src/constants/index.ts | 2 + sample-apps/react-native/dogfood/src/theme.ts | 3 + 9 files changed, 200 insertions(+), 133 deletions(-) create mode 100644 sample-apps/react-native/dogfood/src/assets/RaiseHand.tsx rename sample-apps/react-native/dogfood/src/components/{Drawer.tsx => BottomControlsDrawer.tsx} (56%) diff --git a/packages/react-native-sdk/src/components/Participant/FloatingParticipantView/FloatingView/common.ts b/packages/react-native-sdk/src/components/Participant/FloatingParticipantView/FloatingView/common.ts index 0a5f8a4b4e..a572cc6c9c 100644 --- a/packages/react-native-sdk/src/components/Participant/FloatingParticipantView/FloatingView/common.ts +++ b/packages/react-native-sdk/src/components/Participant/FloatingParticipantView/FloatingView/common.ts @@ -1,4 +1,5 @@ import { StyleProp, ViewStyle } from 'react-native'; +import { useTheme } from '../../../..'; export enum FloatingViewAlignment { // Aligns the floating view to the top left corner. @@ -29,14 +30,16 @@ export function getSnapAlignments({ }): SnapAlignments { const right = rootContainerDimensions.width - floatingViewDimensions.width; const bottom = rootContainerDimensions.height - floatingViewDimensions.height; + const { theme } = useTheme(); + const top = theme.floatingParticipantsView.topPosition || 0; const snapOffsets = { [FloatingViewAlignment.topLeft]: { x: 0, - y: 0, + y: top, }, [FloatingViewAlignment.topRight]: { x: right, - y: 0, + y: top, }, [FloatingViewAlignment.bottomLeft]: { x: 0, diff --git a/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantReaction.tsx b/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantReaction.tsx index 0e8233dcff..cac5d2af64 100644 --- a/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantReaction.tsx +++ b/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantReaction.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { useCall } from '@stream-io/video-react-bindings'; import { Z_INDEX, defaultEmojiReactions } from '../../../constants'; @@ -32,6 +32,7 @@ export const ParticipantReaction = ({ }: ParticipantReactionProps) => { const { reaction, sessionId } = participant; const call = useCall(); + const styles = useStyles(); const { theme: { typefaces, participantReaction }, } = useTheme(); @@ -48,12 +49,6 @@ export const ParticipantReaction = ({ }; }, [call, hideAfterTimeoutInMs, sessionId, reaction]); - // const currentReaction = { - // type: 'reaction', - // emoji_code: ':rolling_on_the_floor_laughing:', - // custom: {}, - // icon: '🤣', - // }; const currentReaction = reaction && supportedReactions.find( @@ -62,25 +57,34 @@ export const ParticipantReaction = ({ ); return ( - - - {currentReaction?.icon} - - + currentReaction?.icon != null && ( + + + {currentReaction?.icon} + + + ) ); }; -const styles = StyleSheet.create({ - container: { - alignSelf: 'flex-end', - marginRight: 10, - marginTop: 10, - height: 44, - width: 44, - borderRadius: 8, - backgroundColor: 'gray', - alignItems: 'center', - justifyContent: 'center', - zIndex: Z_INDEX.IN_FRONT, - }, -}); +const useStyles = () => { + const { theme } = useTheme(); + return useMemo( + () => + StyleSheet.create({ + container: { + alignSelf: 'flex-end', + marginRight: theme.variants.spacingSizes.md, + marginTop: theme.variants.spacingSizes.md, + height: theme.variants.roundButtonSizes.md, + width: theme.variants.roundButtonSizes.md, + borderRadius: theme.variants.borderRadiusSizes.sm, + backgroundColor: theme.colors.sheetOverlay, + alignItems: 'center', + justifyContent: 'center', + zIndex: Z_INDEX.IN_FRONT, + }, + }), + [theme] + ); +}; diff --git a/packages/react-native-sdk/src/constants/index.ts b/packages/react-native-sdk/src/constants/index.ts index 3e8db57542..31beca2cd5 100644 --- a/packages/react-native-sdk/src/constants/index.ts +++ b/packages/react-native-sdk/src/constants/index.ts @@ -35,7 +35,7 @@ export const defaultEmojiReactions: StreamReactionType[] = [ }, { type: 'reaction', - emoji_code: ':fireworks:', + emoji_code: ':tada:', custom: {}, icon: '🎉', }, diff --git a/packages/react-native-sdk/src/theme/theme.ts b/packages/react-native-sdk/src/theme/theme.ts index 0e35318c9b..3560e77198 100644 --- a/packages/react-native-sdk/src/theme/theme.ts +++ b/packages/react-native-sdk/src/theme/theme.ts @@ -95,6 +95,7 @@ export type Theme = { container: ViewStyle; participantViewContainer: ViewStyle; videoFallback: ViewStyle; + topPosition: number; }; chatButton: { container: ViewStyle; @@ -479,6 +480,7 @@ export const defaultTheme: Theme = { container: {}, participantViewContainer: {}, videoFallback: {}, + topPosition: 0, }, participantLabel: { container: {}, diff --git a/sample-apps/react-native/dogfood/src/assets/RaiseHand.tsx b/sample-apps/react-native/dogfood/src/assets/RaiseHand.tsx new file mode 100644 index 0000000000..19db7b1dc2 --- /dev/null +++ b/sample-apps/react-native/dogfood/src/assets/RaiseHand.tsx @@ -0,0 +1,19 @@ +import React from 'react'; +import { Svg, Path } from 'react-native-svg'; +import { ColorValue } from 'react-native/types'; + +type Props = { + color: ColorValue; + size: number; +}; + +const RaiseHand = ({ color, size }: Props) => ( + + + +); + +export default RaiseHand; diff --git a/sample-apps/react-native/dogfood/src/components/Drawer.tsx b/sample-apps/react-native/dogfood/src/components/BottomControlsDrawer.tsx similarity index 56% rename from sample-apps/react-native/dogfood/src/components/Drawer.tsx rename to sample-apps/react-native/dogfood/src/components/BottomControlsDrawer.tsx index 4dc34d4175..3736e1c411 100644 --- a/sample-apps/react-native/dogfood/src/components/Drawer.tsx +++ b/sample-apps/react-native/dogfood/src/components/BottomControlsDrawer.tsx @@ -4,7 +4,7 @@ import { useTheme, getLogger, } from '@stream-io/video-react-native-sdk'; -import React, { useEffect, useMemo, useRef, useState } from 'react'; +import React, { useEffect, useMemo, useRef } from 'react'; import { Modal, View, @@ -19,8 +19,8 @@ import { PanResponder, Easing, } from 'react-native'; - -import { reactions } from '../constants'; +import { BOTTOM_CONTROLS_HEIGHT, reactions } from '../constants'; +import RaiseHand from '../assets/RaiseHand'; export type DrawerOption = { id: string; @@ -35,17 +35,20 @@ type DrawerProps = { options: DrawerOption[]; }; -export const Drawer: React.FC = ({ +export const BottomControlsDrawer: React.FC = ({ isVisible, onClose, options, }) => { + const { theme } = useTheme(); const screenHeight = Dimensions.get('window').height; const drawerHeight = screenHeight * 0.8; const styles = useStyles(); - const offset = -70; const call = useCall(); + // negative offset is needed so the drawer component start above the bottom controls + const offset = -theme.variants.insets.bottom - BOTTOM_CONTROLS_HEIGHT; + const translateY = useRef( new Animated.Value(drawerHeight + offset), ).current; @@ -106,12 +109,6 @@ export const Drawer: React.FC = ({ } }, [isVisible]); - const dragIndicator = ( - - - - ); - const elasticAnimRef = useRef(new Animated.Value(0.5)); const onCloseReaction = (reaction?: SendReactionRequest) => { @@ -129,13 +126,73 @@ export const Drawer: React.FC = ({ }).start(onClose); }; + const dragIndicator = ( + + + + ); + + const emojiReactions = ( + + {reactions.map((item) => ( + + { + onCloseReaction({ + type: item.type, + custom: item.custom, + emoji_code: item.emoji_code, + }); + }} + > + {item.icon} + + + ))} + + ); + + const raiseHand = ( + { + onCloseReaction({ + type: 'raised-hand', + emoji_code: ':raised-hand:', + custom: {}, + }); + }} + > + + + + {'Raise hand'} + + ); + + const otherButtons = ( + item.id} + renderItem={({ item }) => ( + + {item.icon && {item.icon}} + {item.label} + + )} + /> + ); + return ( @@ -145,39 +202,9 @@ export const Drawer: React.FC = ({ style={[styles.container, { transform: [{ translateY }] }]} > {dragIndicator} - - {reactions.map((item) => ( - - { - onCloseReaction({ - type: item.type, - custom: item.custom, - emoji_code: item.emoji_code, - }); - }} - > - {item.icon} - - - ))} - - item.id} - renderItem={({ item }) => ( - - {item.icon && ( - {item.icon} - )} - {item.label} - - )} - /> + {emojiReactions} + {raiseHand} + {otherButtons} @@ -187,7 +214,9 @@ export const Drawer: React.FC = ({ }; const useStyles = () => { - const { theme } = useTheme(); + const { + theme: { colors, variants }, + } = useTheme(); return useMemo( () => StyleSheet.create({ @@ -200,42 +229,75 @@ const useStyles = () => { justifyContent: 'flex-end', }, container: { - backgroundColor: theme.colors.sheetPrimary, - borderTopLeftRadius: 20, - borderTopRightRadius: 20, - padding: 20, + backgroundColor: colors.sheetPrimary, + borderTopLeftRadius: variants.borderRadiusSizes.lg, + borderTopRightRadius: variants.borderRadiusSizes.lg, + padding: variants.spacingSizes.md, maxHeight: '80%', - marginBottom: 0, - gap: 10, }, dragIndicator: { width: '100%', - height: 24, + height: variants.spacingSizes.xs, alignItems: 'center', justifyContent: 'center', - marginBottom: 8, + marginBottom: variants.spacingSizes.md, }, dragIndicatorBar: { - width: 40, - height: 4, - backgroundColor: '#FFFFFF40', + width: 36, + height: 5, + backgroundColor: colors.buttonSecondaryDefault, borderRadius: 2, }, + emojiContainer: { + width: variants.roundButtonSizes.lg, + height: variants.roundButtonSizes.lg, + padding: variants.spacingSizes.xs, + borderRadius: variants.borderRadiusSizes.lg, + backgroundColor: colors.buttonSecondaryDefault, + marginBottom: variants.spacingSizes.sm, + alignItems: 'center', + justifyContent: 'center', + }, + emojiRow: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + flexWrap: 'wrap', + }, + emojiText: { + fontSize: 25, + }, option: { flexDirection: 'row', alignItems: 'center', - borderWidth: 1, - borderRadius: 32, - paddingHorizontal: theme.variants.spacingSizes.md, - height: 44, - backgroundColor: theme.colors.buttonSecondaryDefault, + borderWidth: 2, + borderRadius: variants.borderRadiusSizes.lg, + paddingHorizontal: variants.spacingSizes.md, + height: variants.roundButtonSizes.lg, + backgroundColor: colors.buttonSecondaryDefault, + marginBottom: variants.spacingSizes.xs, + }, + raiseHand: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + borderWidth: 2, + borderRadius: variants.borderRadiusSizes.lg, + paddingHorizontal: variants.spacingSizes.md, + height: variants.roundButtonSizes.lg, + backgroundColor: colors.buttonSecondaryDefault, + marginBottom: variants.spacingSizes.sm, }, iconContainer: { - marginRight: 10, + marginRight: variants.spacingSizes.sm, + }, + handIconContainer: { + marginRight: variants.spacingSizes.sm, + marginTop: variants.spacingSizes.xs, }, label: { - fontSize: 17, - color: theme.colors.iconPrimaryDefault, + fontSize: variants.fontSizes.md, + color: colors.iconPrimaryDefault, fontWeight: '600', }, screen: { @@ -243,34 +305,7 @@ const useStyles = () => { justifyContent: 'center', alignItems: 'center', }, - openDrawerButton: { - padding: 10, - backgroundColor: '#007AFF', - borderRadius: 5, - }, - openDrawerText: { - color: '#FFF', - fontWeight: 'bold', - }, - emojiContainer: { - width: 42, - height: 42, - padding: theme.variants.spacingSizes.xs, - borderRadius: theme.variants.borderRadiusSizes.lg, - backgroundColor: theme.colors.buttonSecondaryDefault, - alignItems: 'center', - justifyContent: 'center', - }, - emojiRow: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - flexWrap: 'wrap', - }, - emojiText: { - fontSize: 25, - }, }), - [theme], + [variants, colors], ); }; diff --git a/sample-apps/react-native/dogfood/src/components/CallControlls/MoreActionsButton.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/MoreActionsButton.tsx index 71e0543001..113e9b31a4 100644 --- a/sample-apps/react-native/dogfood/src/components/CallControlls/MoreActionsButton.tsx +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/MoreActionsButton.tsx @@ -5,7 +5,7 @@ import { } from '@stream-io/video-react-native-sdk'; import { IconWrapper } from '@stream-io/video-react-native-sdk/src/icons'; import MoreActions from '../../assets/MoreActions'; -import { Drawer, DrawerOption } from '../Drawer'; +import { BottomControlsDrawer, DrawerOption } from '../BottomControlsDrawer'; import NoiseCancelation from '../../assets/NoiseCancelation'; import ClosedCaptions from '../../assets/ClosedCaptions'; import Stats from '../../assets/Stats'; @@ -35,7 +35,7 @@ export const MoreActionsButton = ({ const [isDrawerVisible, setIsDrawerVisible] = useState(false); const options: DrawerOption[] = [ { - id: '2', + id: '1', label: 'Noise Cancellation On', icon: ( @@ -45,10 +45,10 @@ export const MoreActionsButton = ({ /> ), - onPress: () => console.log('Noise Cancellation'), + onPress: () => {}, }, { - id: '3', + id: '2', label: 'Start Closed Captions', icon: ( @@ -58,10 +58,10 @@ export const MoreActionsButton = ({ /> ), - onPress: () => console.log('Closed Captions'), + onPress: () => {}, }, { - id: '4', + id: '3', label: 'Stats', icon: ( @@ -71,10 +71,10 @@ export const MoreActionsButton = ({ /> ), - onPress: () => console.log('Stats'), + onPress: () => {}, }, { - id: '5', + id: '4', label: 'Feedback', icon: ( @@ -84,7 +84,7 @@ export const MoreActionsButton = ({ /> ), - onPress: () => console.log('Feedback'), + onPress: () => {}, }, ]; @@ -103,8 +103,7 @@ export const MoreActionsButton = ({ style={moreActionsButton} color={buttonColor} > - setIsDrawerVisible(false)} options={options} diff --git a/sample-apps/react-native/dogfood/src/constants/index.ts b/sample-apps/react-native/dogfood/src/constants/index.ts index 0c69c9f159..a4f2d35f3b 100644 --- a/sample-apps/react-native/dogfood/src/constants/index.ts +++ b/sample-apps/react-native/dogfood/src/constants/index.ts @@ -4,6 +4,8 @@ export const BUTTON_HEIGHT = 50; export const INPUT_HEIGHT = 50; export const AVATAR_SIZE = 50; +export const BOTTOM_CONTROLS_HEIGHT = 45; + export const Z_INDEX = { IN_BACK: 0, IN_MIDDLE: 1, diff --git a/sample-apps/react-native/dogfood/src/theme.ts b/sample-apps/react-native/dogfood/src/theme.ts index e144d412f8..cecfe66dbf 100644 --- a/sample-apps/react-native/dogfood/src/theme.ts +++ b/sample-apps/react-native/dogfood/src/theme.ts @@ -45,5 +45,8 @@ export const useCustomTheme = (): DeepPartial => { left, }, }, + floatingParticipantsView: { + topPosition: 47, + }, } as DeepPartial; }; From 15f2f68cf77858ee8e38afcf9fc317e59a47e76d Mon Sep 17 00:00:00 2001 From: Kristian Martinoski Date: Tue, 29 Oct 2024 16:12:14 +0100 Subject: [PATCH 29/36] fix: lint issues --- .../FloatingView/AnimatedFloatingView.tsx | 5 ++++- .../FloatingView/ReanimatedFloatingView.tsx | 6 ++++-- .../FloatingParticipantView/FloatingView/common.ts | 9 ++++----- packages/react-native-sdk/src/icons/CameraSwitch.tsx | 2 +- packages/react-native-sdk/src/icons/Effects.tsx | 2 +- .../react-native/dogfood/src/assets/ClosedCaptions.tsx | 2 +- sample-apps/react-native/dogfood/src/assets/Feedback.tsx | 2 +- .../react-native/dogfood/src/assets/NoiseCancelation.tsx | 2 +- .../react-native/dogfood/src/assets/RaiseHand.tsx | 2 +- sample-apps/react-native/dogfood/src/assets/Stats.tsx | 2 +- .../dogfood/src/components/BottomControlsDrawer.tsx | 2 +- 11 files changed, 20 insertions(+), 16 deletions(-) diff --git a/packages/react-native-sdk/src/components/Participant/FloatingParticipantView/FloatingView/AnimatedFloatingView.tsx b/packages/react-native-sdk/src/components/Participant/FloatingParticipantView/FloatingView/AnimatedFloatingView.tsx index 1f46fe288d..99088df5a5 100644 --- a/packages/react-native-sdk/src/components/Participant/FloatingParticipantView/FloatingView/AnimatedFloatingView.tsx +++ b/packages/react-native-sdk/src/components/Participant/FloatingParticipantView/FloatingView/AnimatedFloatingView.tsx @@ -15,6 +15,7 @@ import { floatingChildViewContainerStyle, } from './common'; import { getLogger } from '@stream-io/video-client'; +import { useTheme } from '../../../..'; const AnimatedFloatingView = ({ initialAlignment, @@ -29,6 +30,7 @@ const AnimatedFloatingView = ({ const translateRef = useRef(new Animated.ValueXY()); const opacity = useRef(new Animated.Value(0)); + const { theme } = useTheme(); const [rectangle, setRectangle] = React.useState(); // we need to force update the component when the rectangle is available @@ -58,6 +60,7 @@ const AnimatedFloatingView = ({ width: rectangle.width, height: rectangle.height, }, + topOffset: theme.floatingParticipantsView.topPosition, }); const { x, y } = snapAlignments[initialAlignment]; snapAlignmentsRef.current = snapAlignments; @@ -66,7 +69,7 @@ const AnimatedFloatingView = ({ opacity.current.setValue(1); forceUpdate(); // any time the dependency changes, we need to snap to the new alignment - }, [initialAlignment, rectangle, containerWidth, containerHeight]); + }, [initialAlignment, rectangle, containerWidth, containerHeight, theme]); const panResponder = useRef( PanResponder.create({ diff --git a/packages/react-native-sdk/src/components/Participant/FloatingParticipantView/FloatingView/ReanimatedFloatingView.tsx b/packages/react-native-sdk/src/components/Participant/FloatingParticipantView/FloatingView/ReanimatedFloatingView.tsx index f0111d9f0a..432366165c 100644 --- a/packages/react-native-sdk/src/components/Participant/FloatingParticipantView/FloatingView/ReanimatedFloatingView.tsx +++ b/packages/react-native-sdk/src/components/Participant/FloatingParticipantView/FloatingView/ReanimatedFloatingView.tsx @@ -7,6 +7,7 @@ import { floatingChildViewContainerStyle, FloatingViewProps, } from './common'; +import { useTheme } from '../../../..'; type GestureHandlerExportsType = typeof import('react-native-gesture-handler'); type ReanimatedNamespaceType = typeof import('react-native-reanimated').default; type ReanimatedExportsType = typeof import('react-native-reanimated'); @@ -54,6 +55,7 @@ try { // we don't want to show the floating view until we have the layout rectangle const opacity = useSharedValue(0); const [rectangle, setRectangle] = React.useState(); + const { theme } = useTheme(); const snapAlignments = useMemo(() => { if (!rectangle) { @@ -64,7 +66,6 @@ try { [FloatingViewAlignment.bottomRight]: { x: 0, y: 0 }, }; } - return getSnapAlignments({ rootContainerDimensions: { width: containerWidth, @@ -74,8 +75,9 @@ try { width: rectangle.width, height: rectangle.height, }, + topOffset: theme.floatingParticipantsView.topPosition, }); - }, [rectangle, containerWidth, containerHeight]); + }, [rectangle, containerWidth, containerHeight, theme]); const dragGesture = Gesture.Pan() .onStart((_e) => { diff --git a/packages/react-native-sdk/src/components/Participant/FloatingParticipantView/FloatingView/common.ts b/packages/react-native-sdk/src/components/Participant/FloatingParticipantView/FloatingView/common.ts index a572cc6c9c..769f2cbada 100644 --- a/packages/react-native-sdk/src/components/Participant/FloatingParticipantView/FloatingView/common.ts +++ b/packages/react-native-sdk/src/components/Participant/FloatingParticipantView/FloatingView/common.ts @@ -1,5 +1,4 @@ import { StyleProp, ViewStyle } from 'react-native'; -import { useTheme } from '../../../..'; export enum FloatingViewAlignment { // Aligns the floating view to the top left corner. @@ -24,22 +23,22 @@ export type SnapAlignments = Record< export function getSnapAlignments({ rootContainerDimensions, floatingViewDimensions, + topOffset, }: { rootContainerDimensions: { width: number; height: number }; floatingViewDimensions: { width: number; height: number }; + topOffset: number; }): SnapAlignments { const right = rootContainerDimensions.width - floatingViewDimensions.width; const bottom = rootContainerDimensions.height - floatingViewDimensions.height; - const { theme } = useTheme(); - const top = theme.floatingParticipantsView.topPosition || 0; const snapOffsets = { [FloatingViewAlignment.topLeft]: { x: 0, - y: top, + y: topOffset, }, [FloatingViewAlignment.topRight]: { x: right, - y: top, + y: topOffset, }, [FloatingViewAlignment.bottomLeft]: { x: 0, diff --git a/packages/react-native-sdk/src/icons/CameraSwitch.tsx b/packages/react-native-sdk/src/icons/CameraSwitch.tsx index 62bb662cd2..b99fc61b8e 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) => ( - + ( - + ( - + ( - + ( - + ( - + ( - + = ({ useNativeDriver: true, }).start(); } - }, [isVisible]); + }, [isVisible, SNAP_BOTTOM, SNAP_TOP, translateY]); const elasticAnimRef = useRef(new Animated.Value(0.5)); From 3e3dd8a2991a1e3496064b0bc1bac94b9c51fc36 Mon Sep 17 00:00:00 2001 From: Kristian Martinoski Date: Tue, 29 Oct 2024 17:32:13 +0100 Subject: [PATCH 30/36] test: fix failing test case for tada reaction emoji --- .../__tests__/components/ParticipantView.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react-native-sdk/__tests__/components/ParticipantView.test.tsx b/packages/react-native-sdk/__tests__/components/ParticipantView.test.tsx index c604478f83..1b79d43793 100644 --- a/packages/react-native-sdk/__tests__/components/ParticipantView.test.tsx +++ b/packages/react-native-sdk/__tests__/components/ParticipantView.test.tsx @@ -23,7 +23,7 @@ describe('ParticipantView', () => { publishedTracks: [], reaction: { type: 'reaction', - emoji_code: ':fireworks:', + emoji_code: ':tada:', }, }); render( From 796e5ffb4bbf976f3d3708ca409628895d3e84f9 Mon Sep 17 00:00:00 2001 From: Kristian Martinoski Date: Wed, 30 Oct 2024 07:04:59 +0100 Subject: [PATCH 31/36] fix: android specific styling issues --- .../dogfood/src/components/BottomControlsDrawer.tsx | 9 ++++----- .../src/components/CallControlls/BottomControls.tsx | 4 ++-- sample-apps/react-native/dogfood/src/constants/index.ts | 3 +-- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/sample-apps/react-native/dogfood/src/components/BottomControlsDrawer.tsx b/sample-apps/react-native/dogfood/src/components/BottomControlsDrawer.tsx index a9b4fb073d..6e141d39d4 100644 --- a/sample-apps/react-native/dogfood/src/components/BottomControlsDrawer.tsx +++ b/sample-apps/react-native/dogfood/src/components/BottomControlsDrawer.tsx @@ -47,7 +47,7 @@ export const BottomControlsDrawer: React.FC = ({ const call = useCall(); // negative offset is needed so the drawer component start above the bottom controls - const offset = -theme.variants.insets.bottom - BOTTOM_CONTROLS_HEIGHT; + const offset = -BOTTOM_CONTROLS_HEIGHT; const translateY = useRef( new Animated.Value(drawerHeight + offset), @@ -136,8 +136,7 @@ export const BottomControlsDrawer: React.FC = ({ {reactions.map((item) => ( - { onCloseReaction({ type: item.type, @@ -146,8 +145,8 @@ export const BottomControlsDrawer: React.FC = ({ }); }} > - {item.icon} - + {item.icon} + ))} 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 8079d2c233..cb748b902c 100644 --- a/sample-apps/react-native/dogfood/src/components/CallControlls/BottomControls.tsx +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/BottomControls.tsx @@ -8,7 +8,7 @@ import { import React, { useMemo } from 'react'; import { StyleSheet, Text, View } from 'react-native'; import { appTheme } from '../../theme'; -import { Z_INDEX } from '../../constants'; +import { BOTTOM_CONTROLS_HEIGHT, Z_INDEX } from '../../constants'; import { MoreActionsButton } from './MoreActionsButton'; import { ParticipantsButton } from './ParticipantsButton'; import { ChatButton } from './ChatButton'; @@ -70,7 +70,7 @@ const useStyles = (showMicLabel: boolean) => { paddingVertical: !showMicLabel ? theme.variants.spacingSizes.md : 0, paddingHorizontal: theme.variants.spacingSizes.md, backgroundColor: theme.colors.sheetPrimary, - height: 76, + height: BOTTOM_CONTROLS_HEIGHT, }, speakingLabelContainer: { backgroundColor: appTheme.colors.static_overlay, diff --git a/sample-apps/react-native/dogfood/src/constants/index.ts b/sample-apps/react-native/dogfood/src/constants/index.ts index a4f2d35f3b..6f242c2e06 100644 --- a/sample-apps/react-native/dogfood/src/constants/index.ts +++ b/sample-apps/react-native/dogfood/src/constants/index.ts @@ -3,8 +3,7 @@ import { StreamReactionType } from '@stream-io/video-react-native-sdk'; export const BUTTON_HEIGHT = 50; export const INPUT_HEIGHT = 50; export const AVATAR_SIZE = 50; - -export const BOTTOM_CONTROLS_HEIGHT = 45; +export const BOTTOM_CONTROLS_HEIGHT = 76; export const Z_INDEX = { IN_BACK: 0, From 36dd3e3fff836f98bb7aa2306410698ed8a96708 Mon Sep 17 00:00:00 2001 From: Kristian Martinoski Date: Thu, 31 Oct 2024 15:09:11 +0100 Subject: [PATCH 32/36] chore: update docs --- .../docs/reactnative/04-ui-components/call/call-top-view.mdx | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/react-native-sdk/docusaurus/docs/reactnative/04-ui-components/call/call-top-view.mdx b/packages/react-native-sdk/docusaurus/docs/reactnative/04-ui-components/call/call-top-view.mdx index 27f7aad3ce..df2a24a996 100644 --- a/packages/react-native-sdk/docusaurus/docs/reactnative/04-ui-components/call/call-top-view.mdx +++ b/packages/react-native-sdk/docusaurus/docs/reactnative/04-ui-components/call/call-top-view.mdx @@ -3,7 +3,6 @@ id: call-top-view title: CallTopView --- -import ParticipantsInfoBadge from '../../common-content/ui-components/call/call-content/participants-info-badge.mdx'; import OnBackPressed from '../../common-content/ui-components/call/call-content/on-back-pressed.mdx'; import OnParticipantInfoPress from '../../common-content/ui-components/call/call-content/on-participant-info-press.mdx'; From f5458ae55683153d37e6c9391f783a5ea5012e73 Mon Sep 17 00:00:00 2001 From: Kristian Martinoski Date: Thu, 31 Oct 2024 15:28:32 +0100 Subject: [PATCH 33/36] fix: revert some sdk changes --- .../components/ParticipantView.test.tsx | 2 +- .../react-native-sdk/src/constants/index.ts | 32 +++---------------- 2 files changed, 5 insertions(+), 29 deletions(-) diff --git a/packages/react-native-sdk/__tests__/components/ParticipantView.test.tsx b/packages/react-native-sdk/__tests__/components/ParticipantView.test.tsx index 1b79d43793..c604478f83 100644 --- a/packages/react-native-sdk/__tests__/components/ParticipantView.test.tsx +++ b/packages/react-native-sdk/__tests__/components/ParticipantView.test.tsx @@ -23,7 +23,7 @@ describe('ParticipantView', () => { publishedTracks: [], reaction: { type: 'reaction', - emoji_code: ':tada:', + emoji_code: ':fireworks:', }, }); render( diff --git a/packages/react-native-sdk/src/constants/index.ts b/packages/react-native-sdk/src/constants/index.ts index 31beca2cd5..3e893573b6 100644 --- a/packages/react-native-sdk/src/constants/index.ts +++ b/packages/react-native-sdk/src/constants/index.ts @@ -9,12 +9,6 @@ export const FLOATING_VIDEO_VIEW_STYLE = { export const LOBBY_VIDEO_VIEW_HEIGHT = 240; export const defaultEmojiReactions: StreamReactionType[] = [ - { - type: 'reaction', - emoji_code: ':rolling_on_the_floor_laughing:', - custom: {}, - icon: '🤣', - }, { type: 'reaction', emoji_code: ':like:', @@ -22,35 +16,17 @@ export const defaultEmojiReactions: StreamReactionType[] = [ icon: '👍', }, { - type: 'reaction', - emoji_code: ':rocket:', - custom: {}, - icon: '🚀', - }, - { - type: 'reaction', - emoji_code: ':dislike:', + type: 'raised-hand', + emoji_code: ':raise-hand:', custom: {}, - icon: '👎', + icon: '✋', }, { type: 'reaction', - emoji_code: ':tada:', + emoji_code: ':fireworks:', custom: {}, icon: '🎉', }, - { - type: 'reaction', - emoji_code: ':raised-hands:', - custom: {}, - icon: '🙌', - }, - { - type: 'raised-hand', - emoji_code: ':raised-hand:', - custom: {}, - icon: '✋', - }, ]; export const Z_INDEX = { From 23598b089093a8409133f8cb49d3eeef8d43d5ee Mon Sep 17 00:00:00 2001 From: Kristian Martinoski Date: Thu, 31 Oct 2024 16:36:37 +0100 Subject: [PATCH 34/36] fix: use sdk emojis in dogfood --- .../react-native-sdk/src/constants/index.ts | 30 ++++++++++-- .../src/components/BottomControlsDrawer.tsx | 6 ++- .../dogfood/src/constants/index.ts | 47 ------------------- 3 files changed, 31 insertions(+), 52 deletions(-) diff --git a/packages/react-native-sdk/src/constants/index.ts b/packages/react-native-sdk/src/constants/index.ts index 3e893573b6..3e8db57542 100644 --- a/packages/react-native-sdk/src/constants/index.ts +++ b/packages/react-native-sdk/src/constants/index.ts @@ -9,6 +9,12 @@ export const FLOATING_VIDEO_VIEW_STYLE = { export const LOBBY_VIDEO_VIEW_HEIGHT = 240; export const defaultEmojiReactions: StreamReactionType[] = [ + { + type: 'reaction', + emoji_code: ':rolling_on_the_floor_laughing:', + custom: {}, + icon: '🤣', + }, { type: 'reaction', emoji_code: ':like:', @@ -16,10 +22,16 @@ export const defaultEmojiReactions: StreamReactionType[] = [ icon: '👍', }, { - type: 'raised-hand', - emoji_code: ':raise-hand:', + type: 'reaction', + emoji_code: ':rocket:', custom: {}, - icon: '✋', + icon: '🚀', + }, + { + type: 'reaction', + emoji_code: ':dislike:', + custom: {}, + icon: '👎', }, { type: 'reaction', @@ -27,6 +39,18 @@ export const defaultEmojiReactions: StreamReactionType[] = [ custom: {}, icon: '🎉', }, + { + type: 'reaction', + emoji_code: ':raised-hands:', + custom: {}, + icon: '🙌', + }, + { + type: 'raised-hand', + emoji_code: ':raised-hand:', + custom: {}, + icon: '✋', + }, ]; export const Z_INDEX = { diff --git a/sample-apps/react-native/dogfood/src/components/BottomControlsDrawer.tsx b/sample-apps/react-native/dogfood/src/components/BottomControlsDrawer.tsx index 6e141d39d4..07f4e5dd18 100644 --- a/sample-apps/react-native/dogfood/src/components/BottomControlsDrawer.tsx +++ b/sample-apps/react-native/dogfood/src/components/BottomControlsDrawer.tsx @@ -4,6 +4,8 @@ import { useTheme, getLogger, } from '@stream-io/video-react-native-sdk'; +import { defaultEmojiReactions } from '@stream-io/video-react-native-sdk/src/constants'; + import React, { useEffect, useMemo, useRef } from 'react'; import { Modal, @@ -19,7 +21,7 @@ import { PanResponder, Easing, } from 'react-native'; -import { BOTTOM_CONTROLS_HEIGHT, reactions } from '../constants'; +import { BOTTOM_CONTROLS_HEIGHT } from '../constants'; import RaiseHand from '../assets/RaiseHand'; export type DrawerOption = { @@ -134,7 +136,7 @@ export const BottomControlsDrawer: React.FC = ({ const emojiReactions = ( - {reactions.map((item) => ( + {defaultEmojiReactions.map((item) => ( { diff --git a/sample-apps/react-native/dogfood/src/constants/index.ts b/sample-apps/react-native/dogfood/src/constants/index.ts index 6f242c2e06..27355178fd 100644 --- a/sample-apps/react-native/dogfood/src/constants/index.ts +++ b/sample-apps/react-native/dogfood/src/constants/index.ts @@ -1,5 +1,3 @@ -import { StreamReactionType } from '@stream-io/video-react-native-sdk'; - export const BUTTON_HEIGHT = 50; export const INPUT_HEIGHT = 50; export const AVATAR_SIZE = 50; @@ -10,48 +8,3 @@ export const Z_INDEX = { IN_MIDDLE: 1, IN_FRONT: 2, }; - -export const reactions: StreamReactionType[] = [ - { - type: 'reaction', - emoji_code: ':rolling_on_the_floor_laughing:', - custom: {}, - icon: '🤣', - }, - { - type: 'reaction', - emoji_code: ':like:', - custom: {}, - icon: '👍', - }, - { - type: 'reaction', - emoji_code: ':rocket:', - custom: {}, - icon: '🚀', - }, - { - type: 'reaction', - emoji_code: ':dislike:', - custom: {}, - icon: '👎', - }, - { - type: 'reaction', - emoji_code: ':fireworks:', - custom: {}, - icon: '🎉', - }, - { - type: 'reaction', - emoji_code: ':raised-hands:', - custom: {}, - icon: '🙌', - }, - { - type: 'raised-hand', - emoji_code: ':raised-hand:', - custom: {}, - icon: '✋', - }, -]; From 28ae8797d3c312c0842ef9554a6c8d6037e48998 Mon Sep 17 00:00:00 2001 From: Kristian Martinoski Date: Fri, 1 Nov 2024 15:53:55 +0100 Subject: [PATCH 35/36] fix: revert changes --- .../Call/CallContent/CallContent.tsx | 53 +++++------ .../CallLayout/CallParticipantsFullscreen.tsx | 90 ------------------- .../Call/CallLayout/CallParticipantsGrid.tsx | 18 +++- .../CallLayout/CallParticipantsSpotlight.tsx | 4 +- .../CallParticipantsList.tsx | 70 +++++---------- .../ParticipantView/ParticipantLabel.tsx | 2 +- .../react-native-sdk/src/constants/TestIds.ts | 1 - .../react-native-sdk/src/constants/index.ts | 6 +- .../react-native-sdk/src/icons/Spotlight.tsx | 18 ++++ packages/react-native-sdk/src/icons/index.tsx | 1 + .../dogfood/src/assets/FullScreen.tsx | 17 ---- 11 files changed, 86 insertions(+), 194 deletions(-) delete mode 100644 packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsFullscreen.tsx create mode 100644 packages/react-native-sdk/src/icons/Spotlight.tsx delete mode 100644 sample-apps/react-native/dogfood/src/assets/FullScreen.tsx 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 7aaadc3b34..b6126f5071 100644 --- a/packages/react-native-sdk/src/components/Call/CallContent/CallContent.tsx +++ b/packages/react-native-sdk/src/components/Call/CallContent/CallContent.tsx @@ -33,10 +33,6 @@ import { ScreenShareOverlayProps, } from '../../utility/ScreenShareOverlay'; import RTCViewPipIOS from './RTCViewPipIOS'; -import { - CallParticipantsFullscreen, - CallParticipantsFullscreenProps, -} from '../CallLayout/CallParticipantsFullscreen'; export type StreamReactionType = StreamReaction & { icon: string; @@ -74,7 +70,7 @@ export type CallContentProps = Pick< /** * This switches the participant's layout between the grid, spotlight and fullscreen mode. */ - layout?: 'grid' | 'spotlight' | 'fullscreen'; + layout?: 'grid' | 'spotlight'; /** * Reactions that are to be supported in the call */ @@ -120,8 +116,12 @@ export const CallContent = ({ const { theme: { callContent }, } = useTheme(); - const { useCallSettings, useRemoteParticipants, useLocalParticipant } = - useCallStateHooks(); + const { + useCallSettings, + useHasOngoingScreenShare, + useRemoteParticipants, + useLocalParticipant, + } = useCallStateHooks(); useAutoEnterPiPEffect(disablePictureInPicture); @@ -132,9 +132,14 @@ 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 isFullScreen = layout === 'fullscreen'; - const showFloatingView = isFullScreen && remoteParticipants.length === 1; - + const hasScreenShare = useHasOngoingScreenShare(); + const showSpotlightLayout = hasScreenShare || layout === 'spotlight'; + + const showFloatingView = + !showSpotlightLayout && + !isInPiPMode && + remoteParticipants.length > 0 && + remoteParticipants.length < 3; const isRemoteParticipantInFloatingView = showFloatingView && showRemoteParticipantInFloatingView && @@ -169,13 +174,6 @@ export const CallContent = ({ const callParticipantsGridProps: CallParticipantsGridProps = { ...participantViewProps, landscape, - ParticipantView, - CallParticipantsList, - supportedReactions, - }; - - const callParticipantsFullscreenProps: CallParticipantsFullscreenProps = { - ...participantViewProps, showLocalParticipant: isRemoteParticipantInFloatingView, ParticipantView, CallParticipantsList, @@ -191,21 +189,6 @@ export const CallContent = ({ supportedReactions, }; - const renderCallParticipants = (selectedLayout: string) => { - switch (selectedLayout) { - case 'fullscreen': - return ( - - ); - case 'spotlight': - return ( - - ); - default: - return ; - } - }; - return ( <> {!disablePictureInPicture && ( @@ -237,7 +220,11 @@ export const CallContent = ({ /> )} - {renderCallParticipants(layout)} + {showSpotlightLayout ? ( + + ) : ( + + )}{' '} {!isInPiPMode && CallControls && ( diff --git a/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsFullscreen.tsx b/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsFullscreen.tsx deleted file mode 100644 index 44ec8c6fe3..0000000000 --- a/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsFullscreen.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React from 'react'; -import { StyleSheet, View } from 'react-native'; -import { useCallStateHooks } from '@stream-io/video-react-bindings'; -import { useDebouncedValue } from '../../../utils/hooks/useDebouncedValue'; -import { - CallParticipantsList as DefaultCallParticipantsList, - CallParticipantsListComponentProps, -} from '../CallParticipantsList/CallParticipantsList'; -import { ComponentTestIds } from '../../../constants/TestIds'; -import { useTheme } from '../../../contexts/ThemeContext'; -import { CallContentProps } from '../CallContent'; -import { ParticipantViewComponentProps } from '../../Participant'; - -/** - * Props for the CallParticipantsFullscreen component. - */ -export type CallParticipantsFullscreenProps = ParticipantViewComponentProps & - Pick< - CallContentProps, - 'supportedReactions' | 'CallParticipantsList' | 'disablePictureInPicture' - > & - 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 a8f1b4c4e0..d71bb8c128 100644 --- a/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsGrid.tsx +++ b/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsGrid.tsx @@ -22,6 +22,10 @@ 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. @@ -40,6 +44,7 @@ export const CallParticipantsGrid = ({ ParticipantVideoFallback, ParticipantView, VideoRenderer, + showLocalParticipant = false, supportedReactions, landscape, disablePictureInPicture, @@ -59,8 +64,19 @@ export const CallParticipantsGrid = ({ flexDirection: landscape ? 'row' : 'column', }; - let participants = allParticipants; const isInPiPMode = useIsInPiPMode(disablePictureInPicture); + + const showFloatingView = + !isInPiPMode && + remoteParticipants.length > 0 && + remoteParticipants.length < 3; + + let participants = showFloatingView + ? showLocalParticipant && localParticipant + ? [localParticipant] + : remoteParticipants + : allParticipants; + 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 c169045c8f..5f42652183 100644 --- a/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsSpotlight.tsx +++ b/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsSpotlight.tsx @@ -63,7 +63,7 @@ export const CallParticipantsSpotlight = ({ const _allParticipants = useParticipants({ sortBy: speakerLayoutSortPreset, }); - let allParticipants = useDebouncedValue(_allParticipants, 300); // we debounce the participants to avoid unnecessary rerenders that happen when participant tracks are all subscribed simultaneously + const allParticipants = useDebouncedValue(_allParticipants, 300); // we debounce the participants to avoid unnecessary rerenders that happen when participant tracks are all subscribed simultaneously const [participantInSpotlight, ...otherParticipants] = allParticipants; const isScreenShareOnSpotlight = participantInSpotlight && hasScreenShare(participantInSpotlight); @@ -103,7 +103,7 @@ export const CallParticipantsSpotlight = ({ > {participantInSpotlight && ParticipantView && - (isScreenShareOnSpotlight && ScreenShareOverlay ? ( + (participantInSpotlight.isLocalParticipant && ScreenShareOverlay ? ( ) : ( @@ -72,6 +71,7 @@ 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,10 +82,7 @@ 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, @@ -169,31 +166,15 @@ export const CallParticipantsList = ({ }); const itemContainerStyle = useMemo>(() => { - const style = { - width: itemWidth, - height: itemHeight, - marginHorizontal: theme.variants.spacingSizes.xs, - marginVertical: theme.variants.spacingSizes.xs, - }; - + const style = { width: itemWidth, height: itemHeight }; if (horizontal) { - 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]; + return [styles.participantWrapperHorizontal, style]; } - if (landscape) { - const landscapeStyle = { - marginVertical: theme.variants.spacingSizes.xs, - borderRadius: theme.variants.borderRadiusSizes.md, - }; - return [landscapeStyle, style]; + return [styles.landScapeStyle, style]; } return style; - }, [itemWidth, itemHeight, horizontal, landscape, theme]); + }, [itemWidth, itemHeight, horizontal, landscape]); const participantProps: ParticipantViewComponentProps = { ParticipantLabel, @@ -227,9 +208,9 @@ export const CallParticipantsList = ({ [itemContainerStyle] ); - // 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; + // 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; if (!shouldWrapByColumns) { return ( @@ -270,19 +251,17 @@ export const CallParticipantsList = ({ ); }; -const useStyles = () => { - const { theme } = useTheme(); - return useMemo( - () => - StyleSheet.create({ - flexed: { - flex: 1, - margin: theme.variants.spacingSizes.xs, - }, - }), - [theme] - ); -}; +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, + }, +}); /** * 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. @@ -313,16 +292,15 @@ 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 2 rows - itemHeight = containerHeight / 2; + // generally, we display the participants in 3 rows + itemHeight = containerHeight / 3; } } let itemWidth = containerWidth / numberOfColumns; - 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; + 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; } return { itemHeight, itemWidth }; 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 36be9835c3..a226b41b1b 100644 --- a/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantLabel.tsx +++ b/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantLabel.tsx @@ -101,7 +101,7 @@ export const ParticipantLabel = ({ {participantLabel} - + {isPinningEnabled && ( diff --git a/packages/react-native-sdk/src/constants/TestIds.ts b/packages/react-native-sdk/src/constants/TestIds.ts index f5f38f92de..aadf5b2682 100644 --- a/packages/react-native-sdk/src/constants/TestIds.ts +++ b/packages/react-native-sdk/src/constants/TestIds.ts @@ -10,7 +10,6 @@ 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 3e8db57542..67774527bb 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: 228, - width: 140, - borderRadius: 16, + height: 140, + width: 80, + borderRadius: 10, }; export const LOBBY_VIDEO_VIEW_HEIGHT = 240; diff --git a/packages/react-native-sdk/src/icons/Spotlight.tsx b/packages/react-native-sdk/src/icons/Spotlight.tsx new file mode 100644 index 0000000000..30a39acb21 --- /dev/null +++ b/packages/react-native-sdk/src/icons/Spotlight.tsx @@ -0,0 +1,18 @@ +import React from 'react'; +import { Svg, Path } from 'react-native-svg'; +import { ColorValue } from 'react-native/types'; + +type Props = { + color: ColorValue; +}; + +export const SpotLight = ({ color }: Props) => ( + + + +); diff --git a/packages/react-native-sdk/src/icons/index.tsx b/packages/react-native-sdk/src/icons/index.tsx index d3f655b6e4..f3b3ee722f 100644 --- a/packages/react-native-sdk/src/icons/index.tsx +++ b/packages/react-native-sdk/src/icons/index.tsx @@ -8,6 +8,7 @@ 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/sample-apps/react-native/dogfood/src/assets/FullScreen.tsx b/sample-apps/react-native/dogfood/src/assets/FullScreen.tsx deleted file mode 100644 index 553b889f9f..0000000000 --- a/sample-apps/react-native/dogfood/src/assets/FullScreen.tsx +++ /dev/null @@ -1,17 +0,0 @@ -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) => ( - - - -); From eaebe92692765532e405411f225e83501299fb68 Mon Sep 17 00:00:00 2001 From: Kristian Martinoski Date: Fri, 1 Nov 2024 15:55:16 +0100 Subject: [PATCH 36/36] fix: revert callContent changes --- .../src/components/Call/CallContent/CallContent.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 b6126f5071..cf2e591efc 100644 --- a/packages/react-native-sdk/src/components/Call/CallContent/CallContent.tsx +++ b/packages/react-native-sdk/src/components/Call/CallContent/CallContent.tsx @@ -68,7 +68,7 @@ export type CallContentProps = Pick< > & CallContentComponentProps & { /** - * This switches the participant's layout between the grid, spotlight and fullscreen mode. + * This switches the participant's layout between the grid and the spotlight mode. */ layout?: 'grid' | 'spotlight'; /** @@ -224,7 +224,7 @@ export const CallContent = ({ ) : ( - )}{' '} + )} {!isInPiPMode && CallControls && (