diff --git a/packages/react-bindings/src/hooks/callUtilHooks.ts b/packages/react-bindings/src/hooks/callUtilHooks.ts index e8ce9ef83f..e4e1a74233 100644 --- a/packages/react-bindings/src/hooks/callUtilHooks.ts +++ b/packages/react-bindings/src/hooks/callUtilHooks.ts @@ -1,6 +1,7 @@ import { useCallback, useEffect, useState } from 'react'; import { useCall } from '../contexts'; import { useIsCallRecordingInProgress } from './callStateHooks'; + /** * Custom hook for toggling call recording in a video call. * diff --git a/packages/react-native-sdk/__tests__/components/CallControls.test.tsx b/packages/react-native-sdk/__tests__/components/CallControls.test.tsx index 49188e04b4..2b611fc123 100644 --- a/packages/react-native-sdk/__tests__/components/CallControls.test.tsx +++ b/packages/react-native-sdk/__tests__/components/CallControls.test.tsx @@ -3,11 +3,10 @@ import { mockClientWithUser } from '../mocks/client'; import mockParticipant from '../mocks/participant'; import { ButtonTestIds, ComponentTestIds } from '../../src/constants/TestIds'; import { mockCall } from '../mocks/call'; -import { fireEvent, render, screen, waitFor } from '../utils/RNTLTools'; +import { fireEvent, render, screen } from '../utils/RNTLTools'; import { OwnCapability } from '@stream-io/video-client'; import { defaultEmojiReactions } from '../../src/constants'; -import { CallControls } from '../../src/components/Call/CallControls/CallControls'; -import { ChatButton } from '../../src/components/Call/CallControls/ChatButton'; +import { CallControls } from '../../src'; import { HangUpCallButton } from '../../src/components/Call/CallControls/HangupCallButton'; import { ReactionsButton } from '../../src/components/Call/CallControls/ReactionsButton'; @@ -18,48 +17,6 @@ enum P_IDS { LOCAL_1 = 'local-1', } -describe('ChatButton', () => { - it('should render an unread badge indicator when the value is defined in the chatButton prop', async () => { - const call = mockCall(mockClientWithUser(), [ - mockParticipant({ - isLocalParticipant: true, - sessionId: P_IDS.LOCAL_1, - userId: P_IDS.LOCAL_1, - }), - ]); - - render(, { - call, - }); - - const indicator = await screen.findByText('1'); - - expect(indicator).toBeVisible(); - }); - - it('should not render an unread badge indicator when the value is 0 in the chatButton prop', async () => { - const call = mockCall(mockClientWithUser(), [ - mockParticipant({ - isLocalParticipant: true, - sessionId: P_IDS.LOCAL_1, - userId: P_IDS.LOCAL_1, - }), - ]); - - render(, { - call, - }); - - await waitFor(() => - expect(() => - screen.getByTestId(ComponentTestIds.CHAT_UNREAD_BADGE_COUNT_INDICATOR) - ).toThrow( - /Unable to find an element with testID: chat-unread-badge-count-indicator/i - ) - ); - }); -}); - describe('ReactionsButton', () => { it('render reaction button in call controls component', async () => { const call = mockCall( diff --git a/packages/react-native-sdk/__tests__/components/ParticipantView.test.tsx b/packages/react-native-sdk/__tests__/components/ParticipantView.test.tsx index 15077486ba..c604478f83 100644 --- a/packages/react-native-sdk/__tests__/components/ParticipantView.test.tsx +++ b/packages/react-native-sdk/__tests__/components/ParticipantView.test.tsx @@ -43,7 +43,6 @@ describe('ParticipantView', () => { expect( await screen.findByTestId(ComponentTestIds.PARTICIPANT_AVATAR) ).toBeOnTheScreen(); - expect(screen.getByTestId(IconTestIds.MUTED_VIDEO)).toBeOnTheScreen(); expect(screen.getByText(testParticipant.name)).toBeOnTheScreen(); // reaction is visible and then disappears after 5500 ms expect(screen.getByText('🎉')).toBeOnTheScreen(); diff --git a/packages/react-native-sdk/docusaurus/docs/reactnative/04-ui-components/call/call-content.mdx b/packages/react-native-sdk/docusaurus/docs/reactnative/04-ui-components/call/call-content.mdx index bf561290fd..63086107a8 100644 --- a/packages/react-native-sdk/docusaurus/docs/reactnative/04-ui-components/call/call-content.mdx +++ b/packages/react-native-sdk/docusaurus/docs/reactnative/04-ui-components/call/call-content.mdx @@ -10,7 +10,6 @@ import CallContentSpotlight from '../../assets/04-ui-components/call/call-conten import CallTopView from '../../common-content/ui-components/call/call-content/call-top-view.mdx'; import CallControls from '../../common-content/ui-components/call/call-content/call-controls.mdx'; -import ParticipantsInfoBadge from '../../common-content/ui-components/call/call-content/participants-info-badge.mdx'; import Landscape from '../../common-content/ui-components/call/call-content/landscape.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'; 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 6f82a92401..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'; @@ -60,10 +59,6 @@ Style to override the container of the `CallTopView`. | ---------------------------------------------------------- | | [ViewStyle](https://reactnative.dev/docs/view-style-props) | -### `ParticipantsInfoBadge` - - - ## Customization You can create your own custom `CallTopView` using the [Call Top View UI Cookbook guide](../../../ui-cookbook/replacing-call-top-view/). diff --git a/packages/react-native-sdk/docusaurus/docs/reactnative/05-ui-cookbook/19-call-quality-rating.mdx b/packages/react-native-sdk/docusaurus/docs/reactnative/05-ui-cookbook/19-call-quality-rating.mdx index ea569029fd..b71429ee6e 100644 --- a/packages/react-native-sdk/docusaurus/docs/reactnative/05-ui-cookbook/19-call-quality-rating.mdx +++ b/packages/react-native-sdk/docusaurus/docs/reactnative/05-ui-cookbook/19-call-quality-rating.mdx @@ -69,7 +69,7 @@ const FeedbackModal: = () => { @@ -93,8 +93,8 @@ const FeedbackModal: = () => { = rating - ? colors.iconAlertSuccess - : colors.typeSecondary + ? colors.iconSuccess + : colors.iconPrimary } /> diff --git a/packages/react-native-sdk/docusaurus/docs/reactnative/common-content/ui-components/call/call-content/participants-info-badge.mdx b/packages/react-native-sdk/docusaurus/docs/reactnative/common-content/ui-components/call/call-content/participants-info-badge.mdx deleted file mode 100644 index 92a7cb3c9d..0000000000 --- a/packages/react-native-sdk/docusaurus/docs/reactnative/common-content/ui-components/call/call-content/participants-info-badge.mdx +++ /dev/null @@ -1,5 +0,0 @@ -Component to customize the ParticipantInfoBadge of the CallTopView. - -| Type | Default Value | -| ----------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `ComponentType`\| `undefined` | [`ParticipantsInfoBadge`](https://github.com/GetStream/stream-video-js/blob/main/packages/react-native-sdk/src/components/Call/CallTopView/ParticipantsInfoBadge.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 51637a173e..19839b5d87 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,6 @@ -import React, { useEffect, useState } from 'react'; +import React, { useEffect, useState, useMemo } from 'react'; import { StyleSheet, View, ViewStyle } from 'react-native'; import InCallManager from 'react-native-incall-manager'; - -import { - CallTopView as DefaultCallTopView, - CallTopViewProps, -} from '../CallTopView'; import { CallParticipantsGrid, CallParticipantsGridProps, @@ -45,10 +40,6 @@ export type StreamReactionType = StreamReaction & { type CallContentComponentProps = ParticipantViewComponentProps & Pick & { - /** - * Component to customize the CallTopView component. - */ - CallTopView?: React.ComponentType | null; /** * Component to customize the CallControls component. */ @@ -71,10 +62,6 @@ export type CallContentProps = Pick< HangUpCallButtonProps, 'onHangupCallHandler' > & - Pick< - CallTopViewProps, - 'onBackPressed' | 'onParticipantInfoPress' | 'ParticipantsInfoBadge' - > & CallContentComponentProps & { /** * This switches the participant's layout between the grid and the spotlight mode. @@ -100,11 +87,8 @@ export type CallContentProps = Pick< }; export const CallContent = ({ - onBackPressed, - onParticipantInfoPress, onHangupCallHandler, CallParticipantsList, - CallTopView = DefaultCallTopView, CallControls = DefaultCallControls, FloatingParticipantView = DefaultFloatingParticipantView, ScreenShareOverlay = DefaultScreenShareOverlay, @@ -113,7 +97,6 @@ export const CallContent = ({ ParticipantReaction, ParticipantVideoFallback, ParticipantView, - ParticipantsInfoBadge, VideoRenderer, layout = 'grid', landscape = false, @@ -125,6 +108,7 @@ export const CallContent = ({ showRemoteParticipantInFloatingView, setShowRemoteParticipantInFloatingView, ] = useState(false); + const styles = useStyles(); const { theme: { callContent }, } = useTheme(); @@ -213,20 +197,13 @@ export const CallContent = ({ /> )} - + - {!isInPiPMode && CallTopView && ( - - )} {showFloatingView && FloatingParticipantView && ( { + const { theme } = useTheme(); + return useMemo( + () => + StyleSheet.create({ + container: { + flex: 1, + paddingBottom: theme.variants.insets.bottom, + paddingLeft: theme.variants.insets.left, + paddingRight: theme.variants.insets.right, + paddingTop: theme.variants.insets.top, + backgroundColor: theme.colors.sheetPrimary, + }, + content: { flex: 1 }, + view: { + ...StyleSheet.absoluteFillObject, + zIndex: Z_INDEX.IN_FRONT, + }, + }), + [theme] + ); +}; diff --git a/packages/react-native-sdk/src/components/Call/CallControls/AcceptCallButton.tsx b/packages/react-native-sdk/src/components/Call/CallControls/AcceptCallButton.tsx index 4eeab20f43..03188e46ea 100644 --- a/packages/react-native-sdk/src/components/Call/CallControls/AcceptCallButton.tsx +++ b/packages/react-native-sdk/src/components/Call/CallControls/AcceptCallButton.tsx @@ -2,7 +2,7 @@ import { useCall } from '@stream-io/video-react-bindings'; import { getLogger } from '@stream-io/video-client'; import React from 'react'; import { CallControlsButton } from './CallControlsButton'; -import { Phone } from '../../../icons'; +import { IconWrapper, Phone } from '../../../icons'; import { useTheme } from '../../../contexts/ThemeContext'; /** @@ -34,7 +34,7 @@ export const AcceptCallButton = ({ const { theme: { colors, - variants: { buttonSizes }, + variants: { buttonSizes, iconSizes }, acceptCallButton, }, } = useTheme(); @@ -57,11 +57,13 @@ export const AcceptCallButton = ({ return ( - + + + ); }; diff --git a/packages/react-native-sdk/src/components/Call/CallControls/CallControls.tsx b/packages/react-native-sdk/src/components/Call/CallControls/CallControls.tsx index 6033525a77..4545b6bed7 100644 --- a/packages/react-native-sdk/src/components/Call/CallControls/CallControls.tsx +++ b/packages/react-native-sdk/src/components/Call/CallControls/CallControls.tsx @@ -40,7 +40,7 @@ export const CallControls = ({ - + ); }; 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 cc811d7a82..f5d36035a4 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, @@ -57,8 +62,9 @@ export const CallControlsButton = ( const { theme: { - variants: { buttonSizes }, colors, + defaults, + variants: { roundButtonSizes }, callControlsButton: { container }, }, } = useTheme(); @@ -67,18 +73,18 @@ export const CallControlsButton = ( styles.container, { backgroundColor: disabled - ? colors.disabled - : colorProp || colors.static_white, + ? disabledColorProp || colors.buttonDisabled + : colorProp || colors.buttonSecondary, opacity: pressed ? 0.2 : 1, - height: size || buttonSizes.sm, - width: size || buttonSizes.sm, - borderRadius: (size || buttonSizes.sm) / 2, - borderColor: colors.content_bg, + height: size || roundButtonSizes.lg, + width: size || roundButtonSizes.lg, + borderRadius: defaults.borderRadius, }, styleProp?.container ?? null, container, ]; + const childrenSize = (size || roundButtonSizes.lg) / 2 - 5; return ( @@ -105,16 +108,7 @@ export const CallControlsButton = ( const styles = StyleSheet.create({ container: { justifyContent: 'center', - borderWidth: 1, alignItems: 'center', - // For iOS - shadowColor: '#000', - shadowOffset: { - width: 0, - height: 2, - }, - shadowOpacity: 0.25, - shadowRadius: 8, // For android elevation: 6, diff --git a/packages/react-native-sdk/src/components/Call/CallControls/ChatButton.tsx b/packages/react-native-sdk/src/components/Call/CallControls/ChatButton.tsx deleted file mode 100644 index c980227458..0000000000 --- a/packages/react-native-sdk/src/components/Call/CallControls/ChatButton.tsx +++ /dev/null @@ -1,90 +0,0 @@ -import React from 'react'; -import { StyleSheet, Text, View } from 'react-native'; -import { CallControlsButton } from './CallControlsButton'; -import { Chat } from '../../../icons'; -import { ComponentTestIds } from '../../../constants/TestIds'; -import { Z_INDEX } from '../../../constants'; -import { useTheme } from '../../../contexts/ThemeContext'; - -/** - * The props for the Chat Button in the Call Controls. - */ -export type ChatButtonProps = { - /** - * Handler to be called when the chat button is pressed. - * @returns void - */ - onPressHandler?: () => void; - /** - * The count of the current unread message to be displayed above on the Chat button. - */ - unreadBadgeCount?: number; -}; - -/** - * Button to open the Chat window while in the call. - * - * This call also display the unread count indicator/badge is there messages that are unread. - */ -export const ChatButton = ({ - onPressHandler, - unreadBadgeCount, -}: ChatButtonProps) => { - const { - theme: { colors, chatButton }, - } = useTheme(); - return ( - - - - - ); -}; - -const UnreadBadgeCountIndicator = ({ - count, -}: { - count: ChatButtonProps['unreadBadgeCount']; -}) => { - const { - theme: { colors, typefaces }, - } = useTheme(); - - // Don't show badge if count is 0 or undefined - if (!count) { - return null; - } - - return ( - - - {count} - - - ); -}; - -const styles = StyleSheet.create({ - chatBadge: { - borderRadius: 30, - position: 'absolute', - left: 15, - bottom: 20, - zIndex: Z_INDEX.IN_FRONT, - height: 24, - width: 24, - justifyContent: 'center', - }, - chatBadgeText: { - textAlign: 'center', - }, -}); 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 cd9d5513ec..3faf9b638e 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. @@ -42,7 +43,7 @@ export const HangUpCallButton = ({ const { useCallCallingState } = useCallStateHooks(); const callingState = useCallCallingState(); const { - theme: { colors, hangupCallButton }, + theme: { colors, hangupCallButton, variants }, } = useTheme(); const onPress = async () => { @@ -67,20 +68,14 @@ export const HangUpCallButton = ({ return ( - + + + ); }; - -// 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/CallControls/IncomingCallControls.tsx b/packages/react-native-sdk/src/components/Call/CallControls/IncomingCallControls.tsx index bc376ff57c..485a3371b2 100644 --- a/packages/react-native-sdk/src/components/Call/CallControls/IncomingCallControls.tsx +++ b/packages/react-native-sdk/src/components/Call/CallControls/IncomingCallControls.tsx @@ -24,12 +24,16 @@ export const IncomingCallControls = ({ onRejectCallHandler, }: IncomingCallControlsProps) => { const { - theme: { incomingCall }, + theme: { + incomingCall, + variants: { buttonSizes }, + }, } = useTheme(); return ( diff --git a/packages/react-native-sdk/src/components/Call/CallControls/LobbyControls.tsx b/packages/react-native-sdk/src/components/Call/CallControls/LobbyControls.tsx index 191b4d8205..078b1875e3 100644 --- a/packages/react-native-sdk/src/components/Call/CallControls/LobbyControls.tsx +++ b/packages/react-native-sdk/src/components/Call/CallControls/LobbyControls.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { StyleSheet, View } from 'react-native'; import { ToggleAudioPreviewButton } from './ToggleAudioPreviewButton'; import { ToggleVideoPreviewButton } from './ToggleVideoPreviewButton'; @@ -11,6 +11,7 @@ export const LobbyControls = () => { const { theme: { lobbyControls }, } = useTheme(); + const styles = useStyles(); return ( @@ -19,10 +20,17 @@ export const LobbyControls = () => { ); }; -const styles = StyleSheet.create({ - container: { - paddingVertical: 12, - flexDirection: 'row', - justifyContent: 'space-evenly', - }, -}); +const useStyles = () => { + const { theme } = useTheme(); + return useMemo( + () => + StyleSheet.create({ + container: { + paddingTop: theme.variants.spacingSizes.xs, + flexDirection: 'row', + justifyContent: 'space-evenly', + }, + }), + [theme] + ); +}; diff --git a/packages/react-native-sdk/src/components/Call/CallControls/ReactionsButton.tsx b/packages/react-native-sdk/src/components/Call/CallControls/ReactionsButton.tsx index 03d406d61a..51e56d8563 100644 --- a/packages/react-native-sdk/src/components/Call/CallControls/ReactionsButton.tsx +++ b/packages/react-native-sdk/src/components/Call/CallControls/ReactionsButton.tsx @@ -3,7 +3,7 @@ import React, { useState } from 'react'; import { CallControlsButton } from './CallControlsButton'; import { OwnCapability } from '@stream-io/video-client'; import { ButtonTestIds } from '../../../constants/TestIds'; -import { Reaction } from '../../../icons'; +import { IconWrapper, Reaction } from '../../../icons'; import { ReactionsPicker } from './internal/ReactionsPicker'; import { LayoutChangeEvent, LayoutRectangle } from 'react-native'; import { useTheme } from '../../../contexts/ThemeContext'; @@ -71,7 +71,9 @@ export const ReactionsButton = ({ onPress={reactionsButtonHandler} onLayout={onReactionsButtonLayout} > - + + + {showReactionsPicker && ( 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 5679674efc..a27c009ce4 100644 --- a/packages/react-native-sdk/src/components/Call/CallControls/RejectCallButton.tsx +++ b/packages/react-native-sdk/src/components/Call/CallControls/RejectCallButton.tsx @@ -1,7 +1,7 @@ import { useCall, useCallStateHooks } from '@stream-io/video-react-bindings'; import React from 'react'; import { CallControlsButton } from './CallControlsButton'; -import { PhoneDown } from '../../../icons'; +import { IconWrapper, PhoneDown } from '../../../icons'; import { CallingState, getLogger } from '@stream-io/video-client'; import { useTheme } from '../../../contexts/ThemeContext'; @@ -54,7 +54,7 @@ export const RejectCallButton = ({ theme: { colors, rejectCallButton, - variants: { buttonSizes }, + variants: { buttonSizes, iconSizes }, }, } = useTheme(); const rejectCallHandler = async () => { @@ -79,13 +79,15 @@ export const RejectCallButton = ({ return ( - + + + ); }; diff --git a/packages/react-native-sdk/src/components/Call/CallControls/ScreenShareToggleButton.tsx b/packages/react-native-sdk/src/components/Call/CallControls/ScreenShareToggleButton.tsx index d9a70c8a9a..c8bbc5314a 100644 --- a/packages/react-native-sdk/src/components/Call/CallControls/ScreenShareToggleButton.tsx +++ b/packages/react-native-sdk/src/components/Call/CallControls/ScreenShareToggleButton.tsx @@ -6,6 +6,7 @@ import { StopScreenShare } from '../../../icons/StopScreenShare'; import { CallControlsButton } from './CallControlsButton'; import { useTheme } from '../../../contexts/ThemeContext'; import { useScreenShareButton } from '../../../hooks/useScreenShareButton'; +import { IconWrapper } from '../../../icons'; /** * The props for the Screen Share button in the Call Controls. @@ -32,7 +33,7 @@ export const ScreenShareToggleButton = ({ onScreenShareStoppedHandler, }: ScreenShareToggleButtonProps) => { const { - theme: { colors, screenShareToggleButton }, + theme: { colors, screenShareToggleButton, variants }, } = useTheme(); const screenCapturePickerViewiOSRef = useRef(null); @@ -48,17 +49,27 @@ export const ScreenShareToggleButton = ({ return ( - {hasPublishedScreenShare ? ( - - ) : ( - - )} + + {hasPublishedScreenShare ? ( + + ) : ( + + )} + {Platform.OS === 'ios' && ( )} diff --git a/packages/react-native-sdk/src/components/Call/CallControls/ToggleAudioPreviewButton.tsx b/packages/react-native-sdk/src/components/Call/CallControls/ToggleAudioPreviewButton.tsx index 0d6482d7aa..733d680659 100644 --- a/packages/react-native-sdk/src/components/Call/CallControls/ToggleAudioPreviewButton.tsx +++ b/packages/react-native-sdk/src/components/Call/CallControls/ToggleAudioPreviewButton.tsx @@ -1,7 +1,7 @@ import { useCallStateHooks } from '@stream-io/video-react-bindings'; import React from 'react'; import { useTheme } from '../../../contexts'; -import { Mic, MicOff } from '../../../icons'; +import { IconWrapper, Mic, MicOff } from '../../../icons'; import { CallControlsButton } from './CallControlsButton'; /** @@ -26,6 +26,7 @@ export const ToggleAudioPreviewButton = ({ colors, toggleAudioPreviewButton, variants: { buttonSizes }, + defaults, }, } = useTheme(); const { useMicrophoneState } = useCallStateHooks(); @@ -42,23 +43,20 @@ export const ToggleAudioPreviewButton = ({ return ( - {!optimisticIsMute ? ( - - ) : ( - - )} + + {!optimisticIsMute ? ( + + ) : ( + + )} + ); }; diff --git a/packages/react-native-sdk/src/components/Call/CallControls/ToggleAudioPublishingButton.tsx b/packages/react-native-sdk/src/components/Call/CallControls/ToggleAudioPublishingButton.tsx index ab1d01faec..700277f7af 100644 --- a/packages/react-native-sdk/src/components/Call/CallControls/ToggleAudioPublishingButton.tsx +++ b/packages/react-native-sdk/src/components/Call/CallControls/ToggleAudioPublishingButton.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { OwnCapability } from '@stream-io/video-client'; import { Restricted, useCallStateHooks } from '@stream-io/video-react-bindings'; import { CallControlsButton } from './CallControlsButton'; -import { Mic, MicOff } from '../../../icons'; +import { IconWrapper, Mic, MicOff } from '../../../icons'; import { useTheme } from '../../../contexts/ThemeContext'; /** @@ -26,7 +26,7 @@ export const ToggleAudioPublishingButton = ({ const { optimisticIsMute, microphone } = useMicrophoneState(); const { - theme: { colors, toggleAudioPublishingButton }, + theme: { colors, toggleAudioPublishingButton, defaults }, } = useTheme(); const onPress = async () => { if (onPressHandler) { @@ -41,14 +41,18 @@ export const ToggleAudioPublishingButton = ({ - {!optimisticIsMute ? ( - - ) : ( - - )} + + {!optimisticIsMute ? ( + + ) : ( + + )} + ); 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 68c99ec396..86c91d0b65 100644 --- a/packages/react-native-sdk/src/components/Call/CallControls/ToggleCameraFaceButton.tsx +++ b/packages/react-native-sdk/src/components/Call/CallControls/ToggleCameraFaceButton.tsx @@ -2,8 +2,9 @@ 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'; +import { ColorValue } from 'react-native'; /** * Props for the Toggle Camera face button. @@ -14,6 +15,11 @@ export type ToggleCameraFaceButtonProps = { * @returns void */ onPressHandler?: () => void; + + /** + * Background color of the button. + */ + backgroundColor?: ColorValue; }; /** @@ -21,6 +27,7 @@ export type ToggleCameraFaceButtonProps = { */ export const ToggleCameraFaceButton = ({ onPressHandler, + backgroundColor, }: ToggleCameraFaceButtonProps) => { const { useCameraState, useCallSettings } = useCallStateHooks(); const { camera, optimisticIsMute, direction } = useCameraState(); @@ -28,7 +35,7 @@ export const ToggleCameraFaceButton = ({ const isVideoEnabledInCall = callSettings?.video.enabled; const { - theme: { colors, toggleCameraFaceButton }, + theme: { colors, toggleCameraFaceButton, variants }, } = useTheme(); const onPress = async () => { if (onPressHandler) { @@ -47,17 +54,23 @@ export const ToggleCameraFaceButton = ({ - + + + ); diff --git a/packages/react-native-sdk/src/components/Call/CallControls/ToggleVideoPreviewButton.tsx b/packages/react-native-sdk/src/components/Call/CallControls/ToggleVideoPreviewButton.tsx index 28e519614e..8845e76f86 100644 --- a/packages/react-native-sdk/src/components/Call/CallControls/ToggleVideoPreviewButton.tsx +++ b/packages/react-native-sdk/src/components/Call/CallControls/ToggleVideoPreviewButton.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { useCallStateHooks } from '@stream-io/video-react-bindings'; import { useTheme } from '../../../contexts'; import { CallControlsButton } from './CallControlsButton'; -import { Video, VideoSlash } from '../../../icons'; +import { IconWrapper, Video, VideoSlash } from '../../../icons'; /** * Props for the Toggle Video preview button @@ -25,7 +25,7 @@ export const ToggleVideoPreviewButton = ({ theme: { colors, toggleVideoPreviewButton, - variants: { buttonSizes }, + variants: { buttonSizes, iconSizes }, }, } = useTheme(); const { useCameraState, useCallSettings } = useCallStateHooks(); @@ -47,23 +47,20 @@ export const ToggleVideoPreviewButton = ({ return ( - {!optimisticIsMute ? ( - ); }; diff --git a/packages/react-native-sdk/src/components/Call/CallControls/ToggleVideoPublishingButton.tsx b/packages/react-native-sdk/src/components/Call/CallControls/ToggleVideoPublishingButton.tsx index 6486e48de7..5d1dbb071c 100644 --- a/packages/react-native-sdk/src/components/Call/CallControls/ToggleVideoPublishingButton.tsx +++ b/packages/react-native-sdk/src/components/Call/CallControls/ToggleVideoPublishingButton.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { OwnCapability } from '@stream-io/video-client'; import { Restricted, useCallStateHooks } from '@stream-io/video-react-bindings'; import { CallControlsButton } from './CallControlsButton'; -import { Video, VideoSlash } from '../../../icons'; +import { IconWrapper, Video, VideoSlash } from '../../../icons'; import { useTheme } from '../../../contexts/ThemeContext'; /** @@ -27,7 +27,7 @@ export const ToggleVideoPublishingButton = ({ const callSettings = useCallSettings(); const isVideoEnabledInCall = callSettings?.video.enabled; const { - theme: { colors }, + theme: { colors, variants }, } = useTheme(); const onPress = async () => { if (onPressHandler) { @@ -45,13 +45,20 @@ export const ToggleVideoPublishingButton = ({ - {!optimisticIsMute ? ( - ); 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..1fa2a0543c 100644 --- a/packages/react-native-sdk/src/components/Call/CallControls/index.tsx +++ b/packages/react-native-sdk/src/components/Call/CallControls/index.tsx @@ -6,7 +6,6 @@ export * from './ToggleVideoPreviewButton'; export * from './ToggleAudioPublishingButton'; export * from './ToggleVideoPublishingButton'; export * from './ToggleCameraFaceButton'; -export * from './ChatButton'; export * from './ReactionsButton'; export * from './CallControls'; export * from './CallControlsButton'; diff --git a/packages/react-native-sdk/src/components/Call/CallControls/internal/ReactionsPicker.tsx b/packages/react-native-sdk/src/components/Call/CallControls/internal/ReactionsPicker.tsx index c86ac1c241..5708ea6534 100644 --- a/packages/react-native-sdk/src/components/Call/CallControls/internal/ReactionsPicker.tsx +++ b/packages/react-native-sdk/src/components/Call/CallControls/internal/ReactionsPicker.tsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef } from 'react'; +import React, { useEffect, useMemo, useRef } from 'react'; import { LayoutRectangle, Pressable, @@ -18,26 +18,24 @@ type ReactionPickerProps = Pick & { onRequestedClose: () => void; }; -const TOP_PADDING = 4; -const REACTION_MARGIN_BOTTOM = 4; - export const ReactionsPicker = ({ supportedReactions = defaultEmojiReactions, reactionsButtonLayoutRectangle, onRequestedClose, }: ReactionPickerProps) => { const { - theme: { colors, reactionsPicker }, + theme: { colors, reactionsPicker, variants }, } = useTheme(); + const styles = useStyles(); const call = useCall(); const size = reactionsButtonLayoutRectangle?.width ?? 0; const reactionItemSize = size * 0.8; const popupHeight = // the top padding - TOP_PADDING + + variants.spacingSizes.xs + // take margins into account - REACTION_MARGIN_BOTTOM * supportedReactions.length + + variants.spacingSizes.xs * supportedReactions.length + // the size of the reaction icon items (same size as reactions button * amount of reactions) reactionItemSize * supportedReactions.length; @@ -104,7 +102,7 @@ export const ReactionsPicker = ({ styles.reactionsPopup, reactionsPopupStyle, { - backgroundColor: colors.static_grey, + backgroundColor: colors.sheetSecondary, }, reactionsPicker.reactionsPopup, ]} @@ -119,10 +117,7 @@ export const ReactionsPicker = ({ style={[ styles.reactionItem, reactionItemStyle, - { - // temporary background color until we have theming - backgroundColor: colors.overlay, - }, + { backgroundColor: colors.buttonSecondary }, reactionsPicker.reactionItem, ]} onPress={() => { @@ -159,7 +154,7 @@ export const ReactionsPicker = ({ style={[ reactionsButtonDimmerStyle, { - backgroundColor: colors.static_grey, + backgroundColor: colors.sheetSecondary, }, reactionsPicker.reactionsButtonDimmer, ]} @@ -169,22 +164,29 @@ export const ReactionsPicker = ({ ); }; -const styles = StyleSheet.create({ - reactionsPopup: { - position: 'absolute', - alignItems: 'center', - paddingTop: TOP_PADDING, - }, - reactionsButtonDimmer: { - position: 'absolute', - opacity: 0.5, - }, - reactionItem: { - alignItems: 'center', - justifyContent: 'center', - marginBottom: REACTION_MARGIN_BOTTOM, - }, - reactionText: { - fontSize: 18.5, - }, -}); +const useStyles = () => { + const { theme } = useTheme(); + return useMemo( + () => + StyleSheet.create({ + reactionsPopup: { + position: 'absolute', + alignItems: 'center', + paddingTop: theme.variants.spacingSizes.xs, + }, + reactionsButtonDimmer: { + position: 'absolute', + opacity: 0.5, + }, + reactionItem: { + alignItems: 'center', + justifyContent: 'center', + marginBottom: theme.variants.spacingSizes.xs, + }, + reactionText: { + fontSize: 18.5, + }, + }), + [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 022f86a400..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.dark_gray }, + { 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/CallLayout/CallParticipantsSpotlight.tsx b/packages/react-native-sdk/src/components/Call/CallLayout/CallParticipantsSpotlight.tsx index 2b6d8f5316..a588162dd8 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, @@ -56,8 +56,9 @@ export const CallParticipantsSpotlight = ({ disablePictureInPicture, }: CallParticipantsSpotlightProps) => { const { - theme: { colors, callParticipantsSpotlight }, + theme: { callParticipantsSpotlight, variants }, } = useTheme(); + const styles = useStyles(); const { useParticipants } = useCallStateHooks(); const _allParticipants = useParticipants({ sortBy: speakerLayoutSortPreset, @@ -88,7 +89,7 @@ export const CallParticipantsSpotlight = ({ }; const spotlightContainerLandscapeStyles: ViewStyle = { - marginHorizontal: landscape ? 0 : 8, + marginHorizontal: landscape ? 0 : variants.spacingSizes.xs, }; const showShareScreenOverlay = @@ -102,9 +103,6 @@ export const CallParticipantsSpotlight = ({ style={[ styles.container, landscapeStyles, - { - backgroundColor: colors.dark_gray, - }, callParticipantsSpotlight.container, ]} > @@ -160,20 +158,28 @@ 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 = () => { + const { theme } = useTheme(); + return useMemo( + () => + StyleSheet.create({ + container: { + flex: 1, + backgroundColor: theme.colors.sheetPrimary, + }, + fullScreenSpotlightContainer: { + flex: 1, + }, + spotlightContainer: { + flex: 2, + overflow: 'hidden', + borderRadius: theme.variants.borderRadiusSizes.sm, + marginHorizontal: theme.variants.spacingSizes.sm, + }, + callParticipantsListContainer: { + flex: 1, + }, + }), + [theme] + ); +}; 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..4aeef63b79 100644 --- a/packages/react-native-sdk/src/components/Call/CallParticipantsList/CallParticipantsList.tsx +++ b/packages/react-native-sdk/src/components/Call/CallParticipantsList/CallParticipantsList.tsx @@ -21,6 +21,7 @@ import { ParticipantViewProps, } from '../../Participant/ParticipantView'; import { CallContentProps } from '../CallContent'; +import { useTheme } from '../../../contexts'; type FlatListProps = React.ComponentProps< typeof FlatList @@ -83,6 +84,7 @@ export const CallParticipantsList = ({ supportedReactions, landscape, }: CallParticipantsListProps) => { + const styles = useStyles(); const [containerLayout, setContainerLayout] = useState({ width: 0, height: 0, @@ -163,10 +165,15 @@ export const CallParticipantsList = ({ participantsLength: participants.length, numberOfColumns, horizontal, + margin: styles.participant.margin, }); const itemContainerStyle = useMemo>(() => { - const style = { width: itemWidth, height: itemHeight }; + const style = { + width: itemWidth, + height: itemHeight, + margin: styles.participant.margin, + }; if (horizontal) { return [styles.participantWrapperHorizontal, style]; } @@ -174,7 +181,7 @@ export const CallParticipantsList = ({ return [styles.landScapeStyle, style]; } return style; - }, [itemWidth, itemHeight, horizontal, landscape]); + }, [itemWidth, itemHeight, horizontal, landscape, styles]); const participantProps: ParticipantViewComponentProps = { ParticipantLabel, @@ -251,19 +258,27 @@ 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 }, + participantWrapperHorizontal: { + // note: if marginHorizontal is changed, be sure to change the width calculation in calculateParticipantViewSize function + marginHorizontal: theme.variants.spacingSizes.sm, + borderRadius: theme.variants.borderRadiusSizes.sm, + }, + landScapeStyle: { + borderRadius: theme.variants.borderRadiusSizes.sm, + }, + participant: { + margin: theme.variants.spacingSizes.xs, + }, + }), + [theme] + ); +}; /** * This function calculates the size of the participant view based on the size of the container (the phone's screen size) and the number of participants. @@ -280,12 +295,14 @@ function calculateParticipantViewSize({ participantsLength, numberOfColumns, horizontal, + margin, }: { containerHeight: number; containerWidth: number; participantsLength: number; numberOfColumns: number; horizontal: boolean | undefined; + margin: number; }) { let itemHeight = containerHeight; // in vertical mode, we calculate the height of the participant view based on the containerHeight (aka the phone's screen height) @@ -305,5 +322,7 @@ function calculateParticipantViewSize({ itemWidth = itemWidth - 8 * 2; } + itemHeight = itemHeight - margin; + itemWidth = itemWidth - margin; return { itemHeight, itemWidth }; } diff --git a/packages/react-native-sdk/src/components/Call/CallTopView/CallTopView.tsx b/packages/react-native-sdk/src/components/Call/CallTopView/CallTopView.tsx deleted file mode 100644 index 4460425573..0000000000 --- a/packages/react-native-sdk/src/components/Call/CallTopView/CallTopView.tsx +++ /dev/null @@ -1,162 +0,0 @@ -import React, { useState } from 'react'; -import { - View, - StyleSheet, - Text, - Pressable, - StyleProp, - ViewStyle, -} from 'react-native'; -import { - ParticipantsInfoBadge as DefaultParticipantsInfoBadge, - ParticipantsInfoBadgeProps, -} from './ParticipantsInfoBadge'; -import { Back } from '../../../icons/Back'; -import { TopViewBackground } from '../../../icons'; -import { useCallStateHooks, useI18n } from '@stream-io/video-react-bindings'; -import { CallingState } from '@stream-io/video-client'; -import { useTheme } from '../../../contexts/ThemeContext'; - -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 Participant icon is pressed in the CallTopView. - * @returns - */ - onParticipantInfoPress?: () => void; - /** - * Title to be rendered at the center of the Header. - */ - title?: string; - /** - * Style to override the container of the CallTopView. - */ - style?: StyleProp; - /** - * Component to customize the ParticipantInfoBadge of the CallTopView. - */ - ParticipantsInfoBadge?: React.ComponentType | null; -}; - -export const CallTopView = ({ - onBackPressed, - onParticipantInfoPress, - title, - style: styleProp, - ParticipantsInfoBadge = DefaultParticipantsInfoBadge, -}: CallTopViewProps) => { - const [callTopViewHeight, setCallTopViewHeight] = useState(0); - const [callTopViewWidth, setCallTopViewWidth] = useState(0); - const { - theme: { - colors, - typefaces, - variants: { iconSizes }, - callTopView, - }, - } = useTheme(); - 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; - 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. */} - - - - {onBackPressed && ( - [ - styles.backIconContainer, - { - opacity: pressed ? 0.2 : 1, - height: iconSizes.md, - width: iconSizes.md, - }, - callTopView.backIconContainer, - ]} - onPress={onBackPressed} - > - - - )} - - - {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', - }, -}); diff --git a/packages/react-native-sdk/src/components/Call/CallTopView/ParticipantsInfoBadge.tsx b/packages/react-native-sdk/src/components/Call/CallTopView/ParticipantsInfoBadge.tsx deleted file mode 100644 index babeefe0cd..0000000000 --- a/packages/react-native-sdk/src/components/Call/CallTopView/ParticipantsInfoBadge.tsx +++ /dev/null @@ -1,119 +0,0 @@ -import React from 'react'; -import { Pressable, StyleSheet, Text, View } from 'react-native'; -import { Participants } from '../../../icons'; -import { useCallStateHooks } from '@stream-io/video-react-bindings'; -import { Z_INDEX } from '../../../constants'; -import { CallTopViewProps } from '..'; -import { ButtonTestIds } from '../../../constants/TestIds'; -import { useTheme } from '../../../contexts/ThemeContext'; -import { CallingState } from '@stream-io/video-client'; - -/** - * Props for the ParticipantsInfoBadge component. - */ -export type ParticipantsInfoBadgeProps = Pick< - CallTopViewProps, - 'onParticipantInfoPress' ->; - -/** - * Badge that shows the number of participants in the call. - * When pressed, it opens the ParticipantsInfoList. - */ -export const ParticipantsInfoBadge = ({ - onParticipantInfoPress, -}: ParticipantsInfoBadgeProps) => { - const { - theme: { - colors, - participantInfoBadge, - typefaces, - variants: { iconSizes }, - }, - } = useTheme(); - const { useParticipantCount, useCallMembers, useCallCallingState } = - useCallStateHooks(); - const participantCount = useParticipantCount(); - const members = useCallMembers(); - const callingState = useCallCallingState(); - - let count = 0; - /** - * We show member's length if Incoming and Outgoing Call Views are rendered. - * Else we show the count of the participants that are in the call. - * Since the members count also includes caller/callee, we reduce the count by 1. - **/ - if (callingState === CallingState.RINGING) { - count = members.length - 1; - } else { - count = participantCount; - } - - if (count === 0) { - return null; - } - - return ( - [ - styles.container, - { opacity: pressed ? 0.2 : 1 }, - participantInfoBadge.container, - ]} - disabled={!onParticipantInfoPress} - testID={ButtonTestIds.PARTICIPANTS_INFO} - > - - - - - - {count} - - - - ); -}; - -const styles = StyleSheet.create({ - container: { - flexDirection: 'row', - }, - participantCountContainer: { - justifyContent: 'center', - paddingHorizontal: 8, - borderRadius: 30, - zIndex: Z_INDEX.IN_FRONT, - bottom: 12, - right: 12, - }, - participantCountText: { - includeFontPadding: false, - textAlign: 'center', - }, -}); 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 7734edc003..0000000000 --- a/packages/react-native-sdk/src/components/Call/CallTopView/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './CallTopView'; -export * from './ParticipantsInfoBadge'; diff --git a/packages/react-native-sdk/src/components/Call/Lobby/JoinCallButton.tsx b/packages/react-native-sdk/src/components/Call/Lobby/JoinCallButton.tsx index ec15a49dc0..76c9bef7c4 100644 --- a/packages/react-native-sdk/src/components/Call/Lobby/JoinCallButton.tsx +++ b/packages/react-native-sdk/src/components/Call/Lobby/JoinCallButton.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { LobbyProps } from './Lobby'; import { Pressable, StyleSheet, Text } from 'react-native'; import { useCall, useI18n } from '@stream-io/video-react-bindings'; @@ -22,6 +22,7 @@ export const JoinCallButton = ({ const { theme: { colors, typefaces, joinCallButton }, } = useTheme(); + const styles = useStyles(); const { t } = useI18n(); const call = useCall(); @@ -45,7 +46,7 @@ export const JoinCallButton = ({ { + const { theme } = useTheme(); + return useMemo( + () => + StyleSheet.create({ + container: { + borderRadius: theme.variants.borderRadiusSizes.lg, + marginTop: theme.variants.spacingSizes.md, + paddingVertical: theme.variants.spacingSizes.sm, + }, + label: { + textAlign: 'center', + }, + }), + [theme] + ); +}; 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 72755fe18f..381a7f7f27 100644 --- a/packages/react-native-sdk/src/components/Call/Lobby/Lobby.tsx +++ b/packages/react-native-sdk/src/components/Call/Lobby/Lobby.tsx @@ -1,6 +1,5 @@ -import React, { ComponentType } from 'react'; -import { StyleSheet, Text, View, ViewStyle } from 'react-native'; -import { MicOff } from '../../../icons'; +import React, { ComponentType, useMemo } from 'react'; +import { StyleSheet, Text, View } from 'react-native'; import { useCallStateHooks, useConnectedUser, @@ -8,7 +7,6 @@ import { } from '@stream-io/video-react-bindings'; import { Avatar } from '../../utility/Avatar'; import { StreamVideoParticipant } from '@stream-io/video-client'; -import { LOBBY_VIDEO_VIEW_HEIGHT } from '../../../constants'; import { RTCView } from '@stream-io/react-native-webrtc'; import { LobbyControls as DefaultLobbyControls } from '../CallControls/LobbyControls'; import { @@ -64,6 +62,7 @@ export const Lobby = ({ const { theme: { colors, lobby, typefaces }, } = useTheme(); + const styles = useStyles(landscape); const connectedUser = useConnectedUser(); const { useCameraState, useCallSettings } = useCallStateHooks(); const callSettings = useCallSettings(); @@ -81,47 +80,21 @@ export const Lobby = ({ name: connectedUser?.name, } as StreamVideoParticipant; - const landscapeStyles: ViewStyle = { - flexDirection: landscape ? 'row' : 'column', - }; - return ( - + {connectedUser && ( - - - {t('Before Joining')} + <> + + {t('Before joining')} - - {isVideoEnabledInCall - ? t('Setup your audio and video') - : t('Setup your audio')} + + {t('Setup your audio and video')} {isVideoEnabledInCall && ( @@ -141,48 +114,38 @@ export const Lobby = ({ )} - + + )} + {LobbyControls && } + {LobbyFooter && ( + )} - - {LobbyControls && } - {LobbyFooter && ( - - )} - ); }; const ParticipantStatus = () => { const { - theme: { - colors, - typefaces, - lobby, - variants: { iconSizes }, - }, + theme: { colors, typefaces, lobby }, } = useTheme(); + const styles = useStyles(); const connectedUser = useConnectedUser(); - const { useMicrophoneState } = useCallStateHooks(); const participantLabel = connectedUser?.name ?? connectedUser?.id; - const { status: micStatus } = useMicrophoneState(); return ( { > {participantLabel} - {(!micStatus || micStatus === 'disabled') && ( - - - - )} ); }; -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 = (landscape = false) => { + const { theme } = useTheme(); + return useMemo( + () => + StyleSheet.create({ + heading: { + textAlign: 'center', + color: theme.colors.textPrimary, + paddingBottom: theme.variants.spacingSizes.xs, + }, + subHeading: { + textAlign: 'center', + paddingBottom: theme.variants.spacingSizes.md, + color: theme.colors.textSecondary, + }, + container: { + flex: 1, + justifyContent: 'center', + backgroundColor: theme.colors.sheetPrimary, + paddingRight: + theme.variants.insets.right + theme.variants.spacingSizes.sm, + paddingLeft: + theme.variants.insets.left + theme.variants.spacingSizes.sm, + paddingTop: theme.variants.insets.top, + paddingBottom: theme.variants.insets.bottom, + }, + videoContainer: { + height: landscape ? '40%' : '50%', + borderRadius: theme.variants.borderRadiusSizes.md, + justifyContent: 'space-between', + alignItems: 'center', + overflow: 'hidden', + }, + topView: {}, + participantStatusContainer: { + alignSelf: 'flex-start', + flexDirection: 'row', + alignItems: 'center', + padding: theme.variants.spacingSizes.sm, + borderTopRightRadius: theme.variants.borderRadiusSizes.sm, + }, + avatarContainer: { + flex: 2, + justifyContent: 'center', + }, + userNameLabel: { + flexShrink: 1, + }, + }), + [theme, landscape] + ); +}; diff --git a/packages/react-native-sdk/src/components/Call/Lobby/LobbyFooter.tsx b/packages/react-native-sdk/src/components/Call/Lobby/LobbyFooter.tsx index 2f495df6e1..904af1c6e4 100644 --- a/packages/react-native-sdk/src/components/Call/Lobby/LobbyFooter.tsx +++ b/packages/react-native-sdk/src/components/Call/Lobby/LobbyFooter.tsx @@ -1,12 +1,9 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { LobbyProps } from './Lobby'; import { View, StyleSheet, Text } from 'react-native'; -import { - useCall, - useCallStateHooks, - useI18n, -} from '@stream-io/video-react-bindings'; import { useTheme } from '../../../contexts/ThemeContext'; +import { useCallStateHooks, useI18n } from '@stream-io/video-react-bindings'; +import { Lock } from '../../../icons/Lock'; /** * Props for the Lobby Footer in the Lobby component. @@ -24,42 +21,44 @@ export const LobbyFooter = ({ JoinCallButton, }: LobbyFooterProps) => { const { - theme: { colors, lobby, typefaces }, + theme: { colors, lobby, variants }, } = useTheme(); + const styles = useStyles(); const { useCallSession } = useCallStateHooks(); - const { t } = useI18n(); - - const call = useCall(); const session = useCallSession(); + const numberOfParticipants = session?.participants.length; - const participantsCount = session?.participants.length; + const participantsText = useMemo(() => { + if (!numberOfParticipants) { + return t('Currently there are no other participants in the call.'); + } + if (numberOfParticipants === 1) { + return t('There is {{numberOfParticipants}} more person in the call.', { + numberOfParticipants, + }); + } + return t('There are {{numberOfParticipants}} more people in the call.', { + numberOfParticipants, + }); + }, [numberOfParticipants, t]); return ( - - - {t('You are about to join a call with id {{ callId }}.', { - callId: call?.id, - }) + - ' ' + - (participantsCount - ? t('{{ numberOfParticipants }} participant(s) are in the call.', { - numberOfParticipants: participantsCount, - }) - : t('You are first to join the call.'))} - + + + + + + + {t('You are about to join a call.') + ' ' + participantsText} + + {JoinCallButton && ( )} @@ -67,9 +66,32 @@ export const LobbyFooter = ({ ); }; -const styles = StyleSheet.create({ - infoContainer: { - padding: 12, - borderRadius: 10, - }, -}); +const useStyles = () => { + const { theme } = useTheme(); + return useMemo( + () => + StyleSheet.create({ + mainContainer: { + padding: theme.variants.spacingSizes.sm, + }, + textContainer: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + backgroundColor: theme.colors.sheetTertiary, + paddingHorizontal: theme.variants.spacingSizes.md, + paddingVertical: theme.variants.spacingSizes.xs, + borderRadius: theme.variants.borderRadiusSizes.sm, + }, + iconContainer: { + marginRight: theme.variants.spacingSizes.sm, + }, + infoText: { + fontSize: theme.variants.fontSizes.sm, + lineHeight: 20, + fontWeight: '400', + }, + }), + [theme] + ); +}; 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 f54c5201a4..b97fbc61e6 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, @@ -27,10 +23,6 @@ import { useApplyDefaultMediaStreamSettings } from '../../../hooks/useApplyDefau * Props for the IncomingCall Component. */ export type IncomingCallProps = IncomingCallControlsProps & { - /** - * Prop to customize the CallTopView component in the IncomingCall component. - */ - CallTopView?: React.ComponentType | null; /** * Prop to customize the IncomingCall controls. */ @@ -49,7 +41,6 @@ export type IncomingCallProps = IncomingCallControlsProps & { export const IncomingCall = ({ onAcceptCallHandler, onRejectCallHandler, - CallTopView = DefaultCallTopView, IncomingCallControls = DefaultIncomingCallControls, landscape, }: IncomingCallProps) => { @@ -66,7 +57,6 @@ export const IncomingCall = ({ return ( - {CallTopView && } @@ -75,7 +65,7 @@ export const IncomingCall = ({ @@ -140,7 +130,7 @@ const Background: React.FunctionComponent<{ @@ -156,7 +146,7 @@ export const styles = StyleSheet.create({ content: { flex: 1, }, - topContainer: { flex: 1 }, + topContainer: { flex: 1, justifyContent: 'center' }, incomingCallText: { marginTop: 8, textAlign: 'center', 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 2af2143c65..c69b457b90 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'; @@ -20,10 +16,6 @@ import { useApplyDefaultMediaStreamSettings } from '../../../hooks/useApplyDefau * Props for the OutgoingCall Component. */ export type OutgoingCallProps = OutgoingCallControlsProps & { - /** - * Prop to customize the CallTopView component in the IncomingCall component. - */ - CallTopView?: React.ComponentType | null; /** * Prop to customize the OutgoingCall controls. */ @@ -40,7 +32,6 @@ export type OutgoingCallProps = OutgoingCallControlsProps & { * Used after the user has initiated a call. */ export const OutgoingCall = ({ - CallTopView = DefaultCallTopView, OutgoingCallControls = DefaultOutgoingCallControls, landscape, }: OutgoingCallProps) => { @@ -65,7 +56,6 @@ export const OutgoingCall = ({ outgoingCall.container, ]} > - {CallTopView && } @@ -74,7 +64,7 @@ export const OutgoingCall = ({ { @@ -125,7 +115,7 @@ const Background = () => { @@ -147,7 +137,7 @@ const styles = StyleSheet.create({ container: { zIndex: Z_INDEX.IN_MIDDLE, }, - topContainer: { flex: 1 }, + topContainer: { flex: 1, justifyContent: 'center' }, content: { flex: 1, }, 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..a5481de26c 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, @@ -41,10 +40,6 @@ export type RingingCallContentProps = { * Prop to customize the accepted CallContent component in the RingingCallContent. This is shown after the call is accepted. */ CallContent?: React.ComponentType | null; - /** - * Prop to customize the CallTopView component in the RingingCallContent. - */ - CallTopView?: React.ComponentType | null; /** * Prop to override the component shown when the call is left. */ @@ -69,7 +64,6 @@ const RingingCallPanel = ({ IncomingCall = DefaultIncomingCall, OutgoingCall = DefaultOutgoingCall, CallContent = DefaultCallContent, - CallTopView, CallLeftIndicator = DefaultCallLeftIndicator, CallPreparingIndicator = DefaultCallPreparingIndicator, landscape, @@ -84,12 +78,8 @@ const RingingCallPanel = ({ switch (callingState) { case CallingState.RINGING: return isCallCreatedByMe - ? OutgoingCall && ( - - ) - : IncomingCall && ( - - ); + ? OutgoingCall && + : IncomingCall && ; case CallingState.LEFT: return ( CallLeftIndicator && @@ -101,11 +91,7 @@ const RingingCallPanel = ({ ) ); default: - return ( - CallContent && ( - - ) - ); + return CallContent && ; } }; diff --git a/packages/react-native-sdk/src/components/Call/RingingCallContent/TextBasedIndicator.tsx b/packages/react-native-sdk/src/components/Call/RingingCallContent/TextBasedIndicator.tsx index c65cde778e..f109517c03 100644 --- a/packages/react-native-sdk/src/components/Call/RingingCallContent/TextBasedIndicator.tsx +++ b/packages/react-native-sdk/src/components/Call/RingingCallContent/TextBasedIndicator.tsx @@ -18,7 +18,7 @@ export const TextBasedIndicator = (props: TextBasedIndicatorProps) => { } = useTheme(); return ( - + {props.onBackPress && ( { }, ]} > - + )} @@ -39,7 +39,7 @@ export const TextBasedIndicator = (props: TextBasedIndicatorProps) => { diff --git a/packages/react-native-sdk/src/components/Call/RingingCallContent/UserInfo.tsx b/packages/react-native-sdk/src/components/Call/RingingCallContent/UserInfo.tsx index 3341747739..d008d30532 100644 --- a/packages/react-native-sdk/src/components/Call/RingingCallContent/UserInfo.tsx +++ b/packages/react-native-sdk/src/components/Call/RingingCallContent/UserInfo.tsx @@ -109,7 +109,7 @@ export const UserInfo = ({ style={[ styles.name, fontStyleByMembersCount, - { color: colors.static_white }, + { color: colors.textPrimary }, userInfo.name, ]} > 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/Livestream/HostLivestream/HostLivestream.tsx b/packages/react-native-sdk/src/components/Livestream/HostLivestream/HostLivestream.tsx index 8e20bf1529..46a026631a 100644 --- a/packages/react-native-sdk/src/components/Livestream/HostLivestream/HostLivestream.tsx +++ b/packages/react-native-sdk/src/components/Livestream/HostLivestream/HostLivestream.tsx @@ -1,4 +1,4 @@ -import React, { useEffect } from 'react'; +import React, { useEffect, useMemo } from 'react'; import { StyleSheet, View } from 'react-native'; import InCallManager from 'react-native-incall-manager'; @@ -73,6 +73,7 @@ export const HostLivestream = ({ hls, disableStopPublishedStreamsOnEndStream, }: HostLivestreamProps) => { + const styles = useStyles(); const { theme: { colors, hostLivestream }, } = useTheme(); @@ -108,9 +109,7 @@ export const HostLivestream = ({ @@ -161,22 +160,34 @@ export const HostLivestream = ({ ); }; -const styles = StyleSheet.create({ - container: { - flex: 1, - }, - topViewContainer: { - position: 'absolute', - top: 0, - left: 0, - right: 0, - zIndex: Z_INDEX.IN_FRONT, - }, - controlsViewContainer: { - position: 'absolute', - bottom: 0, - left: 0, - right: 0, - zIndex: Z_INDEX.IN_FRONT, - }, -}); +const useStyles = () => { + const { theme } = useTheme(); + return useMemo( + () => + StyleSheet.create({ + container: { + flex: 1, + paddingBottom: theme.variants.insets.bottom, + paddingLeft: theme.variants.insets.left, + paddingRight: theme.variants.insets.right, + paddingTop: theme.variants.insets.top, + backgroundColor: theme.colors.sheetPrimary, + }, + topViewContainer: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + zIndex: Z_INDEX.IN_FRONT, + }, + controlsViewContainer: { + position: 'absolute', + bottom: 0, + left: 0, + right: 0, + zIndex: Z_INDEX.IN_FRONT, + }, + }), + [theme] + ); +}; diff --git a/packages/react-native-sdk/src/components/Livestream/LivestreamControls/HostLivestreamControls.tsx b/packages/react-native-sdk/src/components/Livestream/LivestreamControls/HostLivestreamControls.tsx index c5a904b631..7f4d77f243 100644 --- a/packages/react-native-sdk/src/components/Livestream/LivestreamControls/HostLivestreamControls.tsx +++ b/packages/react-native-sdk/src/components/Livestream/LivestreamControls/HostLivestreamControls.tsx @@ -45,7 +45,7 @@ export const HostLivestreamControls = ({ diff --git a/packages/react-native-sdk/src/components/Livestream/LivestreamControls/LivestreamAudioControlButton.tsx b/packages/react-native-sdk/src/components/Livestream/LivestreamControls/LivestreamAudioControlButton.tsx index fcf460e1c4..7cf858100d 100644 --- a/packages/react-native-sdk/src/components/Livestream/LivestreamControls/LivestreamAudioControlButton.tsx +++ b/packages/react-native-sdk/src/components/Livestream/LivestreamControls/LivestreamAudioControlButton.tsx @@ -2,7 +2,7 @@ import { useCallStateHooks } from '@stream-io/video-react-bindings'; import React from 'react'; import { useTheme } from '../../../contexts'; import { Pressable, StyleSheet, View } from 'react-native'; -import { Mic, MicOff } from '../../../icons'; +import { IconWrapper, Mic, MicOff } from '../../../icons'; /** * The LivestreamAudioControlButton controls the audio stream publish/unpublish while in the livestream for the host. @@ -28,7 +28,7 @@ export const LivestreamAudioControlButton = () => { style={[ styles.container, { - backgroundColor: colors.dark_gray, + backgroundColor: colors.buttonSecondary, height: buttonSizes.xs, width: buttonSizes.xs, }, @@ -45,11 +45,13 @@ export const LivestreamAudioControlButton = () => { livestreamAudioControlButton.icon, ]} > - {!optimisticIsMute ? ( - - ) : ( - - )} + + {!optimisticIsMute ? ( + + ) : ( + + )} + ); diff --git a/packages/react-native-sdk/src/components/Livestream/LivestreamControls/LivestreamScreenShareToggleButton.tsx b/packages/react-native-sdk/src/components/Livestream/LivestreamControls/LivestreamScreenShareToggleButton.tsx index 75debafb17..3ac73ecc7b 100644 --- a/packages/react-native-sdk/src/components/Livestream/LivestreamControls/LivestreamScreenShareToggleButton.tsx +++ b/packages/react-native-sdk/src/components/Livestream/LivestreamControls/LivestreamScreenShareToggleButton.tsx @@ -33,8 +33,8 @@ export const LivestreamScreenShareToggleButton = () => { styles.container, { backgroundColor: hasPublishedScreenShare - ? colors.error - : colors.dark_gray, + ? colors.buttonWarning + : colors.buttonSecondary, height: buttonSizes.xs, width: buttonSizes.xs, }, @@ -52,9 +52,9 @@ export const LivestreamScreenShareToggleButton = () => { ]} > {hasPublishedScreenShare ? ( - + ) : ( - + )} {Platform.OS === 'ios' && ( diff --git a/packages/react-native-sdk/src/components/Livestream/LivestreamControls/LivestreamVideoControlButton.tsx b/packages/react-native-sdk/src/components/Livestream/LivestreamControls/LivestreamVideoControlButton.tsx index 2b4b5c9071..e77b91c719 100644 --- a/packages/react-native-sdk/src/components/Livestream/LivestreamControls/LivestreamVideoControlButton.tsx +++ b/packages/react-native-sdk/src/components/Livestream/LivestreamControls/LivestreamVideoControlButton.tsx @@ -2,7 +2,7 @@ import { useCallStateHooks } from '@stream-io/video-react-bindings'; import React from 'react'; import { useTheme } from '../../../contexts'; import { Pressable, StyleSheet, View } from 'react-native'; -import { Video, VideoSlash } from '../../../icons'; +import { IconWrapper, Video, VideoSlash } from '../../../icons'; /** * The LivestreamVideoControlButton controls the video stream publish/unpublish while in the livestream for the host. @@ -34,7 +34,7 @@ export const LivestreamVideoControlButton = () => { style={[ styles.container, { - backgroundColor: colors.dark_gray, + backgroundColor: colors.buttonSecondary, height: buttonSizes.xs, width: buttonSizes.xs, }, @@ -51,11 +51,13 @@ export const LivestreamVideoControlButton = () => { livestreamVideoControlButton.icon, ]} > - {!optimisticIsMute ? ( - ); diff --git a/packages/react-native-sdk/src/components/Livestream/LivestreamControls/ViewerLeaveStreamButton.tsx b/packages/react-native-sdk/src/components/Livestream/LivestreamControls/ViewerLeaveStreamButton.tsx index f21ec5b3b9..468c6a6c1c 100644 --- a/packages/react-native-sdk/src/components/Livestream/LivestreamControls/ViewerLeaveStreamButton.tsx +++ b/packages/react-native-sdk/src/components/Livestream/LivestreamControls/ViewerLeaveStreamButton.tsx @@ -59,9 +59,7 @@ export const ViewerLeaveStreamButton = ({ diff --git a/packages/react-native-sdk/src/components/Livestream/LivestreamControls/ViewerLivestreamControls.tsx b/packages/react-native-sdk/src/components/Livestream/LivestreamControls/ViewerLivestreamControls.tsx index 0aec79c7f2..a212dc38b5 100644 --- a/packages/react-native-sdk/src/components/Livestream/LivestreamControls/ViewerLivestreamControls.tsx +++ b/packages/react-native-sdk/src/components/Livestream/LivestreamControls/ViewerLivestreamControls.tsx @@ -35,9 +35,7 @@ export const ViewerLivestreamControls = ({ diff --git a/packages/react-native-sdk/src/components/Livestream/LivestreamTopView/DurationBadge.tsx b/packages/react-native-sdk/src/components/Livestream/LivestreamTopView/DurationBadge.tsx index 53b5830824..e0b7f97e5e 100644 --- a/packages/react-native-sdk/src/components/Livestream/LivestreamTopView/DurationBadge.tsx +++ b/packages/react-native-sdk/src/components/Livestream/LivestreamTopView/DurationBadge.tsx @@ -120,7 +120,7 @@ export const DurationBadge = ({ mode }: DurationBadgeProps) => { @@ -139,7 +139,7 @@ export const DurationBadge = ({ mode }: DurationBadgeProps) => { diff --git a/packages/react-native-sdk/src/components/Livestream/LivestreamTopView/FollowerCount.tsx b/packages/react-native-sdk/src/components/Livestream/LivestreamTopView/FollowerCount.tsx index e8b2552d33..be561bb545 100644 --- a/packages/react-native-sdk/src/components/Livestream/LivestreamTopView/FollowerCount.tsx +++ b/packages/react-native-sdk/src/components/Livestream/LivestreamTopView/FollowerCount.tsx @@ -26,7 +26,7 @@ export const FollowerCount = ({}: FollowerCountProps) => { @@ -42,7 +42,7 @@ export const FollowerCount = ({}: FollowerCountProps) => { diff --git a/packages/react-native-sdk/src/components/Livestream/LivestreamTopView/HostLivestreamTopView.tsx b/packages/react-native-sdk/src/components/Livestream/LivestreamTopView/HostLivestreamTopView.tsx index f86ab3e734..8e9b155ca7 100644 --- a/packages/react-native-sdk/src/components/Livestream/LivestreamTopView/HostLivestreamTopView.tsx +++ b/packages/react-native-sdk/src/components/Livestream/LivestreamTopView/HostLivestreamTopView.tsx @@ -57,7 +57,7 @@ export const HostLivestreamTopView = ({ { { + const styles = useStyles(); const { theme: { viewerLivestream }, } = useTheme(); @@ -123,8 +124,20 @@ export const ViewerLivestream = ({ ); }; -const styles = StyleSheet.create({ - container: { - flex: 1, - }, -}); +const useStyles = () => { + const { theme } = useTheme(); + return useMemo( + () => + StyleSheet.create({ + container: { + flex: 1, + paddingBottom: theme.variants.insets.bottom, + paddingLeft: theme.variants.insets.left, + paddingRight: theme.variants.insets.right, + paddingTop: theme.variants.insets.top, + backgroundColor: theme.colors.sheetPrimary, + }, + }), + [theme] + ); +}; 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..7699db8e2c 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 @@ -64,7 +64,6 @@ try { [FloatingViewAlignment.bottomRight]: { x: 0, y: 0 }, }; } - return getSnapAlignments({ rootContainerDimensions: { width: containerWidth, diff --git a/packages/react-native-sdk/src/components/Participant/FloatingParticipantView/index.tsx b/packages/react-native-sdk/src/components/Participant/FloatingParticipantView/index.tsx index 3a47646e41..a5fe5bb54e 100644 --- a/packages/react-native-sdk/src/components/Participant/FloatingParticipantView/index.tsx +++ b/packages/react-native-sdk/src/components/Participant/FloatingParticipantView/index.tsx @@ -66,6 +66,7 @@ const CustomLocalParticipantViewVideoFallback = () => { colors, floatingParticipantsView, variants: { iconSizes }, + defaults, }, } = useTheme(); @@ -73,12 +74,12 @@ const CustomLocalParticipantViewVideoFallback = () => { - + ); @@ -170,9 +171,7 @@ export const FloatingParticipantView = ({ style={[ styles.participantViewContainer, participantViewStyle, - { - shadowColor: colors.static_black, - }, + { shadowColor: colors.sheetPrimary }, floatingParticipantsView.participantViewContainer, ]} // video z order must be one above the one used in grid view @@ -192,7 +191,6 @@ export const FloatingParticipantView = ({ const styles = StyleSheet.create({ container: { - margin: 8, // Needed to make the view on top and draggable zIndex: Z_INDEX.IN_MIDDLE, flex: 1, 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 3244365c41..9f7c8894ab 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 { MicOff, @@ -12,6 +12,7 @@ import { ParticipantViewProps } from './ParticipantView'; import { Z_INDEX } from '../../../constants'; import { hasAudio, hasVideo } from '@stream-io/video-client'; import { useTheme } from '../../../contexts/ThemeContext'; +import SpeechIndicator from './SpeechIndicator'; /** * Props for the ParticipantLabel component. @@ -43,6 +44,7 @@ export const ParticipantLabel = ({ }, }, } = useTheme(); + const styles = useStyles(); const { name, userId, pin, sessionId, isLocalParticipant } = participant; const call = useCall(); const { t } = useI18n(); @@ -67,7 +69,7 @@ export const ParticipantLabel = ({ - + - - {participantLabel} - - {isAudioMuted && ( - - - - )} - {isVideoMuted && ( - - + + + {participantLabel} + + {isAudioMuted && ( + + + + )} + {isVideoMuted && ( + + + + )} + {isPinningEnabled && ( + + + + )} + + - )} - {isPinningEnabled && ( - - - - )} + ); }; -const styles = StyleSheet.create({ - container: { - flexDirection: 'row', - alignItems: 'center', - padding: 8, - borderRadius: 5, - flexShrink: 1, - zIndex: Z_INDEX.IN_FRONT, - }, - userNameLabel: { - flexShrink: 1, - }, - 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: 3, + fontSize: 13, + fontWeight: '400', + color: theme.colors.textPrimary, + }, + screenShareIconContainer: { + marginRight: theme.variants.spacingSizes.sm, + justifyContent: 'center', + }, + audioMutedIconContainer: { + marginLeft: theme.variants.spacingSizes.xs, + justifyContent: 'center', + }, + videoMutedIconContainer: { + marginLeft: theme.variants.spacingSizes.xs, + justifyContent: 'center', + }, + pinIconContainer: { + marginLeft: theme.variants.spacingSizes.xs, + justifyContent: 'center', + }, + }), + [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 e9438094fd..0103e49755 100644 --- a/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantNetworkQualityIndicator.tsx +++ b/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantNetworkQualityIndicator.tsx @@ -27,11 +27,11 @@ const useConnectionQualitySignalColors = ( switch (connectionQuality) { case SfuModels.ConnectionQuality.EXCELLENT: - return [colors.primary, colors.primary, colors.primary]; + return [colors.iconSuccess, colors.iconSuccess, colors.iconSuccess]; case SfuModels.ConnectionQuality.GOOD: - return [colors.primary, colors.primary, colors.static_white]; + return [colors.iconSuccess, colors.iconSuccess, colors.iconPrimary]; case SfuModels.ConnectionQuality.POOR: - return [colors.error, colors.static_white, colors.static_white]; + return [colors.iconWarning, colors.iconPrimary, colors.iconPrimary]; default: return null; } @@ -57,7 +57,7 @@ export const ParticipantNetworkQualityIndicator = ({ style={[ styles.container, { - backgroundColor: colors.static_overlay, + backgroundColor: colors.sheetOverlay, height: iconSizes.lg, width: iconSizes.lg, }, @@ -98,6 +98,6 @@ const styles = StyleSheet.create({ container: { zIndex: Z_INDEX.IN_FRONT, alignSelf: 'flex-end', - borderRadius: 5, + borderTopLeftRadius: 5, }, }); 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..053f2739ed 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,12 +32,9 @@ export const ParticipantReaction = ({ }: ParticipantReactionProps) => { const { reaction, sessionId } = participant; const call = useCall(); + const styles = useStyles(); const { - theme: { - typefaces, - variants: { iconSizes }, - participantReaction, - }, + theme: { typefaces, participantReaction }, } = useTheme(); useEffect(() => { @@ -60,26 +57,39 @@ export const ParticipantReaction = ({ ); return ( - - - {currentReaction?.icon} - - + currentReaction?.icon != null && ( + + + + {currentReaction?.icon} + + + + ) ); }; -const styles = StyleSheet.create({ - container: { - alignSelf: 'flex-start', - zIndex: Z_INDEX.IN_FRONT, - }, -}); +const useStyles = () => { + const { theme } = useTheme(); + return useMemo( + () => + StyleSheet.create({ + container: { + flex: 1, + zIndex: Z_INDEX.IN_FRONT, + }, + reaction: { + borderRadius: theme.variants.borderRadiusSizes.sm, + backgroundColor: theme.colors.sheetOverlay, + alignSelf: 'flex-end', + marginRight: theme.variants.spacingSizes.md, + marginTop: theme.variants.spacingSizes.md, + height: theme.variants.roundButtonSizes.md, + width: theme.variants.roundButtonSizes.md, + alignItems: 'center', + justifyContent: 'center', + }, + }), + [theme] + ); +}; diff --git a/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantVideoFallback.tsx b/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantVideoFallback.tsx index bacf63da1e..6a3ea79347 100644 --- a/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantVideoFallback.tsx +++ b/packages/react-native-sdk/src/components/Participant/ParticipantView/ParticipantVideoFallback.tsx @@ -29,18 +29,14 @@ export const ParticipantVideoFallback = ({ {!image ? ( )} - + {ParticipantLabel && ( )} @@ -160,20 +165,26 @@ export const ParticipantView = ({ ); }; -const styles = StyleSheet.create({ - container: { - justifyContent: 'space-between', - padding: 4, - overflow: 'hidden', - borderWidth: 2, - borderColor: 'transparent', - }, - footerContainer: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - }, - highligtedContainer: { - borderWidth: 2, - }, -}); +const useStyles = () => { + const { theme } = useTheme(); + return useMemo( + () => + StyleSheet.create({ + container: { + overflow: 'hidden', + justifyContent: 'flex-end', + borderRadius: theme.variants.borderRadiusSizes.md, + }, + footerContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + }, + highligtedContainer: { + borderWidth: 2, + }, + networkIndicatorOnly: { justifyContent: 'flex-end' }, + }), + [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 new file mode 100644 index 0000000000..fea409742a --- /dev/null +++ b/packages/react-native-sdk/src/components/Participant/ParticipantView/SpeechIndicator.tsx @@ -0,0 +1,103 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { View, Animated, StyleSheet } from 'react-native'; +import { useTheme } from '../../..'; + +/** + * 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 styles = useStyles(); + + const [animationValues] = useState(() => [ + new Animated.Value(0.6), + new Animated.Value(0.6), + new Animated.Value(0.6), + ]); + + 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: Animated.Value) => ({ + transform: [{ scaleY: animatedValue }], + }); + + return ( + + {animationValues.map((animatedValue, index) => ( + + ))} + + ); +}; + +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.iconSecondary, + borderRadius: 2, + }, + }), + [theme] + ); +}; + +export default SpeechIndicator; diff --git a/packages/react-native-sdk/src/components/utility/Avatar.tsx b/packages/react-native-sdk/src/components/utility/Avatar.tsx index 93804bbad9..a0e6e84f2e 100644 --- a/packages/react-native-sdk/src/components/utility/Avatar.tsx +++ b/packages/react-native-sdk/src/components/utility/Avatar.tsx @@ -69,9 +69,7 @@ export const Avatar = (props: AvatarProps) => { height: size, width: size, }, - { - backgroundColor: colors.primary, - }, + { backgroundColor: colors.primary }, avatar.container, styleProp?.container, ]} @@ -88,7 +86,7 @@ export const Avatar = (props: AvatarProps) => { { @@ -41,7 +41,7 @@ export const ScreenShareOverlay = ({}: ScreenShareOverlayProps) => { style={[ styles.text, typefaces.subtitleBold, - { color: colors.static_white }, + { color: colors.textPrimary }, screenshareOverlay.text, ]} > @@ -53,7 +53,7 @@ export const ScreenShareOverlay = ({}: ScreenShareOverlayProps) => { return [ styles.button, { - backgroundColor: colors.dark_gray, + backgroundColor: colors.sheetSecondary, opacity: pressed ? 0.2 : 1, }, screenshareOverlay.button, @@ -67,12 +67,12 @@ export const ScreenShareOverlay = ({}: ScreenShareOverlayProps) => { screenshareOverlay.buttonIcon, ]} > - + diff --git a/packages/react-native-sdk/src/constants/TestIds.ts b/packages/react-native-sdk/src/constants/TestIds.ts index 90ebe99448..676f133945 100644 --- a/packages/react-native-sdk/src/constants/TestIds.ts +++ b/packages/react-native-sdk/src/constants/TestIds.ts @@ -1,6 +1,7 @@ export enum IconTestIds { MUTED_VIDEO = 'muted-video-icon', HANG_UP_CALL = 'hang-up-call-icon', + PHONE = 'phone-icon', SCREEN_SHARE_INDICATOR = 'screen-share-indicator-icon', SCREEN_SHARE = 'screen-share-icon', } @@ -15,7 +16,6 @@ export enum ComponentTestIds { PARTICIPANTS_INFO = 'participants-info', PARTICIPANT_SCREEN_SHARING = 'participant-screen-sharing', REACTIONS_PICKER = 'reactions-picker', - CHAT_UNREAD_BADGE_COUNT_INDICATOR = 'chat-unread-badge-count-indicator', } export enum ButtonTestIds { diff --git a/packages/react-native-sdk/src/constants/index.ts b/packages/react-native-sdk/src/constants/index.ts index 42241a3ef0..079389c2af 100644 --- a/packages/react-native-sdk/src/constants/index.ts +++ b/packages/react-native-sdk/src/constants/index.ts @@ -6,9 +6,13 @@ export const FLOATING_VIDEO_VIEW_STYLE = { borderRadius: 10, }; -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 +20,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 +37,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/packages/react-native-sdk/src/icons/CameraSwitch.tsx b/packages/react-native-sdk/src/icons/CameraSwitch.tsx index 9839d7cffd..b99fc61b8e 100644 --- a/packages/react-native-sdk/src/icons/CameraSwitch.tsx +++ b/packages/react-native-sdk/src/icons/CameraSwitch.tsx @@ -4,12 +4,13 @@ import { ColorValue } from 'react-native/types'; type Props = { color: ColorValue; + size: number; }; -export const CameraSwitch = ({ color }: Props) => ( - +export const CameraSwitch = ({ color, size }: Props) => ( + 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, + }, +}); diff --git a/packages/react-native-sdk/src/icons/Lock.tsx b/packages/react-native-sdk/src/icons/Lock.tsx new file mode 100644 index 0000000000..d5c2e7a26d --- /dev/null +++ b/packages/react-native-sdk/src/icons/Lock.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; +}; + +export const Lock = ({ color, size }: Props) => { + return ( + + + + ); +}; diff --git a/packages/react-native-sdk/src/icons/Mic.tsx b/packages/react-native-sdk/src/icons/Mic.tsx index 229c63f7dc..684c5233cb 100644 --- a/packages/react-native-sdk/src/icons/Mic.tsx +++ b/packages/react-native-sdk/src/icons/Mic.tsx @@ -4,16 +4,13 @@ import { ColorValue } from 'react-native/types'; type Props = { color: ColorValue; + size: number; }; -export const Mic = ({ color }: Props) => ( - +export const Mic = ({ color, size }: Props) => ( + - diff --git a/packages/react-native-sdk/src/icons/MicOff.tsx b/packages/react-native-sdk/src/icons/MicOff.tsx index 405bc1c4ee..58da25b60e 100644 --- a/packages/react-native-sdk/src/icons/MicOff.tsx +++ b/packages/react-native-sdk/src/icons/MicOff.tsx @@ -4,12 +4,13 @@ import { ColorValue } from 'react-native/types'; type Props = { color: ColorValue; + size: number; }; -export const MicOff = ({ color }: Props) => ( - +export const MicOff = ({ color, size }: Props) => ( + diff --git a/packages/react-native-sdk/src/icons/Participants.tsx b/packages/react-native-sdk/src/icons/Participants.tsx deleted file mode 100644 index 82be4a3fe5..0000000000 --- a/packages/react-native-sdk/src/icons/Participants.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 Participants = ({ color }: Props) => ( - - - -); diff --git a/packages/react-native-sdk/src/icons/Phone.tsx b/packages/react-native-sdk/src/icons/Phone.tsx index a9cc9cff08..ea46da4fc0 100644 --- a/packages/react-native-sdk/src/icons/Phone.tsx +++ b/packages/react-native-sdk/src/icons/Phone.tsx @@ -1,15 +1,23 @@ import React from 'react'; import { Svg, Path } from 'react-native-svg'; import { ColorValue } from 'react-native/types'; +import { IconTestIds } from '../constants/TestIds'; type Props = { color: ColorValue; + size: number; }; -export const Phone = ({ color }: Props) => ( - +export const Phone = ({ 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..7504ee5207 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/PinVertical.tsx b/packages/react-native-sdk/src/icons/PinVertical.tsx index e00dd377db..1261319a46 100644 --- a/packages/react-native-sdk/src/icons/PinVertical.tsx +++ b/packages/react-native-sdk/src/icons/PinVertical.tsx @@ -4,10 +4,11 @@ import { ColorValue } from 'react-native/types'; type Props = { color: ColorValue; + size: number; }; -export const PinVertical = ({ color }: Props) => ( - +export const PinVertical = ({ color, size }: Props) => ( + ( - + { +export const ScreenShare = ({ color, size }: Props) => { return ( - + ); diff --git a/packages/react-native-sdk/src/icons/StopScreenShare.tsx b/packages/react-native-sdk/src/icons/StopScreenShare.tsx index e636b9852f..d9b68c2ffe 100644 --- a/packages/react-native-sdk/src/icons/StopScreenShare.tsx +++ b/packages/react-native-sdk/src/icons/StopScreenShare.tsx @@ -4,11 +4,12 @@ import { ColorValue } from 'react-native/types'; type Props = { color: ColorValue; + size: number; }; -export const StopScreenShare = ({ color }: Props) => { +export const StopScreenShare = ({ color, size }: Props) => { return ( - + ( - +export const Video = ({ color, size }: Props) => ( + diff --git a/packages/react-native-sdk/src/icons/VideoSlash.tsx b/packages/react-native-sdk/src/icons/VideoSlash.tsx index 6f583561e4..1b7d3471d1 100644 --- a/packages/react-native-sdk/src/icons/VideoSlash.tsx +++ b/packages/react-native-sdk/src/icons/VideoSlash.tsx @@ -5,12 +5,18 @@ import { IconTestIds } from '../constants/TestIds'; type Props = { color: ColorValue; + size: number; }; -export const VideoSlash = ({ color }: Props) => ( - +export const VideoSlash = ({ 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..f3b3ee722f 100644 --- a/packages/react-native-sdk/src/icons/index.tsx +++ b/packages/react-native-sdk/src/icons/index.tsx @@ -6,8 +6,6 @@ export * from './PhoneDown'; export * from './Settings'; export * from './Video'; export * from './VideoSlash'; -export * from './Chat'; -export * from './Participants'; export * from './ThreeDots'; export * from './PinVertical'; export * from './Spotlight'; @@ -21,3 +19,4 @@ export * from './StartStreamIcon'; export * from './StopScreenShare'; export * from './EndStreamIcon'; export * from './LeaveStreamIcon'; +export * from './IconWrapper'; diff --git a/packages/react-native-sdk/src/theme/colors.ts b/packages/react-native-sdk/src/theme/colors.ts index 813aa54421..8788543e23 100644 --- a/packages/react-native-sdk/src/theme/colors.ts +++ b/packages/react-native-sdk/src/theme/colors.ts @@ -1,49 +1,34 @@ import { palette } from './constants'; +import { ColorScheme } from './types'; -const opacityToHex = (opacity: number) => { - return Math.round(opacity * 255) - .toString(16) - .padStart(2, '0'); -}; +const colors: ColorScheme = { + primary: palette.primary100, + secondary: palette.neutral90, + success: palette.success100, + warning: palette.warning100, + + buttonPrimary: palette.primary100, + buttonSecondary: palette.neutral90, + buttonSuccess: palette.success100, + buttonWarning: palette.warning100, + buttonDisabled: palette.primary180, + + iconPrimary: palette.neutral0, + iconSecondary: palette.primary100, + iconSuccess: palette.success100, + iconWarning: palette.warning100, + + sheetPrimary: palette.neutral120, + sheetSecondary: palette.neutral110, + sheetTertiary: palette.neutral90, + sheetOverlay: palette.overlay, -const colors = { - primary: palette.blue500, - error: palette.red400, - info: palette.green500, - static_black: palette.grey950, - static_white: palette.grey50, - static_overlay: palette.grey950 + opacityToHex(0.85), - static_grey: palette.grey700, - disabled: palette.grey600, - text_low_emphasis: palette.grey500, - text_high_emphasis: palette.grey950, - controls_bg: palette.grey50, - borders: palette.grey300, - overlay: palette.grey950 + opacityToHex(0.4), - overlay_dark: palette.grey950 + opacityToHex(0.6), - bars: palette.grey50, - content_bg: palette.grey950 + opacityToHex(0.05), - dark_gray: palette.grey800, + textPrimary: palette.neutral0, + textSecondary: palette.neutral30, }; -const darkThemeColors = { - primary: palette.blue500, - error: palette.red400, - info: palette.green500, - static_black: palette.grey950, - static_white: palette.grey50, - static_overlay: palette.grey950 + opacityToHex(0.85), - static_grey: palette.grey700, - disabled: palette.grey600, - text_low_emphasis: palette.grey500, - text_high_emphasis: palette.grey50, - controls_bg: palette.grey900, - borders: palette.grey700, - overlay: palette.grey950 + opacityToHex(0.4), - overlay_dark: palette.grey50 + opacityToHex(0.6), - bars: palette.grey900, - content_bg: palette.grey950 + opacityToHex(0.05), - dark_gray: palette.grey800, +const colorPalette = { + colors, }; -export { colors, darkThemeColors }; +export { colorPalette, colors }; diff --git a/packages/react-native-sdk/src/theme/constants.ts b/packages/react-native-sdk/src/theme/constants.ts index 1d9dafe176..af8eb9cf62 100644 --- a/packages/react-native-sdk/src/theme/constants.ts +++ b/packages/react-native-sdk/src/theme/constants.ts @@ -1,49 +1,56 @@ const ref = { palette: { - green50: '#F6FEF9', - green100: '#E9F1FF', - green200: '#A6F2C6', - green300: '#79ECA9', - green400: '#4CE68C', - green500: '#20E070', - green600: '#19B359', - green700: '#138643', - green800: '#0D592C', - green900: '#062D16', - green950: '#041B0D', - blue50: '#F5FAFF', - blue100: '#E0F0FF', - blue200: '#CCDFFF', - blue300: '#669FFF', - blue400: '#337EFF', - blue500: '#005FFF', - blue600: '#004CCC', - blue700: '#003999', - blue800: '#002666', - blue900: '#00163D', - blue950: '#000D24', - red50: '#FFF5F5', - red100: '#FFE5E7', - red200: '#FF999F', - red300: '#FF666E', - red400: '#FF3742', - red500: '#FF000E', - red600: '#CC000B', - red700: '#990008', - red800: '#660006', - red900: '#330003', - red950: '#1F0002', - grey50: '#FFFFFF', - grey100: '#F7F7F8', - grey200: '#E9EAED', - grey300: '#DBDDE1', - grey400: '#B4B7BB', - grey500: '#72767E', - grey600: '#4C525C', - grey700: '#272A30', - grey800: '#1C1E22', - grey900: '#121416', - grey950: '#080707', + primary100: '#005fff', + primary150: '#123d82', + primary180: '#1b2c43', + primary20: '#ccdfff', + primary70: '#4c8fff', + + secondary100: '#69e5f6', + secondary140: '#448592', + secondary180: '#263942', + secondary20: '#f0fcfe', + secondary60: '#a5effa', + + tertiary100: '#b38af8', + tertiary170: '#4b446b', + tertiary190: '#2d3042', + tertiary20: '#f7f3fe', + tertiary60: '#d1b9fb', + + success100: '#00e2a1', + success150: '#12715c', + success190: '#1d2f34', + success20: '#e5fcf6', + success60: '#66eec7', + + warning100: '#dc433b', + warning150: '#7d3535', + warning190: '#31292f', + warning20: '#f8d9d8', + warning70: '#e77b76', + + caution100: '#ffd646', + caution150: '#786c38', + caution180: '#353830', + caution20: '#fffbec', + caution60: '#ffe690', + + neutral0: '#eff0f1', + neutral10: '#e3e4e5', + neutral100: '#0d1721', + neutral110: '#101213', + neutral120: '#000000', + neutral20: '#caccce', + neutral30: '#b0b4b7', + neutral40: '#979ca0', + neutral50: '#7e8389', + neutral60: '#656b72', + neutral70: '#4c535b', + neutral80: '#323b44', + neutral90: '#19232d', + + overlay: '#0c0d0ea6', }, }; diff --git a/packages/react-native-sdk/src/theme/index.ts b/packages/react-native-sdk/src/theme/index.ts index a67a95daf5..50fd821aed 100644 --- a/packages/react-native-sdk/src/theme/index.ts +++ b/packages/react-native-sdk/src/theme/index.ts @@ -1,9 +1,2 @@ -import { colors, darkThemeColors } from './colors'; -import { Theme } from './types'; - -export const colorPallet: Theme = { - light: colors, - dark: darkThemeColors, -}; - export * from './theme'; +export * from './colors'; diff --git a/packages/react-native-sdk/src/theme/theme.ts b/packages/react-native-sdk/src/theme/theme.ts index 2d3008688f..dc50bdba9f 100644 --- a/packages/react-native-sdk/src/theme/theme.ts +++ b/packages/react-native-sdk/src/theme/theme.ts @@ -1,32 +1,38 @@ import { ImageStyle, TextStyle, ViewStyle } from 'react-native/types'; import { colors } from './colors'; -import { ColorScheme, FontStyle, FontTypes } from './types'; +import { + ColorScheme, + DimensionType, + FontStyle, + FontTypes, + Insets, +} from './types'; +import { ColorValue } from 'react-native'; export type Theme = { variants: { - buttonSizes: { - xs: number; - sm: number; - md: number; - lg: number; - xl: number; - }; - iconSizes: { - xs: number; - sm: number; - md: number; - lg: number; - xl: number; - }; - avatarSizes: { - xs: number; - sm: number; - md: number; - lg: number; - xl: number; - }; + buttonSizes: DimensionType; + roundButtonSizes: DimensionType; + iconSizes: DimensionType; + avatarSizes: DimensionType; + fontSizes: DimensionType; + spacingSizes: DimensionType; + borderRadiusSizes: DimensionType; + insets: Insets; }; typefaces: Record; + defaults: { + color: ColorValue; + backgroundColor: ColorValue; + margin: number; + padding: number; + fontSize: number; + iconSize: number; + fontWeight: TextStyle['fontWeight']; + borderRadius: ViewStyle['borderRadius']; + borderColor: ColorValue; + borderWidth: ViewStyle['borderWidth']; + }; colors: ColorScheme; avatar: { container: ViewStyle; @@ -171,15 +177,6 @@ export type Theme = { buttonGroup: ViewStyle; deviceControlButtons: ViewStyle; }; - callTopView: { - container: ViewStyle; - content: ViewStyle; - backIconContainer: ViewStyle; - leftElement: ViewStyle; - centerElement: ViewStyle; - rightElement: ViewStyle; - title: TextStyle; - }; userInfo: { container: ViewStyle; avatarGroup: ViewStyle; @@ -279,10 +276,27 @@ export type Theme = { buttonIcon: ViewStyle; buttonText: TextStyle; }; + + // Index signature for additional dynamic properties + [component: string]: any; }; export const defaultTheme: Theme = { variants: { + roundButtonSizes: { + xs: 16, + sm: 24, + md: 36, + lg: 44, + xl: 56, + }, + borderRadiusSizes: { + xs: 4, + sm: 8, + md: 16, + lg: 24, + xl: 32, + }, buttonSizes: { xs: 40, sm: 50, @@ -304,6 +318,26 @@ export const defaultTheme: Theme = { lg: 160, xl: 180, }, + spacingSizes: { + xs: 4, + sm: 8, + md: 16, + lg: 24, + xl: 32, + }, + fontSizes: { + xs: 8, + sm: 12, + md: 16, + lg: 20, + xl: 24, + }, + insets: { + top: 0, + right: 0, + bottom: 0, + left: 0, + }, }, typefaces: { heading4: { @@ -335,6 +369,18 @@ export const defaultTheme: Theme = { fontWeight: '400', }, }, + defaults: { + color: colors.primary, + backgroundColor: colors.sheetPrimary, + margin: 10, + padding: 10, + fontSize: 16, + fontWeight: '500', + borderRadius: 32, + iconSize: 28, + borderColor: colors.buttonPrimary, + borderWidth: 1, + }, colors: colors, avatar: { container: {}, @@ -483,15 +529,6 @@ export const defaultTheme: Theme = { deviceControlButtons: {}, }, ringingCallContent: { container: {} }, - callTopView: { - container: {}, - content: {}, - backIconContainer: {}, - leftElement: {}, - centerElement: {}, - rightElement: {}, - title: {}, - }, userInfo: { container: {}, avatarGroup: {}, diff --git a/packages/react-native-sdk/src/theme/types.ts b/packages/react-native-sdk/src/theme/types.ts index 54348cc1a1..add9ab00f6 100644 --- a/packages/react-native-sdk/src/theme/types.ts +++ b/packages/react-native-sdk/src/theme/types.ts @@ -1,30 +1,39 @@ import { ColorValue, TextStyle } from 'react-native'; -// TODO: check if this is used somewhere and remove if not +/** + * ColorScheme defines the complete color palette for the application's theme. + * It provides a centralized type definition for maintaining consistent colors + * across different UI components and contexts. + */ export type ColorScheme = { primary: ColorValue; - error: ColorValue; - info: ColorValue; - static_black: ColorValue; - static_white: ColorValue; - static_overlay: ColorValue; - static_grey: ColorValue; - disabled: ColorValue; - text_low_emphasis: ColorValue; - text_high_emphasis: ColorValue; - controls_bg: ColorValue; - borders: ColorValue; - overlay: ColorValue; - overlay_dark: ColorValue; - bars: ColorValue; - content_bg: ColorValue; - dark_gray: ColorValue; + secondary: ColorValue; + success: ColorValue; + warning: ColorValue; + + buttonPrimary: ColorValue; + buttonSecondary: ColorValue; + buttonSuccess: ColorValue; + buttonWarning: ColorValue; + buttonDisabled: ColorValue; + + iconPrimary: ColorValue; + iconSecondary: ColorValue; + iconSuccess: ColorValue; + iconWarning: ColorValue; + + sheetPrimary: ColorValue; + sheetSecondary: ColorValue; + sheetTertiary: ColorValue; + sheetOverlay: ColorValue; + + textPrimary: ColorValue; + textSecondary: ColorValue; + // allow any other color [key: string]: ColorValue; }; -export type ColorType = Record<'light' | 'dark', ColorScheme>; - export type FontTypes = | 'heading4' | 'heading5' @@ -38,6 +47,48 @@ export type FontStyle = { fontWeight: TextStyle['fontWeight']; }; -export type FontsScheme = Record; +/** + * DimensionType defines a set of standardized size values for component scaling. + * Each property represents a size tier from extra small (xs) to extra large (xl). + * + * @property xs - Extra small size (typically used for minimal spacing or compact elements) + * @property sm - Small size (used for tight but readable spacing) + * @property md - Medium size (default size for most components) + * @property lg - Large size (used for emphasized or prominent elements) + * @property xl - Extra large size (used for maximum emphasis or touch targets) + * + * Common use cases: + * - Padding and margin values + * - Icon sizes + * - Button dimensions + * - Component spacing + */ +export type DimensionType = { + xs: number; + sm: number; + md: number; + lg: number; + xl: number; +}; -export type Theme = ColorType; +/** + * Insets represent spacing measurements for the four edges of a component or screen. + * + * @property top - Distance from the upper edge (e.g., status bar, notch) + * @property right - Distance from the right edge (e.g., curved screen edges) + * @property bottom - Distance from the bottom edge (e.g., home indicator, navigation bar) + * @property left - Distance from the left edge (e.g., curved screen edges) + * + * Common use cases: + * - Safe area padding to avoid device UI elements + * - Component internal padding + * - Layout margin spacing + */ +export type Insets = { + top: number; + right: number; + bottom: number; + left: number; +}; + +export type FontsScheme = Record; diff --git a/packages/react-native-sdk/src/translations/en.json b/packages/react-native-sdk/src/translations/en.json index 95888d78f4..c64f028136 100644 --- a/packages/react-native-sdk/src/translations/en.json +++ b/packages/react-native-sdk/src/translations/en.json @@ -11,7 +11,7 @@ "End Stream": "End Stream", "Leave Stream": "Leave Stream", "Live": "Live", - "Before Joining": "Before Joining", + "Before joining": "Before joining", "Setup your audio and video": "Setup your audio and video", "Setup your audio": "Setup your audio", "You are first to Join the call.": "You are first to Join the call.", @@ -23,5 +23,9 @@ "{{ numberOfParticipants }} participant(s) are in the call.": "{{ numberOfParticipants }} participant(s) are in the call.", "You are about to join a call with id {{ callId }}.": "You are about to join a call with id {{ callId }}.", "Preparing call": "Preparing call", - "You have left the call": "You have left the call" -} + "You have left the call": "You have left the call", + "You are about to join a call.": "You are about to join a call.", + "Currently there are no other participants in the call.": "Currently there are no other participants in the call.", + "There is {{numberOfParticipants}} more person in the call.": "There is {{numberOfParticipants}} more person in the call.", + "There are {{numberOfParticipants}} more people in the call.": "There are {{numberOfParticipants}} more people in the call." +} \ No newline at end of file diff --git a/sample-apps/react-native/dogfood/App.tsx b/sample-apps/react-native/dogfood/App.tsx index 6d5a3c295a..80b92b2097 100755 --- a/sample-apps/react-native/dogfood/App.tsx +++ b/sample-apps/react-native/dogfood/App.tsx @@ -12,7 +12,10 @@ import { useAppGlobalStoreSetState, useAppGlobalStoreValue, } from './src/contexts/AppContext'; -import { SafeAreaProvider } from 'react-native-safe-area-context'; +import { + SafeAreaProvider, + useSafeAreaInsets, +} from 'react-native-safe-area-context'; import { navigationRef, StaticNavigationService, @@ -29,14 +32,16 @@ import { setPushConfig } from './src/utils/setPushConfig'; import { useSyncPermissions } from './src/hooks/useSyncPermissions'; import { NavigationHeader } from './src/components/NavigationHeader'; import { GestureHandlerRootView } from 'react-native-gesture-handler'; -import { LogBox, StyleSheet } from 'react-native'; +import { LogBox } from 'react-native'; import { LiveStream } from './src/navigators/Livestream'; import { REACT_NATIVE_DOGFOOD_APP_ENVIRONMENT } from '@env'; import PushNotificationIOS from '@react-native-community/push-notification-ios'; import { + defaultTheme, isPushNotificationiOSStreamVideoEvent, onPushNotificationiOSStreamVideoEvent, } from '@stream-io/video-react-native-sdk'; +import { appTheme } from './src/theme'; // only enable warning and error logs from webrtc library Logger.enable(`${Logger.ROOT_PREFIX}:(WARN|ERROR)`); @@ -58,6 +63,12 @@ const StackNavigator = () => { const userImageUrl = useAppGlobalStoreValue((store) => store.userImageUrl); const userName = useAppGlobalStoreValue((store) => store.userName); const setState = useAppGlobalStoreSetState(); + const { bottom } = useSafeAreaInsets(); + const themeMode = useAppGlobalStoreValue((store) => store.themeMode); + const color = + themeMode === 'light' + ? appTheme.colors.static_white + : defaultTheme.colors.sheetPrimary; useProntoLinkEffect(); useSyncPermissions(); @@ -146,8 +157,14 @@ const StackNavigator = () => { return ; } + const containerStyle = { + flex: 1, + paddingBottom: bottom, + backgroundColor: color, + }; + return ( - + {mode} @@ -168,9 +185,3 @@ export default function App() { ); } - -const styles = StyleSheet.create({ - container: { - flex: 1, - }, -}); diff --git a/sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample.xcodeproj/project.pbxproj b/sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample.xcodeproj/project.pbxproj index b1cec804a4..6c72c76101 100644 --- a/sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample.xcodeproj/project.pbxproj +++ b/sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample.xcodeproj/project.pbxproj @@ -714,6 +714,7 @@ SWIFT_OBJC_BRIDGING_HEADER = "StreamReactNativeVideoSDKSample-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Debug; @@ -819,6 +820,7 @@ PROVISIONING_PROFILE_SPECIFIER = "match AppStore io.getstream.rnvideosample"; SWIFT_OBJC_BRIDGING_HEADER = "StreamReactNativeVideoSDKSample-Bridging-Header.h"; SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; }; name = Release; @@ -911,6 +913,7 @@ ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "$(inherited) DEBUG"; USE_HERMES = true; }; @@ -999,6 +1002,7 @@ ); REACT_NATIVE_PATH = "${PODS_ROOT}/../../node_modules/react-native"; SDKROOT = iphoneos; + TARGETED_DEVICE_FAMILY = "1,2"; USE_HERMES = true; VALIDATE_PRODUCT = YES; }; diff --git a/sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample/Images.xcassets/AppIcon.appiconset/114.png b/sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample/Images.xcassets/AppIcon.appiconset/114.png new file mode 100644 index 0000000000..bc0d7ad5b6 Binary files /dev/null and b/sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample/Images.xcassets/AppIcon.appiconset/114.png differ diff --git a/sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample/Images.xcassets/AppIcon.appiconset/120-1.png b/sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample/Images.xcassets/AppIcon.appiconset/120 1.png similarity index 100% rename from sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample/Images.xcassets/AppIcon.appiconset/120-1.png rename to sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample/Images.xcassets/AppIcon.appiconset/120 1.png diff --git a/sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample/Images.xcassets/AppIcon.appiconset/128.png b/sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample/Images.xcassets/AppIcon.appiconset/128.png new file mode 100644 index 0000000000..502b6c1529 Binary files /dev/null and b/sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample/Images.xcassets/AppIcon.appiconset/128.png differ diff --git a/sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample/Images.xcassets/AppIcon.appiconset/136.png b/sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample/Images.xcassets/AppIcon.appiconset/136.png new file mode 100644 index 0000000000..1a29b0af13 Binary files /dev/null and b/sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample/Images.xcassets/AppIcon.appiconset/136.png differ diff --git a/sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample/Images.xcassets/AppIcon.appiconset/152 1.png b/sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample/Images.xcassets/AppIcon.appiconset/152 1.png new file mode 100644 index 0000000000..6f1a971ac2 Binary files /dev/null and b/sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample/Images.xcassets/AppIcon.appiconset/152 1.png differ diff --git a/sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample/Images.xcassets/AppIcon.appiconset/167 1.png b/sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample/Images.xcassets/AppIcon.appiconset/167 1.png new file mode 100644 index 0000000000..da558c4f83 Binary files /dev/null and b/sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample/Images.xcassets/AppIcon.appiconset/167 1.png differ diff --git a/sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample/Images.xcassets/AppIcon.appiconset/192.png b/sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample/Images.xcassets/AppIcon.appiconset/192.png new file mode 100644 index 0000000000..8826fd301d Binary files /dev/null and b/sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample/Images.xcassets/AppIcon.appiconset/192.png differ diff --git a/sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample/Images.xcassets/AppIcon.appiconset/76.png b/sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample/Images.xcassets/AppIcon.appiconset/76.png new file mode 100644 index 0000000000..a7d0c63e58 Binary files /dev/null and b/sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample/Images.xcassets/AppIcon.appiconset/76.png differ diff --git a/sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample/Images.xcassets/AppIcon.appiconset/Contents.json b/sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample/Images.xcassets/AppIcon.appiconset/Contents.json index 6a1c494ce1..50f1e3d96a 100644 --- a/sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample/Images.xcassets/AppIcon.appiconset/Contents.json +++ b/sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample/Images.xcassets/AppIcon.appiconset/Contents.json @@ -2,56 +2,113 @@ "images" : [ { "filename" : "40.png", - "idiom" : "iphone", + "idiom" : "universal", + "platform" : "ios", "scale" : "2x", "size" : "20x20" }, { "filename" : "60.png", - "idiom" : "iphone", + "idiom" : "universal", + "platform" : "ios", "scale" : "3x", "size" : "20x20" }, { "filename" : "58.png", - "idiom" : "iphone", + "idiom" : "universal", + "platform" : "ios", "scale" : "2x", "size" : "29x29" }, { "filename" : "87.png", - "idiom" : "iphone", + "idiom" : "universal", + "platform" : "ios", "scale" : "3x", "size" : "29x29" }, + { + "filename" : "76.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "38x38" + }, + { + "filename" : "114.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "38x38" + }, { "filename" : "80.png", - "idiom" : "iphone", + "idiom" : "universal", + "platform" : "ios", "scale" : "2x", "size" : "40x40" }, { - "filename" : "120.png", - "idiom" : "iphone", + "filename" : "120 1.png", + "idiom" : "universal", + "platform" : "ios", "scale" : "3x", "size" : "40x40" }, { - "filename" : "120-1.png", - "idiom" : "iphone", + "filename" : "120.png", + "idiom" : "universal", + "platform" : "ios", "scale" : "2x", "size" : "60x60" }, { "filename" : "180.png", - "idiom" : "iphone", + "idiom" : "universal", + "platform" : "ios", "scale" : "3x", "size" : "60x60" }, + { + "filename" : "128.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "64x64" + }, + { + "filename" : "192.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "3x", + "size" : "64x64" + }, + { + "filename" : "136.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "68x68" + }, + { + "filename" : "152 1.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "76x76" + }, + { + "filename" : "167 1.png", + "idiom" : "universal", + "platform" : "ios", + "scale" : "2x", + "size" : "83.5x83.5" + }, { "filename" : "appstore.png", - "idiom" : "ios-marketing", - "scale" : "1x", + "idiom" : "universal", + "platform" : "ios", "size" : "1024x1024" } ], diff --git a/sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample/Info.plist b/sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample/Info.plist index 3174b6d91b..567dc27048 100644 --- a/sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample/Info.plist +++ b/sample-apps/react-native/dogfood/ios/StreamReactNativeVideoSDKSample/Info.plist @@ -93,9 +93,10 @@ UISupportedInterfaceOrientations + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIInterfaceOrientationPortrait UIViewControllerBasedStatusBarAppearance diff --git a/sample-apps/react-native/dogfood/src/assets/Audio.tsx b/sample-apps/react-native/dogfood/src/assets/Audio.tsx new file mode 100644 index 0000000000..63d4c7a526 --- /dev/null +++ b/sample-apps/react-native/dogfood/src/assets/Audio.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 Audio = ({ color, size }: Props) => ( + + + +); + +export default Audio; diff --git a/sample-apps/react-native/dogfood/src/assets/CallDuration.tsx b/sample-apps/react-native/dogfood/src/assets/CallDuration.tsx new file mode 100644 index 0000000000..e98dd0225e --- /dev/null +++ b/sample-apps/react-native/dogfood/src/assets/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/sample-apps/react-native/dogfood/src/assets/Chat.tsx b/sample-apps/react-native/dogfood/src/assets/Chat.tsx index adc7b30f55..c4792835d8 100644 --- a/sample-apps/react-native/dogfood/src/assets/Chat.tsx +++ b/sample-apps/react-native/dogfood/src/assets/Chat.tsx @@ -4,13 +4,16 @@ import { ColorValue } from 'react-native/types'; type Props = { color: ColorValue; + size: number; }; -export const Chat = ({ color }: Props) => ( - +const Chat = ({ color, size }: Props) => ( + ); + +export default Chat; diff --git a/sample-apps/react-native/dogfood/src/assets/Close.tsx b/sample-apps/react-native/dogfood/src/assets/Close.tsx new file mode 100644 index 0000000000..192671469a --- /dev/null +++ b/sample-apps/react-native/dogfood/src/assets/Close.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 Close = ({ color, size }: Props) => ( + + + +); + +export default Close; 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..642f68cff3 --- /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/Effects.tsx b/sample-apps/react-native/dogfood/src/assets/Effects.tsx new file mode 100644 index 0000000000..820aa95125 --- /dev/null +++ b/sample-apps/react-native/dogfood/src/assets/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/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..23a661ea0b --- /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/Grid.tsx b/sample-apps/react-native/dogfood/src/assets/Grid.tsx new file mode 100644 index 0000000000..6b6e09b8b8 --- /dev/null +++ b/sample-apps/react-native/dogfood/src/assets/Grid.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { Svg, Path } from 'react-native-svg'; +import { ColorValue } from 'react-native/types'; + +type Props = { + color: ColorValue; + size: number; +}; + +export const Grid = ({ color, size }: Props) => ( + + + +); diff --git a/sample-apps/react-native/dogfood/src/assets/LightDark.tsx b/sample-apps/react-native/dogfood/src/assets/LightDark.tsx new file mode 100644 index 0000000000..f48ee64648 --- /dev/null +++ b/sample-apps/react-native/dogfood/src/assets/LightDark.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 LightDark = ({ color, size }: Props) => ( + + + +); + +export default LightDark; diff --git a/packages/react-native-sdk/src/icons/Chat.tsx b/sample-apps/react-native/dogfood/src/assets/LiveStreamChat.tsx similarity index 91% rename from packages/react-native-sdk/src/icons/Chat.tsx rename to sample-apps/react-native/dogfood/src/assets/LiveStreamChat.tsx index adc7b30f55..2c7073acea 100644 --- a/packages/react-native-sdk/src/icons/Chat.tsx +++ b/sample-apps/react-native/dogfood/src/assets/LiveStreamChat.tsx @@ -6,7 +6,7 @@ type Props = { color: ColorValue; }; -export const Chat = ({ color }: Props) => ( +export const LiveStreamChat = ({ color }: Props) => ( ( + + + +); + +export default MoreActions; 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..7263a99be4 --- /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/Participants.tsx b/sample-apps/react-native/dogfood/src/assets/Participants.tsx new file mode 100644 index 0000000000..02b95ca1c0 --- /dev/null +++ b/sample-apps/react-native/dogfood/src/assets/Participants.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 Participants = ({ color, size }: Props) => ( + + + +); + +export default Participants; 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..1cc6b52cde --- /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/assets/RecordCall.tsx b/sample-apps/react-native/dogfood/src/assets/RecordCall.tsx new file mode 100644 index 0000000000..736609bbf5 --- /dev/null +++ b/sample-apps/react-native/dogfood/src/assets/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/sample-apps/react-native/dogfood/src/assets/Spotlight.tsx b/sample-apps/react-native/dogfood/src/assets/Spotlight.tsx new file mode 100644 index 0000000000..a360b5edcd --- /dev/null +++ b/sample-apps/react-native/dogfood/src/assets/Spotlight.tsx @@ -0,0 +1,17 @@ +import React from 'react'; +import { Svg, Path } from 'react-native-svg'; +import { ColorValue } from 'react-native/types'; + +type Props = { + color: ColorValue; + size: number; +}; + +export const SpotLight = ({ color, size }: Props) => ( + + + +); diff --git a/sample-apps/react-native/dogfood/src/assets/Star.tsx b/sample-apps/react-native/dogfood/src/assets/Star.tsx new file mode 100644 index 0000000000..1ebde674e8 --- /dev/null +++ b/sample-apps/react-native/dogfood/src/assets/Star.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 Star = ({ color, size }: Props) => ( + + + +); + +export default Star; 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..213762aa42 --- /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/assets/feedbackLogo.png b/sample-apps/react-native/dogfood/src/assets/feedbackLogo.png new file mode 100644 index 0000000000..477432050b Binary files /dev/null and b/sample-apps/react-native/dogfood/src/assets/feedbackLogo.png differ diff --git a/sample-apps/react-native/dogfood/src/components/ActiveCall.tsx b/sample-apps/react-native/dogfood/src/components/ActiveCall.tsx index d739b28a83..b7d5847fc5 100644 --- a/sample-apps/react-native/dogfood/src/components/ActiveCall.tsx +++ b/sample-apps/react-native/dogfood/src/components/ActiveCall.tsx @@ -1,39 +1,43 @@ -import React, { useCallback, useEffect, useState } from 'react'; +import React, { useCallback, useEffect, useState, useMemo } from 'react'; import { useCall, CallContent, - CallControlProps, + useTheme, } from '@stream-io/video-react-native-sdk'; - -import { ActivityIndicator, StyleSheet } from 'react-native'; -import { SafeAreaView } from 'react-native-safe-area-context'; +import { ActivityIndicator, StatusBar, StyleSheet, View } from 'react-native'; import { ParticipantsInfoList } from './ParticipantsInfoList'; -import { - CallControlsComponent, - CallControlsComponentProps, -} from './CallControlsComponent'; +import { BottomControls } from './CallControlls/BottomControls'; import { useOrientation } from '../hooks/useOrientation'; +import { Z_INDEX } from '../constants'; +import { TopControls } from './CallControlls/TopControls'; +import { useLayout } from '../contexts/LayoutContext'; +import { useToggleCallRecording } from '@stream-io/video-react-bindings'; +import { useAppGlobalStoreValue } from '../contexts/AppContext'; -type ActiveCallProps = CallControlsComponentProps & { - onBackPressed?: () => void; +type ActiveCallProps = { + onHangupCallHandler?: () => void; onCallEnded: () => void; + onChatOpenHandler: () => void; + unreadCountIndicator: number; }; export const ActiveCall = ({ onChatOpenHandler, - onBackPressed, - onCallEnded, onHangupCallHandler, + onCallEnded, unreadCountIndicator, }: ActiveCallProps) => { const [isCallParticipantsVisible, setIsCallParticipantsVisible] = useState(false); const call = useCall(); const currentOrientation = useOrientation(); + const styles = useStyles(); + const { selectedLayout } = useLayout(); + const themeMode = useAppGlobalStoreValue((store) => store.themeMode); - const onOpenCallParticipantsInfo = () => { + const onOpenCallParticipantsInfo = useCallback(() => { setIsCallParticipantsVisible(true); - }; + }, []); useEffect(() => { return call?.on('call.ended', () => { @@ -41,42 +45,112 @@ export const ActiveCall = ({ }); }, [call, onCallEnded]); - const CustomControlsComponent = useCallback( - ({ landscape }: CallControlProps) => { - return ( - - ); - }, - [onChatOpenHandler, onHangupCallHandler, unreadCountIndicator], - ); + const { toggleCallRecording, isAwaitingResponse, isCallRecordingInProgress } = + useToggleCallRecording(); + + const CustomBottomControls = useCallback(() => { + return ( + + ); + }, [ + onChatOpenHandler, + onOpenCallParticipantsInfo, + unreadCountIndicator, + toggleCallRecording, + isAwaitingResponse, + isCallRecordingInProgress, + ]); + + const CustomTopControls = useCallback(() => { + return ( + + ); + }, [isAwaitingResponse, isCallRecordingInProgress, onHangupCallHandler]); if (!call) { return ; } return ( - + + + - + ); }; -const styles = StyleSheet.create({ - container: { - flex: 1, - }, -}); +const useStyles = () => { + const { theme } = useTheme(); + return useMemo( + () => + StyleSheet.create({ + container: { + flex: 1, + paddingTop: theme.variants.insets.top, + backgroundColor: theme.colors.sheetPrimary, + }, + callContent: { flex: 1 }, + topUnsafeArea: { + position: 'absolute', + top: 0, + left: 0, + right: 0, + height: theme.variants.insets.top, + backgroundColor: theme.colors.sheetPrimary, + zIndex: Z_INDEX.IN_FRONT, + }, + bottomUnsafeArea: { + position: 'absolute', + left: 0, + right: 0, + bottom: 0, + 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, + }, + }), + [theme], + ); +}; diff --git a/sample-apps/react-native/dogfood/src/components/BottomControlsDrawer.tsx b/sample-apps/react-native/dogfood/src/components/BottomControlsDrawer.tsx new file mode 100644 index 0000000000..f6f30d6b9d --- /dev/null +++ b/sample-apps/react-native/dogfood/src/components/BottomControlsDrawer.tsx @@ -0,0 +1,316 @@ +import { + SendReactionRequest, + useCall, + 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, + View, + Text, + TouchableOpacity, + FlatList, + StyleSheet, + SafeAreaView, + Animated, + TouchableWithoutFeedback, + Dimensions, + PanResponder, + Easing, +} from 'react-native'; +import { BOTTOM_CONTROLS_HEIGHT } from '../constants'; +import RaiseHand from '../assets/RaiseHand'; + +export type DrawerOption = { + id: string; + label: string; + icon?: JSX.Element; + onPress: () => void; +}; + +type DrawerProps = { + isVisible: boolean; + onClose: () => void; + options: DrawerOption[]; +}; + +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 call = useCall(); + + // negative offset is needed so the drawer component start above the bottom controls + const offset = -BOTTOM_CONTROLS_HEIGHT; + + 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, SNAP_BOTTOM, SNAP_TOP, translateY]); + + 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); + }; + + const dragIndicator = ( + + + + ); + + const emojiReactions = ( + + {defaultEmojiReactions.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 ( + + + + + + {dragIndicator} + {emojiReactions} + {raiseHand} + {otherButtons} + + + + + + ); +}; + +const useStyles = () => { + const { + theme: { colors, variants }, + } = useTheme(); + return useMemo( + () => + StyleSheet.create({ + overlay: { + flex: 1, + justifyContent: 'flex-end', + }, + safeArea: { + flex: 1, + justifyContent: 'flex-end', + }, + container: { + backgroundColor: colors.sheetPrimary, + borderTopLeftRadius: variants.borderRadiusSizes.lg, + borderTopRightRadius: variants.borderRadiusSizes.lg, + padding: variants.spacingSizes.md, + maxHeight: '80%', + maxWidth: 500, + }, + dragIndicator: { + width: '100%', + height: variants.spacingSizes.xs, + alignItems: 'center', + justifyContent: 'center', + marginBottom: variants.spacingSizes.md, + }, + dragIndicatorBar: { + width: 36, + height: 5, + backgroundColor: colors.buttonSecondary, + borderRadius: 2, + }, + emojiContainer: { + width: variants.roundButtonSizes.lg, + height: variants.roundButtonSizes.lg, + padding: variants.spacingSizes.xs, + borderRadius: variants.borderRadiusSizes.lg, + backgroundColor: colors.buttonSecondary, + 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, + borderColor: colors.sheetTertiary, + borderRadius: variants.borderRadiusSizes.lg, + paddingHorizontal: variants.spacingSizes.md, + height: variants.roundButtonSizes.lg, + backgroundColor: colors.buttonSecondary, + marginBottom: variants.spacingSizes.xs, + }, + raiseHand: { + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'center', + borderWidth: 1, + borderColor: colors.sheetTertiary, + borderRadius: variants.borderRadiusSizes.lg, + paddingHorizontal: variants.spacingSizes.md, + height: variants.roundButtonSizes.lg, + backgroundColor: colors.buttonSecondary, + marginBottom: variants.spacingSizes.sm, + }, + iconContainer: { + marginRight: variants.spacingSizes.sm, + }, + handIconContainer: { + marginRight: variants.spacingSizes.sm, + marginTop: variants.spacingSizes.xs, + }, + label: { + fontSize: variants.fontSizes.md, + color: colors.iconPrimary, + fontWeight: '600', + }, + screen: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + }), + [variants, colors], + ); +}; diff --git a/sample-apps/react-native/dogfood/src/components/Button.tsx b/sample-apps/react-native/dogfood/src/components/Button.tsx index d34e26fb7b..6b2750d1a1 100644 --- a/sample-apps/react-native/dogfood/src/components/Button.tsx +++ b/sample-apps/react-native/dogfood/src/components/Button.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { Pressable, PressableProps, @@ -8,8 +8,12 @@ import { TextStyle, ViewStyle, } from 'react-native'; -import { appTheme } from '../theme'; import { BUTTON_HEIGHT } from '../constants'; +import { + Theme, + defaultTheme, + useTheme, +} from '@stream-io/video-react-native-sdk'; type ButtonPropTypes = Omit & { title: string; @@ -24,6 +28,8 @@ export const Button = ({ titleStyle, ...rest }: ButtonPropTypes) => { + const styles = useStyles(); + return ( { + let appTheme: Theme; + try { + /* eslint-disable react-hooks/rules-of-hooks */ + appTheme = useTheme()?.theme; + } catch (e) { + appTheme = defaultTheme; + } + + return useMemo( + () => + StyleSheet.create({ + button: { + backgroundColor: appTheme.colors.buttonPrimary, + justifyContent: 'center', + borderRadius: 8, + height: BUTTON_HEIGHT, + paddingHorizontal: appTheme.variants.spacingSizes.md, + }, + buttonText: { + color: appTheme.colors.iconPrimary, + fontWeight: appTheme.typefaces.heading6.fontWeight, + textAlign: 'center', + fontSize: 17, + }, + disabledButtonStyle: { + backgroundColor: appTheme.colors.disabled, + }, + }), + [appTheme], + ); +}; diff --git a/sample-apps/react-native/dogfood/src/components/CallControlls/AudioButton.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/AudioButton.tsx new file mode 100644 index 0000000000..5890320cbc --- /dev/null +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/AudioButton.tsx @@ -0,0 +1,47 @@ +import React, { useState } from 'react'; +import { + CallControlsButton, + useTheme, +} from '@stream-io/video-react-native-sdk'; +import { IconWrapper } from '@stream-io/video-react-native-sdk/src/icons'; +import Audio from '../../assets/Audio'; + +/** + * The props for the Audio Button in the Call Controls. + */ +export type AudioButtonProps = { + /** + * Handler to be called when the audio button is pressed. + */ + onPressHandler?: () => void; +}; + +/** + * A button that can be used to switch audio output options + * like speaker, headphones, etc. + */ +export const AudioButton = ({ onPressHandler }: AudioButtonProps) => { + const { + theme: { colors, audioButton, variants }, + } = useTheme(); + + const [isPressed, setIsPressed] = useState(false); + const buttonColor = isPressed ? colors.buttonPrimary : colors.buttonSecondary; + + return ( + { + if (onPressHandler) { + onPressHandler(); + } + setIsPressed(!isPressed); + }} + style={audioButton} + color={buttonColor} + > + + + + ); +}; diff --git a/sample-apps/react-native/dogfood/src/components/CallControlls/BadgeCountIndicator.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/BadgeCountIndicator.tsx new file mode 100644 index 0000000000..536d185074 --- /dev/null +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/BadgeCountIndicator.tsx @@ -0,0 +1,68 @@ +import React, { useMemo } from 'react'; +import { StyleSheet, Text, View } from 'react-native'; +import { useTheme } from '@stream-io/video-react-native-sdk'; +import { Z_INDEX } from '../../constants'; +import { ComponentTestIds } from '../../constants/TestIds'; + +/** + * A badge that displays a number. + * + * @prop {number} count - The number to display in the badge. + * + * @returns {ReactElement} A View with a Text that displays the count in the badge. + */ +export const BadgeCountIndicator = ({ + count, +}: { + count: number | undefined; +}) => { + const { + theme: { colors, typefaces }, + } = useTheme(); + const styles = useStyles(); + + // Don't show badge if count is 0 or undefined + if (!count) { + return null; + } + + return ( + + + {count} + + + ); +}; + +const useStyles = () => { + const { theme } = useTheme(); + return useMemo( + () => + StyleSheet.create({ + badge: { + position: 'absolute', + justifyContent: 'center', + borderRadius: theme.defaults.borderRadius, + left: 13, + bottom: 17, + zIndex: Z_INDEX.IN_FRONT, + height: theme.variants.roundButtonSizes.xs, + width: theme.variants.roundButtonSizes.xs, + }, + badgeText: { + textAlign: 'center', + }, + }), + [theme], + ); +}; diff --git a/sample-apps/react-native/dogfood/src/components/CallControlls/BottomControls.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/BottomControls.tsx new file mode 100644 index 0000000000..988dccbc85 --- /dev/null +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/BottomControls.tsx @@ -0,0 +1,113 @@ +import { + CallContentProps, + ScreenShareToggleButton, + ToggleAudioPublishingButton, + ToggleVideoPublishingButton, + useCallStateHooks, + useTheme, +} from '@stream-io/video-react-native-sdk'; +import React, { useMemo } from 'react'; +import { StyleSheet, Text, View } from 'react-native'; +import { BOTTOM_CONTROLS_HEIGHT, Z_INDEX } from '../../constants'; +import { MoreActionsButton } from './MoreActionsButton'; +import { ParticipantsButton } from './ParticipantsButton'; +import { ChatButton } from './ChatButton'; +import { RecordCallButton } from './RecordCallButton'; + +export type BottomControlsProps = Pick< + CallContentProps, + 'supportedReactions' +> & { + onChatOpenHandler?: () => void; + onParticipantInfoPress?: () => void; + unreadCountIndicator?: number; + toggleCallRecording: () => Promise; + isAwaitingResponse: boolean; + isCallRecordingInProgress: boolean; +}; + +export const BottomControls = ({ + onChatOpenHandler, + unreadCountIndicator, + onParticipantInfoPress, + toggleCallRecording, + isAwaitingResponse, + isCallRecordingInProgress, +}: BottomControlsProps) => { + const { useMicrophoneState } = useCallStateHooks(); + const { isSpeakingWhileMuted } = useMicrophoneState(); + const styles = useStyles(isSpeakingWhileMuted); + + return ( + + {isSpeakingWhileMuted && ( + + You are muted. Unmute to speak. + + )} + + + + + + + + + + + + + + + ); +}; + +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: BOTTOM_CONTROLS_HEIGHT, + }, + speakingLabelContainer: { + backgroundColor: theme.colors.sheetPrimary, + width: '100%', + }, + label: { + textAlign: 'center', + color: theme.colors.textPrimary, + }, + callControlsWrapper: { + display: 'flex', + flexDirection: 'row', + justifyContent: 'flex-start', + zIndex: Z_INDEX.IN_FRONT, + }, + left: { + flex: 2.5, + flexDirection: 'row', + alignItems: 'flex-start', + gap: theme.variants.spacingSizes.xs, + }, + right: { + flex: 1, + flexDirection: 'row', + justifyContent: 'flex-end', + gap: theme.variants.spacingSizes.xs, + }, + }), + [theme, showMicLabel], + ); +}; diff --git a/sample-apps/react-native/dogfood/src/components/CallControlls/CallRecordingModal.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/CallRecordingModal.tsx new file mode 100644 index 0000000000..ad282eee11 --- /dev/null +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/CallRecordingModal.tsx @@ -0,0 +1,166 @@ +import { useTheme } from '@stream-io/video-react-native-sdk'; +import { IconWrapper } from '@stream-io/video-react-native-sdk/src/icons'; +import { RecordCall } from '../../assets/RecordCall'; +import React, { useMemo } from 'react'; +import { + Modal, + View, + Text, + TouchableOpacity, + TouchableWithoutFeedback, + StyleSheet, +} from 'react-native'; + +interface CallRecordingModalProps { + visible: boolean; + isLoading: boolean; + onCancel: () => void; + onConfirm: () => void; + message: string; + title: string; + confirmButton: string; + cancelButton: string; + isEndRecordingModal: boolean; +} + +export const CallRecordingModal: React.FC = ({ + visible, + isLoading, + onCancel, + onConfirm, + message, + title, + confirmButton, + cancelButton, + isEndRecordingModal, +}) => { + const styles = useStyles(isEndRecordingModal); + const { + theme: { colors, variants }, + } = useTheme(); + + return ( + + + + + + + + + + + + {title} + + {message} + + + + {cancelButton} + + + {isLoading ? ( + + Loading... + + ) : ( + {confirmButton} + )} + + + + + + + ); +}; + +const useStyles = (isEndRecordingModal: boolean) => { + const { theme } = useTheme(); + return useMemo( + () => + StyleSheet.create({ + overlay: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + modalView: { + backgroundColor: theme.colors.sheetSecondary, + borderRadius: theme.variants.borderRadiusSizes.lg, + padding: theme.variants.spacingSizes.xl, + width: '80%', + maxWidth: 380, + }, + content: { + marginBottom: theme.variants.spacingSizes.xl, + }, + headerContainer: { + flexDirection: 'row', + alignItems: 'center', + marginBottom: theme.variants.spacingSizes.sm, + }, + iconContainer: { + display: 'flex', + marginRight: theme.variants.spacingSizes.sm, + }, + title: { + color: theme.colors.textPrimary, + fontSize: theme.variants.fontSizes.lg, + fontWeight: '600', + textAlign: 'center', + }, + message: { + color: theme.colors.textSecondary, + fontSize: theme.variants.fontSizes.md, + fontWeight: '400', + textAlign: 'left', + }, + buttonContainer: { + flexDirection: 'row', + justifyContent: 'space-between', + gap: theme.variants.spacingSizes.md, + }, + button: { + flex: 1, + borderRadius: theme.variants.roundButtonSizes.md, + justifyContent: 'center', + alignItems: 'center', + }, + cancelButton: { + backgroundColor: theme.colors.sheetSecondary, + height: 32, + borderWidth: 1, + borderColor: theme.colors.sheetTertiary, + }, + confirmButton: { + height: 32, + backgroundColor: isEndRecordingModal + ? theme.colors.iconWarning + : theme.colors.buttonPrimary, + }, + buttonText: { + color: theme.colors.textPrimary, + fontSize: 13, + fontWeight: '600', + }, + }), + [theme, isEndRecordingModal], + ); +}; diff --git a/sample-apps/react-native/dogfood/src/components/CallControlls/CallStatusBadge.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/CallStatusBadge.tsx new file mode 100644 index 0000000000..25a71634c4 --- /dev/null +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/CallStatusBadge.tsx @@ -0,0 +1,119 @@ +import React, { useEffect, useMemo, useState } from 'react'; +import { View, Text, StyleSheet } from 'react-native'; +import { CallDuration } from '../../assets/CallDuration'; +import { RecordCall } from '../../assets/RecordCall'; +import { IconWrapper } from '@stream-io/video-react-native-sdk/src/icons'; +import { useTheme } from '@stream-io/video-react-native-sdk'; +import { useCallStateHooks } from '@stream-io/video-react-bindings'; + +const formatTime = (seconds: number) => { + const date = new Date(0); + date.setSeconds(seconds); + const format = date.toISOString(); + const hours = format.substring(11, 13); + const minutes = format.substring(14, 16); + const seconds_str = format.substring(17, 19); + return `${hours !== '00' ? hours + ':' : ''}${minutes}:${seconds_str}`; +}; + +export type CallStatusBadgeProps = { + isCallRecordingInProgress: boolean; + isAwaitingResponse: boolean; +}; + +export const CallStatusBadge: React.FC = ({ + isCallRecordingInProgress, + isAwaitingResponse, +}) => { + const { + theme: { + colors, + variants: { iconSizes }, + }, + } = useTheme(); + + // TODO: replace this with useDuration when that https://github.com/GetStream/stream-video-js/pull/1528 is merged + const [elapsed, setElapsed] = useState('00:00'); + const { useCallSession } = useCallStateHooks(); + const session = useCallSession(); + const startedAt = session?.started_at; + const startedAtDate = useMemo(() => { + if (!startedAt) { + return Date.now(); + } + const date = new Date(startedAt).getTime(); + return isNaN(date) ? Date.now() : date; + }, [startedAt]); + + useEffect(() => { + const initialElapsedSeconds = Math.max( + 0, + (Date.now() - startedAtDate) / 1000, + ); + + setElapsed(formatTime(initialElapsedSeconds)); + + const interval = setInterval(() => { + const elapsedSeconds = (Date.now() - startedAtDate) / 1000; + setElapsed(formatTime(elapsedSeconds)); + }, 1000); + + return () => clearInterval(interval); + }, [startedAtDate]); + + const styles = useStyles(isAwaitingResponse); + const recordingMessage = isCallRecordingInProgress + ? 'Stopping recording...' + : 'Recording in progress...'; + + let text = isAwaitingResponse ? recordingMessage : elapsed; + const showRecordingIcon = isCallRecordingInProgress || isAwaitingResponse; + + const icon = showRecordingIcon ? ( + + ) : ( + + ); + + return ( + + + {icon} + + {text} + + ); +}; + +const useStyles = (isLoading: boolean) => { + const { theme } = useTheme(); + return useMemo( + () => + StyleSheet.create({ + container: { + backgroundColor: theme.colors.buttonSecondary, + borderRadius: 8, + flexDirection: 'row', + height: 36, + paddingLeft: 17, + paddingRight: 5, + justifyContent: 'center', + alignItems: 'center', + width: isLoading ? 200 : 80, + }, + text: { + color: theme.colors.textPrimary, + fontSize: 14, + fontWeight: '600', + flexShrink: 0, + marginLeft: 10, + minWidth: 41, + }, + icon: { + marginTop: 2, + marginRight: 5, + }, + }), + [theme, isLoading], + ); +}; diff --git a/sample-apps/react-native/dogfood/src/components/CallControlls/ChatButton.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/ChatButton.tsx new file mode 100644 index 0000000000..6e6918d202 --- /dev/null +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/ChatButton.tsx @@ -0,0 +1,45 @@ +import React from 'react'; +import { + CallControlsButton, + useTheme, +} from '@stream-io/video-react-native-sdk'; +import { IconWrapper } from '@stream-io/video-react-native-sdk/src/icons'; +import { BadgeCountIndicator } from './BadgeCountIndicator'; +import Chat from '../../assets/Chat'; + +/** + * The props for the Chat Button in the Call Controls. + */ +export type ChatButtonProps = { + /** + * Handler to be called when the chat button is pressed. + * @returns void + */ + onPressHandler?: () => void; + /** + * The count of the current unread message to be displayed above on the Chat button. + */ + unreadBadgeCount?: number; +}; + +/** + * Button to open the Chat window while in the call. + * + * This call also display the unread count indicator/badge is there messages that are unread. + */ +export const ChatButton = ({ + onPressHandler, + unreadBadgeCount, +}: ChatButtonProps) => { + const { + theme: { colors, chatButton, variants }, + } = useTheme(); + return ( + + + + + + + ); +}; 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..fd58fe5969 --- /dev/null +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherButton.tsx @@ -0,0 +1,87 @@ +import React, { useState } from 'react'; +import { + CallControlsButton, + useTheme, +} from '@stream-io/video-react-native-sdk'; +import { IconWrapper } from '@stream-io/video-react-native-sdk/src/icons'; +import LayoutSwitcherModal from './LayoutSwitcherModal'; +import { ColorValue } from 'react-native'; +import { Grid } from '../../assets/Grid'; +import { SpotLight } from '../../assets/Spotlight'; +import { useLayout } from '../../contexts/LayoutContext'; + +export type LayoutSwitcherButtonProps = { + /** + * Handler to be called when the layout switcher button is pressed. + * @returns void + */ + onPressHandler?: () => void; +}; + +const getIcon = (selectedButton: string, color: ColorValue, size: number) => { + switch (selectedButton) { + case 'grid': + return ; + case 'spotlight': + return ; + default: + return 'grid'; + } +}; + +/** + * The layout switcher Button can be used to switch different layout arrangements + * of the call participants. + */ +export const LayoutSwitcherButton = ({ + onPressHandler, +}: LayoutSwitcherButtonProps) => { + const { + theme: { colors, variants }, + } = useTheme(); + + const { selectedLayout } = useLayout(); + const [isModalVisible, setIsModalVisible] = useState(false); + const [anchorPosition, setAnchorPosition] = useState<{ + x: number; + y: number; + width: number; + height: number; + } | null>(null); + + const buttonColor = isModalVisible + ? colors.iconSecondary + : colors.iconPrimary; + + const handleOpenModal = () => setIsModalVisible(true); + const handleCloseModal = () => setIsModalVisible(false); + + const handleLayout = (event: any) => { + const { x, y, width, height } = event.nativeEvent.layout; + setAnchorPosition({ x, y: y + height, width, height }); + }; + + return ( + { + handleOpenModal(); + if (onPressHandler) { + onPressHandler(); + } + setIsModalVisible(!isModalVisible); + }} + color={colors.sheetPrimary} + > + + {getIcon(selectedLayout, buttonColor, variants.iconSizes.lg)} + + + + ); +}; diff --git a/sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherModal.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherModal.tsx new file mode 100644 index 0000000000..e39f6d0a45 --- /dev/null +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/LayoutSwitcherModal.tsx @@ -0,0 +1,156 @@ +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 { SpotLight } from '../../assets/Spotlight'; +import { Layout, useLayout } from '../../contexts/LayoutContext'; + +interface AnchorPosition { + x: number; + y: number; + height: number; +} + +interface PopupComponentProps { + anchorPosition?: AnchorPosition | null; + isVisible: boolean; + onClose: () => void; +} + +const LayoutSwitcherModal: React.FC = ({ + isVisible, + onClose, + anchorPosition, +}) => { + const { theme } = useTheme(); + const styles = useStyles(); + const [popupPosition, setPopupPosition] = useState({ top: 0, left: 0 }); + const { selectedLayout, onLayoutSelection } = useLayout(); + const topInset = theme.variants.insets.top; + const leftInset = theme.variants.insets.left; + + useEffect(() => { + if (isVisible && anchorPosition) { + const windowHeight = Dimensions.get('window').height; + const windowWidth = Dimensions.get('window').width; + let top = anchorPosition.y + anchorPosition.height / 2 + topInset; + let left = anchorPosition.x + leftInset + 6; + + // Ensure the popup stays within the screen bounds + if (top + 150 > windowHeight) { + top = anchorPosition.y - 150; + } + if (left + 200 > windowWidth) { + left = windowWidth - 200; + } + + setPopupPosition({ top, left }); + } + }, [isVisible, anchorPosition, topInset, leftInset]); + + if (!isVisible || !anchorPosition) { + return null; + } + + const onPressHandler = (layout: Layout) => { + onLayoutSelection(layout); + onClose(); + }; + + return ( + + + + onPressHandler('grid')} + > + + Grid + + onPressHandler('spotlight')} + > + + Spotlight + + + + + ); +}; + +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.md, + padding: theme.variants.spacingSizes.md, + gap: theme.variants.spacingSizes.sm, + }, + button: { + backgroundColor: theme.colors.buttonSecondary, + borderRadius: theme.variants.borderRadiusSizes.lg, + borderWidth: 1, + borderColor: theme.colors.sheetTertiary, + flexDirection: 'row', + justifyContent: 'flex-start', + alignItems: 'center', + paddingHorizontal: theme.variants.spacingSizes.md, + paddingVertical: theme.variants.spacingSizes.sm, + }, + selectedButton: { + backgroundColor: theme.colors.buttonPrimary, + }, + buttonText: { + color: theme.colors.textPrimary, + textAlign: 'center', + fontWeight: '600', + marginTop: 2, + marginLeft: theme.variants.spacingSizes.xs, + }, + }), + [theme], + ); +}; + +export default LayoutSwitcherModal; diff --git a/sample-apps/react-native/dogfood/src/components/CallControlls/MoreActionsButton.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/MoreActionsButton.tsx new file mode 100644 index 0000000000..516a6ba369 --- /dev/null +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/MoreActionsButton.tsx @@ -0,0 +1,132 @@ +import React, { useState } from 'react'; +import { + CallControlsButton, + useCall, + useTheme, +} from '@stream-io/video-react-native-sdk'; +import { IconWrapper } from '@stream-io/video-react-native-sdk/src/icons'; +import MoreActions from '../../assets/MoreActions'; +import { BottomControlsDrawer, DrawerOption } from '../BottomControlsDrawer'; +import Feedback from '../../assets/Feedback'; +import FeedbackModal from '../FeedbackModal'; +import { + ThemeMode, + useAppGlobalStoreSetState, + useAppGlobalStoreValue, +} from '../../contexts/AppContext'; +import LightDark from '../../assets/LightDark'; + +/** + * The props for the More Actions Button in the Call Controls. + */ +export type MoreActionsButtonProps = { + /** + * Handler to be called when the more actions button is pressed. + */ + onPressHandler?: () => void; +}; + +/** + * A button that can be used to toggle the visibility + * of a menu or bottom sheet with more actions. + * + */ +export const MoreActionsButton = ({ + onPressHandler, +}: MoreActionsButtonProps) => { + const { + theme: { colors, variants, moreActionsButton, defaults }, + } = useTheme(); + const [isDrawerVisible, setIsDrawerVisible] = useState(false); + const [feedbackModalVisible, setFeedbackModalVisible] = useState(false); + const setState = useAppGlobalStoreSetState(); + const themeMode = useAppGlobalStoreValue((store) => store.themeMode); + const call = useCall(); + + const handleRating = async (rating: number) => { + await call + ?.submitFeedback(Math.min(Math.max(1, rating), 5), { + reason: '', + }) + .catch((err) => console.warn('Failed to submit call feedback', err)); + + setFeedbackModalVisible(false); + }; + + const getName = (theme: ThemeMode) => { + if (theme === 'light') { + return 'Dark mode'; + } + return 'Light mode'; + }; + + const options: DrawerOption[] = [ + { + id: '1', + label: 'Feedback', + icon: ( + + + + ), + onPress: () => { + setIsDrawerVisible(false); + setFeedbackModalVisible(true); + }, + }, + { + id: '2', + label: getName(themeMode), + icon: ( + + + + ), + onPress: () => { + if (themeMode === 'light') { + setState({ themeMode: 'dark' }); + } else { + setState({ themeMode: 'light' }); + } + setIsDrawerVisible(false); + }, + }, + ]; + + const buttonColor = isDrawerVisible + ? colors.buttonPrimary + : colors.buttonSecondary; + + return ( + { + if (onPressHandler) { + onPressHandler(); + } + setIsDrawerVisible(!isDrawerVisible); + }} + style={moreActionsButton} + color={buttonColor} + > + setIsDrawerVisible(false)} + options={options} + /> + setFeedbackModalVisible(false)} + onRating={handleRating} + /> + + + + + ); +}; diff --git a/sample-apps/react-native/dogfood/src/components/CallControlls/ParticipantsButton.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/ParticipantsButton.tsx new file mode 100644 index 0000000000..7dbf91a38e --- /dev/null +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/ParticipantsButton.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import { + CallControlsButton, + useTheme, +} from '@stream-io/video-react-native-sdk'; +import { IconWrapper } from '@stream-io/video-react-native-sdk/src/icons'; +import { useCallStateHooks } from '@stream-io/video-react-bindings'; +import { CallingState } from '@stream-io/video-client'; +import { BadgeCountIndicator } from './BadgeCountIndicator'; +import Participants from '../../assets/Participants'; + +/** + * The props for the Participants Button in the Call Controls. + */ +export type ParticipantsButtonProps = { + /** + * Handler to be called when the participants button is pressed. + * @returns void + */ + onParticipantInfoPress?: () => void; + /** + * The count of the current participants present in the call. + */ + participantCount?: number; +}; + +/** + * Button to open the Participant window while in the call. + * + * This button also displays the participant count of the participants in the call. + */ +export const ParticipantsButton = ({ + onParticipantInfoPress, + participantCount, +}: ParticipantsButtonProps) => { + const { + theme: { colors, chatButton, defaults }, + } = useTheme(); + + const { useCallMembers, useCallCallingState } = useCallStateHooks(); + const members = useCallMembers(); + const callingState = useCallCallingState(); + + let count = 0; + /** + * We show member's length if Incoming and Outgoing Call Views are rendered. + * Else we show the count of the participants that are in the call. + * Since the members count also includes caller/callee, we reduce the count by 1. + **/ + if (callingState === CallingState.RINGING) { + count = members.length - 1; + } else { + count = participantCount ?? 0; + } + + // TODO: PBE-5873 [Demo App] On click implement showing the Participant List + return ( + + + + + + + ); +}; diff --git a/sample-apps/react-native/dogfood/src/components/CallControlls/RecordCallButton.tsx b/sample-apps/react-native/dogfood/src/components/CallControlls/RecordCallButton.tsx new file mode 100644 index 0000000000..1e33faebc8 --- /dev/null +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/RecordCallButton.tsx @@ -0,0 +1,93 @@ +import React, { useState } from 'react'; +import { + CallControlsButton, + useTheme, +} from '@stream-io/video-react-native-sdk'; +import { RecordCall } from '../../assets/RecordCall'; +import { IconWrapper } from '@stream-io/video-react-native-sdk/src/icons'; +import { CallRecordingModal } from './CallRecordingModal'; + +/** + * The props for the Record Call Button in the Call Controls. + */ +export type RecordCallButtonProps = { + onPressHandler?: () => void; + toggleCallRecording: () => Promise; + isAwaitingResponse: boolean; + isCallRecordingInProgress: boolean; +}; + +/** + * The Record Call Button is used in the Call Controls component + * and allows the user to toggle call recording. + */ +export const RecordCallButton = ({ + onPressHandler, + toggleCallRecording, + isAwaitingResponse, + isCallRecordingInProgress, +}: RecordCallButtonProps) => { + const { + theme: { colors, recordCallButton, variants }, + } = useTheme(); + + const [isStopRecordingModalOpen, setIsStopRecordingModalOpen] = + useState(false); + + const buttonColor = isCallRecordingInProgress + ? colors.buttonWarning + : colors.buttonSecondary; + + const onPress = async () => { + if (onPressHandler) { + onPressHandler(); + } + if (isCallRecordingInProgress) { + setIsStopRecordingModalOpen(true); + } else { + await toggleCallRecording(); + } + }; + + const endRecording = ( + { + if (!isAwaitingResponse) { + setIsStopRecordingModalOpen(false); + } + }} + onConfirm={async () => { + if (!isAwaitingResponse) { + await toggleCallRecording(); + setIsStopRecordingModalOpen(false); + } + }} + /> + ); + + return ( + + {endRecording} + + + + + ); +}; 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..787d930359 --- /dev/null +++ b/sample-apps/react-native/dogfood/src/components/CallControlls/TopControls.tsx @@ -0,0 +1,102 @@ +import React, { useState, useMemo } from 'react'; +import { View, StyleSheet } 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'; +import { useOrientation } from '../../hooks/useOrientation'; + +export type TopControlsProps = { + onHangupCallHandler?: () => void; + isCallRecordingInProgress: boolean; + isAwaitingResponse: boolean; +}; + +export const TopControls = ({ + onHangupCallHandler, + isCallRecordingInProgress, + isAwaitingResponse, +}: TopControlsProps) => { + const [topControlsHeight, setTopControlsHeight] = useState(0); + const [topControlsWidth, setTopControlsWidth] = useState(0); + const styles = useStyles(); + const { theme } = useTheme(); + const orientation = useOrientation(); + const isLandscape = orientation === 'landscape'; + + 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. */} + + + + + + + {(!isAwaitingResponse || isLandscape) && } + + + + + + + + + + + ); +}; + +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/CallControlsComponent.tsx b/sample-apps/react-native/dogfood/src/components/CallControlsComponent.tsx deleted file mode 100644 index 9e20823073..0000000000 --- a/sample-apps/react-native/dogfood/src/components/CallControlsComponent.tsx +++ /dev/null @@ -1,84 +0,0 @@ -import { - CallContentProps, - ChatButton, - HangUpCallButton, - ReactionsButton, - ToggleAudioPublishingButton, - ToggleCameraFaceButton, - ToggleVideoPublishingButton, - ScreenShareToggleButton, - useCallStateHooks, -} from '@stream-io/video-react-native-sdk'; -import React from 'react'; -import { StyleSheet, Text, View, ViewStyle } from 'react-native'; -import { appTheme } from '../theme'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; -import { Z_INDEX } from '../constants'; -import { VideoEffectsButton } from './VideoEffectsButton'; - -export type CallControlsComponentProps = Pick< - CallContentProps, - 'supportedReactions' -> & { - onChatOpenHandler?: () => void; - onHangupCallHandler?: () => void; - unreadCountIndicator?: number; - landscape?: boolean; -}; - -export const CallControlsComponent = ({ - onChatOpenHandler, - onHangupCallHandler, - unreadCountIndicator, - landscape, -}: CallControlsComponentProps) => { - const { bottom } = useSafeAreaInsets(); - const { useMicrophoneState } = useCallStateHooks(); - const { isSpeakingWhileMuted } = useMicrophoneState(); - const landscapeStyles: ViewStyle = { - flexDirection: landscape ? 'column-reverse' : 'row', - paddingHorizontal: landscape ? 12 : 0, - paddingVertical: landscape ? 0 : 12, - paddingBottom: landscape ? 0 : Math.max(bottom, appTheme.spacing.lg), - }; - - return ( - - {isSpeakingWhileMuted && ( - - You are muted. Unmute to speak. - - )} - - - - - - - - - - - - ); -}; - -const styles = StyleSheet.create({ - speakingLabelContainer: { - backgroundColor: appTheme.colors.static_overlay, - paddingVertical: 10, - width: '100%', - }, - label: { - textAlign: 'center', - color: appTheme.colors.static_white, - }, - callControlsWrapper: { - justifyContent: 'space-evenly', - zIndex: Z_INDEX.IN_FRONT, - backgroundColor: appTheme.colors.static_grey, - }, -}); diff --git a/sample-apps/react-native/dogfood/src/components/FeedbackModal.tsx b/sample-apps/react-native/dogfood/src/components/FeedbackModal.tsx new file mode 100644 index 0000000000..a6529f5bfc --- /dev/null +++ b/sample-apps/react-native/dogfood/src/components/FeedbackModal.tsx @@ -0,0 +1,197 @@ +import { IconWrapper } from '@stream-io/video-react-native-sdk/src/icons'; +import React, { useMemo, useState } from 'react'; +import { + View, + Text, + TouchableOpacity, + StyleSheet, + Modal, + Image, +} from 'react-native'; +import Star from '../assets/Star'; +import { useTheme } from '@stream-io/video-react-native-sdk'; +import Close from '../assets/Close'; +import { FEEDBACK_MODAL_MAX_WIDTH } from '../constants'; + +interface FeedbackModalProps { + visible: boolean; + onClose: () => void; + onRating: (rating: number) => void; +} + +const FeedbackModal: React.FC = ({ + visible, + onClose, + onRating, +}) => { + const styles = useStyles(); + const { + theme: { colors, variants }, + } = useTheme(); + const [selectedRating, setSelectedRating] = useState(null); + + const handleRatingPress = (rating: number) => { + setSelectedRating(rating); + onRating(rating); + }; + + return ( + + + + + + + + + + + + + + + We Value Your Feedback! + + Tell us about your video call experience. + + + + {[1, 2, 3, 4, 5].map((rating) => ( + handleRatingPress(rating)} + style={[styles.ratingButton]} + > + + = rating + ? colors.iconSuccess + : colors.iconPrimary + } + size={68} + /> + + + ))} + + + + Very Bad + + + Very Good + + + + + + ); +}; + +const useStyles = () => { + const { + theme: { colors, variants }, + } = useTheme(); + return useMemo( + () => + StyleSheet.create({ + overlay: { + flex: 1, + justifyContent: 'center', + alignItems: 'center', + }, + modal: { + width: '90%', + backgroundColor: colors.sheetSecondary, + borderRadius: variants.borderRadiusSizes.lg, + alignItems: 'center', + paddingHorizontal: variants.spacingSizes.md, + paddingVertical: variants.spacingSizes.md, + maxWidth: FEEDBACK_MODAL_MAX_WIDTH, + }, + top: { + flex: 1, + marginBottom: variants.spacingSizes.lg, + flexDirection: 'row', + }, + closeButton: { + backgroundColor: colors.buttonSecondary, + borderRadius: variants.borderRadiusSizes.xl, + width: variants.roundButtonSizes.md, + height: variants.roundButtonSizes.md, + }, + topRight: { + flex: 1, + flexDirection: 'row', + justifyContent: 'flex-end', + }, + logo: { + width: 190, + height: 134, + marginBottom: variants.spacingSizes.md, + alignSelf: 'center', + }, + textContainer: { + maxWidth: 230, + textAlign: 'center', + }, + title: { + fontSize: 28, + marginBottom: variants.spacingSizes.sm, + textAlign: 'center', + color: colors.textPrimary, + fontWeight: '600', + }, + subtitle: { + fontSize: 13, + textAlign: 'center', + color: colors.textSecondary, + marginBottom: variants.spacingSizes.xl, + fontWeight: '600', + }, + ratingContainer: { + flexDirection: 'row', + justifyContent: 'center', + marginTop: variants.spacingSizes.md, + }, + ratingButton: { + paddingVertical: variants.spacingSizes.md, + }, + bottom: { + display: 'flex', + flexDirection: 'row', + marginTop: variants.spacingSizes.xl, + }, + left: { + flex: 1, + flexDirection: 'row', + alignItems: 'flex-start', + }, + right: { + flex: 1, + flexDirection: 'row', + justifyContent: 'flex-end', + }, + text: { + color: colors.textSecondary, + fontSize: 13, + fontWeight: '500', + }, + }), + [variants, colors], + ); +}; + +export default FeedbackModal; diff --git a/sample-apps/react-native/dogfood/src/components/LiveStream/LiveStreamChatControlButton.tsx b/sample-apps/react-native/dogfood/src/components/LiveStream/LiveStreamChatControlButton.tsx index 87214f0939..ee39eb8dbc 100644 --- a/sample-apps/react-native/dogfood/src/components/LiveStream/LiveStreamChatControlButton.tsx +++ b/sample-apps/react-native/dogfood/src/components/LiveStream/LiveStreamChatControlButton.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Pressable, View } from 'react-native'; import { appTheme } from '../../theme'; -import { Chat } from '../../assets/Chat'; +import { LiveStreamChat } from '../../assets/LiveStreamChat'; import { StyleSheet } from 'react-native'; type LiveStreamChatControlButtonProps = { @@ -22,7 +22,7 @@ export const LiveStreamChatControlButton = ({ ]} > - + ); diff --git a/sample-apps/react-native/dogfood/src/components/LobbyViewComponent.tsx b/sample-apps/react-native/dogfood/src/components/LobbyViewComponent.tsx index e8370c6206..bf7848ff91 100644 --- a/sample-apps/react-native/dogfood/src/components/LobbyViewComponent.tsx +++ b/sample-apps/react-native/dogfood/src/components/LobbyViewComponent.tsx @@ -31,18 +31,7 @@ export const LobbyViewComponent = ({ return ( <> - {route.name === 'MeetingScreen' ? ( - { - navigation.navigate('GuestModeScreen', { callId }); - }} - > - - {t('Join as Guest or Anonymously')} - - - ) : ( + {route.name !== 'MeetingScreen' && ( { diff --git a/sample-apps/react-native/dogfood/src/components/MeetingUI.tsx b/sample-apps/react-native/dogfood/src/components/MeetingUI.tsx index ab696c7e25..f650841981 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, @@ -128,13 +129,14 @@ export const MeetingUI = ({ callId, navigation, route }: Props) => { ); } else { return ( - + + + ); } }; diff --git a/sample-apps/react-native/dogfood/src/components/NavigationHeader.tsx b/sample-apps/react-native/dogfood/src/components/NavigationHeader.tsx index a08d99c031..4da24c0f0f 100755 --- a/sample-apps/react-native/dogfood/src/components/NavigationHeader.tsx +++ b/sample-apps/react-native/dogfood/src/components/NavigationHeader.tsx @@ -3,8 +3,9 @@ import { StreamVideoRN, useI18n, useStreamVideoClient, + useTheme, } from '@stream-io/video-react-native-sdk'; -import React from 'react'; +import React, { useMemo } from 'react'; import { Alert, StyleSheet, Text } from 'react-native'; import { useAppGlobalStoreSetState, @@ -21,6 +22,7 @@ import { REACT_NATIVE_DOGFOOD_APP_ENVIRONMENT } from '@env'; export const NavigationHeader = ({ route }: NativeStackHeaderProps) => { const videoClient = useStreamVideoClient(); const { t } = useI18n(); + const styles = useStyles(); const userName = useAppGlobalStoreValue((store) => store.userName); const appStoreSetState = useAppGlobalStoreSetState(); @@ -91,41 +93,48 @@ export const NavigationHeader = ({ route }: NativeStackHeaderProps) => { ); }; -const styles = StyleSheet.create({ - header: { - width: '100%', - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - paddingHorizontal: appTheme.spacing.lg, - paddingVertical: appTheme.spacing.lg, - backgroundColor: appTheme.colors.static_grey, - shadowColor: '#000', - shadowOffset: { - width: 0, - height: 2, - }, - shadowOpacity: 0.23, - shadowRadius: 2.62, +const useStyles = () => { + const { theme } = useTheme(); + return useMemo( + () => + StyleSheet.create({ + header: { + width: '100%', + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + paddingHorizontal: appTheme.spacing.lg, + paddingVertical: appTheme.spacing.lg, + backgroundColor: theme.colors.sheetSecondary, + shadowColor: '#000', + shadowOffset: { + width: 0, + height: 2, + }, + shadowOpacity: 0.23, + shadowRadius: 2.62, - elevation: 4, - }, - headerText: { - flexShrink: 1, - fontSize: 20, - fontWeight: '500', - color: appTheme.colors.static_white, - marginRight: appTheme.spacing.lg, - }, - avatar: { - height: AVATAR_SIZE, - width: AVATAR_SIZE, - borderRadius: 50, - }, - chooseAppMode: { - fontWeight: 'bold', - }, - buttonText: { - fontSize: 12, - }, -}); + elevation: 4, + }, + headerText: { + flexShrink: 1, + fontSize: 20, + fontWeight: '500', + color: theme.colors.textPrimary, + marginRight: appTheme.spacing.lg, + }, + avatar: { + height: AVATAR_SIZE, + width: AVATAR_SIZE, + borderRadius: 50, + }, + chooseAppMode: { + fontWeight: 'bold', + }, + buttonText: { + fontSize: 12, + }, + }), + [theme], + ); +}; diff --git a/sample-apps/react-native/dogfood/src/components/ParticipantActions.tsx b/sample-apps/react-native/dogfood/src/components/ParticipantActions.tsx index 7385512955..fca18ecf56 100644 --- a/sample-apps/react-native/dogfood/src/components/ParticipantActions.tsx +++ b/sample-apps/react-native/dogfood/src/components/ParticipantActions.tsx @@ -1,6 +1,5 @@ import { Avatar, - colorPallet, hasAudio, hasVideo, OwnCapability, @@ -8,6 +7,7 @@ import { useCall, useCallStateHooks, useI18n, + useTheme, } from '@stream-io/video-react-native-sdk'; import { Cross } from '../assets/Cross'; import { Mic } from '../assets/Mic'; @@ -18,7 +18,7 @@ import { Video } from '../assets/Video'; import { VideoDisabled } from '../assets/VideoDisabled'; import { VideoSlash } from '../assets/VideoSlash'; import { Pressable, StyleSheet, Text, View } from 'react-native'; -import React, { useCallback } from 'react'; +import React, { useCallback, useMemo } from 'react'; import { generateParticipantTitle } from '../utils'; type CallParticipantOptionType = { @@ -37,6 +37,10 @@ type ParticipantActionsType = { export const ParticipantActions = (props: ParticipantActionsType) => { const { participant, setSelectedParticipant } = props; const call = useCall(); + const { + theme: { colors }, + } = useTheme(); + const styles = useStyles(); const { t } = useI18n(); const { useHasPermissions } = useCallStateHooks(); const userHasMuteUsersCapability = useHasPermissions( @@ -99,7 +103,7 @@ export const ParticipantActions = (props: ParticipantActionsType) => { const muteUserVideoOption = participantPublishesVideo ? { - icon: , + icon: , title: 'Mute Video', onPressHandler: muteUserVideo, } @@ -107,7 +111,7 @@ export const ParticipantActions = (props: ParticipantActionsType) => { const muteUserAudioOption = participantPublishesAudio ? { - icon: , + icon: , title: 'Mute Audio', onPressHandler: muteUserAudio, } @@ -121,37 +125,37 @@ export const ParticipantActions = (props: ParticipantActionsType) => { userHasUpdateCallPermissionsCapability ? [ { - icon: , + icon: , title: 'Disable Video', onPressHandler: async () => await revokePermission(OwnCapability.SEND_VIDEO), }, { - icon: , + icon: , title: 'Disable Audio', onPressHandler: async () => await revokePermission(OwnCapability.SEND_AUDIO), }, { - icon: , + icon: , title: 'Allow Audio', onPressHandler: async () => await grantPermission(OwnCapability.SEND_AUDIO), }, { - icon: {options.map((option, index) => { @@ -234,59 +237,66 @@ export const ParticipantActions = (props: ParticipantActionsType) => { ); }; -const styles = StyleSheet.create({ - outerContainer: { - justifyContent: 'center', - flex: 1, - }, - modalContainer: { - backgroundColor: colorPallet.dark.bars, - borderRadius: 15, - marginHorizontal: 32, - }, - participantInfo: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'flex-start', - padding: 12, - }, - userInfo: { - flexDirection: 'row', - alignItems: 'center', - }, - name: { - marginLeft: 8, - fontSize: 16, - fontWeight: '500', - color: colorPallet.dark.text_high_emphasis, - }, - option: { - paddingHorizontal: 24, - paddingVertical: 12, - flexDirection: 'row', - alignItems: 'center', - }, - iconContainer: { - height: 20, - width: 20, - }, - title: { - marginLeft: 16, - color: colorPallet.dark.text_high_emphasis, - fontSize: 16, - fontWeight: '400', - }, - borderBottom: { - borderBottomColor: colorPallet.dark.borders, - borderBottomWidth: 1, - }, - crossIcon: { - height: 15, - width: 15, - }, - closePressable: { - padding: 8, - borderRadius: 5, - backgroundColor: colorPallet.light.static_grey, - }, -}); +const useStyles = () => { + const { theme } = useTheme(); + return useMemo( + () => + StyleSheet.create({ + outerContainer: { + justifyContent: 'center', + flex: 1, + }, + modalContainer: { + backgroundColor: theme.colors.sheetPrimary, + borderRadius: 15, + marginHorizontal: 32, + }, + participantInfo: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'flex-start', + padding: 12, + }, + userInfo: { + flexDirection: 'row', + alignItems: 'center', + }, + name: { + marginLeft: 8, + fontSize: 16, + fontWeight: '500', + color: theme.colors.iconPrimary, + }, + option: { + paddingHorizontal: 24, + paddingVertical: 12, + flexDirection: 'row', + alignItems: 'center', + }, + iconContainer: { + height: 20, + width: 20, + }, + title: { + marginLeft: 16, + color: theme.colors.iconPrimary, + fontSize: 16, + fontWeight: '400', + }, + borderBottom: { + borderBottomColor: theme.colors.sheetTertiary, + borderBottomWidth: 1, + }, + crossIcon: { + height: 15, + width: 15, + }, + closePressable: { + padding: 8, + borderRadius: 15, + backgroundColor: theme.colors.buttonSecondary, + }, + }), + [theme], + ); +}; diff --git a/sample-apps/react-native/dogfood/src/components/ParticipantsInfoList.tsx b/sample-apps/react-native/dogfood/src/components/ParticipantsInfoList.tsx index 7b5493d837..f2dd58eae4 100644 --- a/sample-apps/react-native/dogfood/src/components/ParticipantsInfoList.tsx +++ b/sample-apps/react-native/dogfood/src/components/ParticipantsInfoList.tsx @@ -1,7 +1,6 @@ -import React, { useCallback, useState } from 'react'; +import React, { useCallback, useMemo, useState } from 'react'; import { Avatar, - colorPallet, hasAudio, hasScreenShare, hasVideo, @@ -12,6 +11,7 @@ import { useCallStateHooks, useConnectedUser, useI18n, + useTheme, } from '@stream-io/video-react-native-sdk'; import { Alert, @@ -29,7 +29,6 @@ import { MicOff } from '../assets/MicOff'; import { ScreenShare } from '../assets/ScreenShare'; import { VideoSlash } from '../assets/VideoSlash'; import { ArrowRight } from '../assets/ArrowRight'; -import { appTheme } from '../theme'; import { ParticipantActions } from './ParticipantActions'; import { generateParticipantTitle } from '../utils'; import { Z_INDEX } from '../constants'; @@ -57,6 +56,8 @@ export const ParticipantsInfoList = ({ isCallParticipantsInfoVisible, setIsCallParticipantsInfoVisible, }: ParticipantsInfoListProps) => { + const styles = useStyles(); + const { theme } = useTheme(); const { useParticipants } = useCallStateHooks(); const participants = useParticipants(); const { t } = useI18n(); @@ -128,14 +129,14 @@ export const ParticipantsInfoList = ({ testID={ButtonTestIds.EXIT_PARTICIPANTS_INFO} > - + {t('Invite')} @@ -180,6 +181,10 @@ type ParticipantInfoType = { }; const ParticipantInfoItem = (props: ParticipantInfoType) => { + const { + theme: { colors }, + } = useTheme(); + const styles = useStyles(); const { participant, setSelectedParticipant } = props; const connectedUser = useConnectedUser(); const participantIsLocalParticipant = @@ -214,22 +219,22 @@ const ParticipantInfoItem = (props: ParticipantInfoType) => { - + )} {isAudioMuted && ( - + )} {isVideoMuted && ( - + )} {!participantIsLocalParticipant && ( - + )} @@ -237,99 +242,113 @@ const ParticipantInfoItem = (props: ParticipantInfoType) => { ); }; -const styles = StyleSheet.create({ - backDropBackground: { - ...StyleSheet.absoluteFillObject, - backgroundColor: colorPallet.dark.overlay, - zIndex: Z_INDEX.IN_BACK, - }, - content: { - zIndex: Z_INDEX.IN_FRONT, - backgroundColor: colorPallet.dark.bars, - borderRadius: 15, - marginHorizontal: 16, - }, - header: { - flexDirection: 'row', - justifyContent: 'space-between', - alignItems: 'center', - paddingVertical: 12, - }, - leftHeaderElement: { - marginLeft: 16, - }, - headerText: { - fontSize: 16, - fontWeight: '600', - color: colorPallet.dark.text_high_emphasis, - }, - closePressable: { - padding: 8, - borderRadius: 5, - marginRight: 16, - backgroundColor: colorPallet.light.static_grey, - }, - buttonGroup: { - flexDirection: 'row', - justifyContent: 'space-between', - paddingVertical: 12, - paddingHorizontal: 4, - }, - screenShareIconContainer: { - height: 25, - width: 25, - }, - genericIconContainer: { - height: 20, - width: 20, - }, - crossIcon: { - height: 15, - width: 15, - }, - button: { - flex: 1, - backgroundColor: colorPallet.dark.primary, - borderRadius: 24, - padding: 8, - marginHorizontal: 8, - }, - buttonText: { - textAlign: 'center', - color: colorPallet.dark.static_white, - fontSize: 16, - fontWeight: '500', - }, - participant: { - paddingHorizontal: 8, - paddingVertical: 4, - flexDirection: 'row', - alignItems: 'center', - justifyContent: 'space-between', - borderBottomColor: colorPallet.dark.borders, - borderBottomWidth: 1, - }, - participantInfo: { - flexDirection: 'row', - alignItems: 'center', - flexShrink: 1, - }, - name: { - marginLeft: 8, - color: colorPallet.dark.text_high_emphasis, - flexShrink: 1, - fontSize: 16, - fontWeight: '500', - }, - icons: { - flexDirection: 'row', - }, - svgContainerStyle: { - marginLeft: 8, - }, - modal: { - alignItems: 'center', - justifyContent: 'center', - backgroundColor: colorPallet.dark.overlay, - }, -}); +const useStyles = () => { + const { theme } = useTheme(); + return useMemo( + () => + StyleSheet.create({ + backDropBackground: { + ...StyleSheet.absoluteFillObject, + zIndex: Z_INDEX.IN_BACK, + }, + content: { + zIndex: Z_INDEX.IN_FRONT, + backgroundColor: theme.colors.sheetPrimary, + borderRadius: 15, + marginHorizontal: 16, + marginTop: 65, + }, + header: { + flexDirection: 'row', + justifyContent: 'space-between', + alignItems: 'center', + paddingVertical: 12, + }, + leftHeaderElement: { + marginLeft: 16, + }, + headerText: { + fontSize: 16, + fontWeight: '600', + color: theme.colors.textPrimary, + }, + closePressable: { + padding: 8, + borderRadius: 15, + marginRight: 16, + backgroundColor: theme.colors.buttonSecondary, + }, + buttonGroup: { + flexDirection: 'row', + justifyContent: 'space-between', + paddingVertical: 12, + paddingHorizontal: 4, + }, + screenShareIconContainer: { + height: 25, + width: 25, + }, + genericIconContainer: { + height: 20, + width: 20, + }, + crossIcon: { + height: 15, + width: 15, + }, + button: { + flex: 1, + borderRadius: 24, + padding: 8, + marginHorizontal: 8, + borderColor: theme.colors.buttonSecondary, + borderWidth: 2, + }, + inviteButton: { + flex: 1, + backgroundColor: theme.colors.buttonPrimary, + borderRadius: 24, + padding: 8, + marginHorizontal: 8, + }, + buttonText: { + textAlign: 'center', + color: theme.colors.iconPrimary, + fontSize: 16, + fontWeight: '500', + }, + participant: { + paddingHorizontal: 8, + paddingVertical: 4, + flexDirection: 'row', + alignItems: 'center', + justifyContent: 'space-between', + borderBottomColor: theme.colors.sheetTertiary, + borderBottomWidth: 1, + }, + participantInfo: { + flexDirection: 'row', + alignItems: 'center', + flexShrink: 1, + }, + name: { + marginLeft: 8, + color: theme.colors.textPrimary, + flexShrink: 1, + fontSize: 16, + fontWeight: '500', + }, + icons: { + flexDirection: 'row', + }, + svgContainerStyle: { + marginLeft: 8, + }, + modal: { + alignItems: 'center', + justifyContent: 'center', + }, + }), + [theme], + ); +}; diff --git a/sample-apps/react-native/dogfood/src/components/ParticipantsLayoutButton.tsx b/sample-apps/react-native/dogfood/src/components/ParticipantsLayoutButton.tsx deleted file mode 100644 index 982847e71a..0000000000 --- a/sample-apps/react-native/dogfood/src/components/ParticipantsLayoutButton.tsx +++ /dev/null @@ -1,134 +0,0 @@ -import React, { useState } from 'react'; -import { Pressable, Text, Modal, StyleSheet, View } from 'react-native'; -import GridIconSvg from '../assets/GridIconSvg'; -import { appTheme } from '../theme'; - -type Layout = 'grid' | 'spotlight'; - -const LayoutSelectionItem = ({ - layout, - selectedLayout, - setSelectedLayout, - closeModal, -}: { - layout: Layout; - selectedLayout: Layout; - setSelectedLayout: (mode: Layout) => void; - closeModal: () => void; -}) => { - if (!layout) { - return null; - } - - return ( - { - setSelectedLayout(layout); - closeModal(); - }} - style={styles.modalButton} - > - - {layout[0].toUpperCase() + layout.substring(1)} - - - ); -}; - -export const ParticipantsLayoutSwitchButton = ({ - selectedLayout, - setSelectedLayout, -}: { - selectedLayout: Layout; - setSelectedLayout: (m: Layout) => void; -}) => { - const [modalVisible, setModalVisible] = useState(false); - const closeModal = () => setModalVisible(false); - - return ( - <> - - setModalVisible(false)} - > - true}> - - - - - - - setModalVisible(true)} - style={styles.gridButton} - > - - - - - ); -}; - -const styles = StyleSheet.create({ - centeredView: { - flex: 1, - justifyContent: 'center', - alignItems: 'center', - backgroundColor: 'rgba(0, 0, 0, 0.5)', - }, - modalView: { - backgroundColor: appTheme.colors.static_grey, - borderRadius: 20, - padding: appTheme.spacing.md, - alignItems: 'flex-start', - shadowColor: '#000', - shadowOffset: { - width: 0, - height: 2, - }, - shadowOpacity: 0.25, - shadowRadius: 4, - elevation: 5, - }, - gridButton: { - height: 30, - width: 30, - }, - modalButton: { - padding: appTheme.spacing.lg, - }, - modalText: { - fontSize: 20, - fontWeight: 'bold', - }, - buttonsContainer: { - paddingHorizontal: appTheme.spacing.sm, - }, -}); diff --git a/sample-apps/react-native/dogfood/src/components/TextInput.tsx b/sample-apps/react-native/dogfood/src/components/TextInput.tsx index ae41d4c038..c0d579b77f 100644 --- a/sample-apps/react-native/dogfood/src/components/TextInput.tsx +++ b/sample-apps/react-native/dogfood/src/components/TextInput.tsx @@ -1,15 +1,20 @@ -import React from 'react'; +import React, { useMemo } from 'react'; import { TextInput as NativeTextInput, StyleSheet, TextInputProps, } from 'react-native'; -import { appTheme } from '../theme'; import { INPUT_HEIGHT } from '../constants'; +import { + Theme, + defaultTheme, + useTheme, +} from '@stream-io/video-react-native-sdk'; export const TextInput = ( props: Omit, ) => { + const styles = useStyles(); return ( { + let appTheme: Theme; + try { + /* eslint-disable react-hooks/rules-of-hooks */ + appTheme = useTheme()?.theme; + } catch (e) { + appTheme = defaultTheme; + } + return useMemo( + () => + StyleSheet.create({ + input: { + paddingLeft: appTheme.variants.spacingSizes.lg, + marginVertical: appTheme.variants.spacingSizes.md, + height: INPUT_HEIGHT, + backgroundColor: appTheme.colors.sheetSecondary, + borderRadius: 8, + borderColor: appTheme.colors.buttonDisabled, + borderWidth: 1, + color: appTheme.colors.textPrimary, + fontSize: 17, + flex: 1, + }, + }), + [appTheme], + ); +}; 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 528316bcd1..2bbccd41ca 100644 --- a/sample-apps/react-native/dogfood/src/components/VideoEffectsButton/index.tsx +++ b/sample-apps/react-native/dogfood/src/components/VideoEffectsButton/index.tsx @@ -3,9 +3,9 @@ import { useBackgroundFilters, BlurIntensity, BackgroundFiltersProvider, + useTheme, } from '@stream-io/video-react-native-sdk'; -import React, { useState } from 'react'; -import { AutoAwesome } from '../../assets/AutoAwesome'; +import React, { useMemo, useState } from 'react'; import { Image, Modal, @@ -18,6 +18,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 '../../assets/Effects'; type ImageSourceType = ImageURISource | number; @@ -42,6 +44,8 @@ export const VideoEffectsButton = () => ( const FilterButton = () => { const [modalVisible, setModalVisible] = useState(false); const closeModal = () => setModalVisible(false); + const { theme } = useTheme(); + const styles = useStyles(); const { disableCustomFilter } = useCustomVideoFilters(); const { isSupported, disableAllFilters } = useBackgroundFilters(); @@ -75,14 +79,24 @@ const FilterButton = () => { - setModalVisible((prev) => !prev)}> - + setModalVisible((prev) => !prev)} + > + + + ); }; const CustomFiltersRow = ({ closeModal }: { closeModal: () => void }) => { + const styles = useStyles(); const { applyGrayScaleFilter, currentCustomFilter } = useCustomVideoFilters(); const grayScaleSelected = currentCustomFilter === 'GrayScale'; return ( @@ -111,6 +125,7 @@ const ModalFilterButton = ({ onPress: () => void; closeModal: () => void; }) => { + const styles = useStyles(); return ( - +

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