enx-uikit-react-native
Version:
It is a react native component for Enablex users.
653 lines (596 loc) • 27.7 kB
JavaScript
import React, { PureComponent } from "react";
import { View, Text, TouchableHighlight, Image, Dimensions, Platform, StatusBar } from "react-native";
import { Enx, EnxStream, EnxRoom } from "enx-rtc-react-native";
import { EnxSetting } from "..";
import {styles} from "../style/EnxConfirmationViewStyle";
import { th } from "date-fns/locale";
// Safe Area imports with fallback
let SafeAreaProvider, SafeAreaView, useSafeAreaInsets, Edge;
try {
const safeAreaContext = require('react-native-safe-area-context');
SafeAreaProvider = safeAreaContext.SafeAreaProvider;
SafeAreaView = safeAreaContext.SafeAreaView;
useSafeAreaInsets = safeAreaContext.useSafeAreaInsets;
Edge = safeAreaContext.Edge;
} catch (error) {
console.log('Safe area context not available, using fallback');
SafeAreaProvider = View;
SafeAreaView = View;
useSafeAreaInsets = () => ({ top: 0, bottom: 0, left: 0, right: 0 });
Edge = {};
}
class EnxConfirmationScreen extends PureComponent {
constructor(props) {
super(props);
this.state = {
muteAudioAtJoin : EnxSetting.getIsAudioMuted(),
muteVideoAtJoin : EnxSetting.getIsVideoMuted(),
joinAsAudioOnly : EnxSetting.getIsAudioOnly(),
useFontCamera : EnxSetting.getCameraPosition(),
audioUnmuteImage: require("../image_asset/audio_on.png"),
videoUnmuteImage: require("../image_asset/video_on.png"),
audioMuteImage: require("../image_asset/audio_off.png"),
videoMuteImage: require("../image_asset/video_off.png"),
previewImage : require("../image_asset/previewOpt.png"),
cameraFlipImage : require("../image_asset/cameraFlip.png"),
orientation : this.getOrientation(),
windowHeight: Dimensions.get("window").height,
windowWidth: Dimensions.get("window").width,
safeAreaInsets: { top: 0, bottom: 0, left: 0, right: 0 },
statusBarHeight: 0,
hasNotch: false,
}
// Add event listener to detect orientation change
//Dimensions.addEventListener("change", this.onOrientationChange);
this.orientationChangeListener = Dimensions.addEventListener(
"change",
this.onOrientationChange
);
}
// Remove the event listener when the component unmounts
componentWillUnmount() {
// Remove the event listener
if (this.orientationChangeListener && this.orientationChangeListener.remove) {
this.orientationChangeListener.remove(); // Proper removal using remove() method
}
}
//To detect the current orientation of the device
getOrientation = () => {
const { height, width } = Dimensions.get("window");
return height >= width ? "portrait" : "landscape";
};
// Handle orientation changes with smart app header recalculation
onOrientationChange = ({ window: { width, height } }) => {
if (height && width) {
const newOrientation = this.getOrientation(width, height);
const isLandscape = newOrientation === 'landscape';
// Force re-render to ensure proper layout calculation
this.setState({
orientation: newOrientation,
windowHeight: height,
windowWidth: width,
isPortrait: !isLandscape,
}, () => {
// Recalculate safe area insets for new orientation
this.calculateSafeAreaInsets();
// Delay re-calculation to ensure orientation change is complete
setTimeout(() => {
// Recalculate safe area again for proper bottom spacing
this.calculateSafeAreaInsets();
const headerStatus = this.getCompleteHeaderStatus();
console.log("🔄 Confirmation Screen After Orientation Change:", {
orientation: newOrientation,
dimensions: { width, height },
headerStatus,
safeAreaInsets: this.state.safeAreaInsets,
adaptiveSpacing: this.getAdaptiveSpacingForOrientation(isLandscape)
});
// Force component re-render with new calculations
this.forceUpdate();
}, 150); // Increased delay to ensure proper orientation completion
});
}
};
// Check if app header is visible - similar to EnxVideoView
checkAppHeader = () => {
const { navigation } = this.props;
let appHeaderInfo = {
isNavigationHeaderVisible: false,
navigationHeaderHeight: 0,
hasCustomTopBar: false,
totalAppHeaderHeight: 0,
appHeaderDetails: {}
};
try {
// Method 1: Check navigation header from props
if (navigation) {
const navState = navigation.getState ? navigation.getState() : null;
if (navState) {
// Get current route options
const currentRoute = navState.routes[navState.index];
const screenOptions = navigation.getParent()?.getState()?.routes.find(r => r.name === currentRoute.name)?.params?.screenOptions;
appHeaderInfo.isNavigationHeaderVisible = screenOptions?.headerShown !== false;
}
}
// Method 2: Check for React Navigation header height
if (this.props.headerHeight) {
appHeaderInfo.navigationHeaderHeight = this.props.headerHeight;
appHeaderInfo.isNavigationHeaderVisible = this.props.headerHeight > 0;
}
// Method 3: Detect custom top bar (from EnxJoinScreen static options)
appHeaderInfo.hasCustomTopBar = true; // Based on your app configuration
// Method 4: Calculate total app header height
const statusBarHeight = Platform.OS === 'android' ? StatusBar.currentHeight || 0 : 0;
const safeAreaTop = this.state.safeAreaInsets?.top || 0;
const estimatedHeaderHeight = appHeaderInfo.isNavigationHeaderVisible ? 56 : 0; // Standard header height
appHeaderInfo.totalAppHeaderHeight = Math.max(safeAreaTop, statusBarHeight + estimatedHeaderHeight);
appHeaderInfo.appHeaderDetails = {
statusBarHeight,
safeAreaTop,
estimatedHeaderHeight,
hasNotch: this.state.hasNotch || false,
platform: Platform.OS
};
} catch (error) {
console.warn('Error checking app header in confirmation screen:', error);
}
return appHeaderInfo;
};
// Get comprehensive header status including app header
getCompleteHeaderStatus = () => {
const appHeaderInfo = this.checkAppHeader();
return {
appHeader: appHeaderInfo,
hasAnyHeader: appHeaderInfo.isNavigationHeaderVisible || appHeaderInfo.hasCustomTopBar,
totalHeaderHeight: appHeaderInfo.totalAppHeaderHeight
};
};
// Get adaptive spacing for specific orientation
getAdaptiveSpacingForOrientation = (isLandscape) => {
const headerStatus = this.getCompleteHeaderStatus();
const baseCompensation = headerStatus.appHeader.totalAppHeaderHeight;
const isHeaderVisible = headerStatus.hasAnyHeader;
if (!isHeaderVisible) {
return { topSpacing: 0, leftSpacing: 0, compensation: 0 };
}
return {
topSpacing: isLandscape ? 0 : Math.max(baseCompensation * 0.3, 0),
leftSpacing: isLandscape ? Math.max(baseCompensation * 0.2, 0) : 0,
compensation: isLandscape ? baseCompensation * 0.3 : baseCompensation * 0.7
};
};
// Initialize safe area support with app header detection
initializeSafeArea = () => {
this.detectNotch();
this.calculateSafeAreaInsets();
this.configureStatusBar();
// Log app header status
const headerStatus = this.getCompleteHeaderStatus();
console.log("🎯 Confirmation Screen App Header Status:", headerStatus);
};
// Detect if device has notch or rounded corners
detectNotch = () => {
const { height, width } = Dimensions.get('window');
const aspectRatio = height / width;
// Common notch indicators
const hasNotch = Platform.OS === 'ios'
? (aspectRatio > 2.0 || height >= 812)
: (aspectRatio > 2.0 || height >= 780);
this.setState({ hasNotch });
};
// Calculate safe area insets with fallback
calculateSafeAreaInsets = () => {
const { windowHeight, windowWidth, hasNotch, orientation } = this.state;
let safeAreaInsets = { top: 0, bottom: 0, left: 0, right: 0 };
if (Platform.OS === 'ios') {
if (hasNotch) {
if (orientation === 'portrait') {
safeAreaInsets.top = 44;
safeAreaInsets.bottom = 34;
safeAreaInsets.left = 0;
safeAreaInsets.right = 0;
} else {
safeAreaInsets.top = 0;
safeAreaInsets.bottom = 21;
safeAreaInsets.left = 44;
safeAreaInsets.right = 44;
}
} else {
safeAreaInsets.top = orientation === 'portrait' ? 20 : 0;
safeAreaInsets.bottom = 0;
safeAreaInsets.left = 0;
safeAreaInsets.right = 0;
}
} else {
// Android - More robust handling
const statusBarHeight = StatusBar.currentHeight || 24;
if (orientation === 'portrait') {
safeAreaInsets.top = statusBarHeight;
safeAreaInsets.bottom = hasNotch ? 20 : 10;
safeAreaInsets.left = 0;
safeAreaInsets.right = 0;
} else {
safeAreaInsets.top = hasNotch ? statusBarHeight : 0;
safeAreaInsets.bottom = hasNotch ? 10 : 0;
safeAreaInsets.left = hasNotch ? 20 : 0;
safeAreaInsets.right = hasNotch ? 20 : 0;
}
}
this.setState({
safeAreaInsets,
statusBarHeight: safeAreaInsets.top
});
};
// Configure status bar
configureStatusBar = () => {
if (Platform.OS === 'android') {
StatusBar.setTranslucent(true);
StatusBar.setBackgroundColor('transparent');
}
StatusBar.setBarStyle('dark-content');
};
// Calculate responsive dimensions with NO black space for all devices
getResponsiveDimensions = () => {
const { windowHeight, windowWidth, orientation, safeAreaInsets } = this.state;
const isPortrait = orientation === 'portrait';
// Get app header information - recalculated for current orientation
const headerStatus = this.getCompleteHeaderStatus();
const baseAppHeaderHeight = headerStatus.appHeader.totalAppHeaderHeight;
const isAppHeaderVisible = headerStatus.hasAnyHeader;
// Conservative safe area handling
const topSafeArea = Math.max(safeAreaInsets.top || 0, 0);
const bottomSafeArea = Math.max(safeAreaInsets.bottom || 0, 0);
const leftSafeArea = Math.max(safeAreaInsets.left || 0, 0);
const rightSafeArea = Math.max(safeAreaInsets.right || 0, 0);
// MINIMAL app header compensation to eliminate black space
let appHeaderCompensation = 0;
if (isAppHeaderVisible) {
if (isPortrait) {
appHeaderCompensation = Math.max(baseAppHeaderHeight - topSafeArea, 0) * 0.4; // Much smaller
} else {
appHeaderCompensation = Math.max(baseAppHeaderHeight - leftSafeArea, 0) * 0.2; // Minimal
}
}
// Calculate MAXIMUM usable dimensions - ELIMINATE ALL BLACK SPACE
const totalHeight = windowHeight || Dimensions.get('window').height;
const totalWidth = windowWidth || Dimensions.get('window').width;
// Reserve adequate space with orientation-aware bottom spacing
const reservedTop = topSafeArea + (isPortrait && isAppHeaderVisible ? Math.min(appHeaderCompensation, 20) : 0);
const reservedBottom = bottomSafeArea + (isPortrait ? Math.max(80, bottomSafeArea + 40) : 30); // Dynamic bottom spacing for portrait
const reservedLeft = leftSafeArea + (!isPortrait && isAppHeaderVisible ? Math.min(appHeaderCompensation, 15) : 0);
const reservedRight = rightSafeArea;
// MAXIMIZE usable space
const usableHeight = Math.max(totalHeight - reservedTop - reservedBottom, 300);
const usableWidth = Math.max(totalWidth - reservedLeft - reservedRight, 250);
console.log("📐 Confirmation Screen - Full Space Utilization:", {
totalHeight, totalWidth, usableHeight, usableWidth,
reservedTop, reservedBottom, reservedLeft, reservedRight,
orientation: isPortrait ? 'portrait' : 'landscape',
isAppHeaderVisible, appHeaderCompensation
});
if (isPortrait) {
// Portrait: MAXIMIZE video area (75% instead of 70%)
const videoHeight = Math.floor(usableHeight * 0.65);
const controlHeight = usableHeight - videoHeight;
return {
videoContainer: {
width: usableWidth,
height: videoHeight,
},
controlContainer: {
width: usableWidth,
height: controlHeight,
},
appHeaderCompensation,
isAppHeaderVisible,
totalDimensions: { totalHeight, totalWidth },
usableDimensions: { usableHeight, usableWidth }
};
} else {
// Landscape: MAXIMIZE video area (65% instead of 60%)
const videoWidth = Math.floor(usableWidth * 0.50);
const controlWidth = usableWidth - videoWidth;
return {
videoContainer: {
width: videoWidth,
height: usableHeight,
},
controlContainer: {
width: controlWidth,
height: usableHeight,
},
appHeaderCompensation,
isAppHeaderVisible,
totalDimensions: { totalHeight, totalWidth },
usableDimensions: { usableHeight, usableWidth }
};
}
};
render() {
const { orientation, safeAreaInsets, windowHeight, windowWidth } = this.state;
const isPortrait = orientation === "portrait";
const dimensions = this.getResponsiveDimensions();
// Get app header information for UI compensation
const headerStatus = this.getCompleteHeaderStatus();
const appHeaderCompensation = dimensions.appHeaderCompensation || 0;
const isAppHeaderVisible = dimensions.isAppHeaderVisible || false;
// Safeguard dimensions
const safeHeight = windowHeight || Dimensions.get('window').height;
const safeWidth = windowWidth || Dimensions.get('window').width;
// Container style - FULL screen usage, let SafeAreaProvider handle safe areas
const containerStyle = {
flex: 1,
flexDirection: isPortrait ? "column" : "row",
backgroundColor: '#000',
width: '100%',
height: '100%',
// Remove padding - SafeAreaProvider handles safe areas
// In landscape, safe areas are handled by child containers
paddingTop: 0,
paddingBottom: 0,
paddingLeft: 0,
paddingRight: 0,
// NO margins - maximize space usage
margin: 0,
};
// Video container style - FILL available space completely using flex
const videoContainerStyle = {
backgroundColor: '#000',
justifyContent: 'center',
alignItems: 'center',
// Use flex to fill available space - different ratios for portrait vs landscape
flex: isPortrait ? 7 : 3, // Portrait: 7:1 (~87.5%), Landscape: 3:2 (60%)
width: isPortrait ? '100%' : undefined,
height: isPortrait ? undefined : '100%',
overflow: 'hidden',
// Orientation-aware padding for safe areas in landscape
paddingTop: isPortrait ? 0 : Math.max(safeAreaInsets.top || 0, 0),
paddingBottom: isPortrait ? 0 : Math.max(safeAreaInsets.bottom || 0, 0),
paddingLeft: isPortrait ? 0 : Math.max(safeAreaInsets.left || 0, 0),
paddingRight: isPortrait ? 0 : Math.max(safeAreaInsets.right || 0, 0),
// NO margins - fill completely to eliminate black space
margin: 0,
// Ensure proper positioning
position: 'relative',
};
// Control container style - MINIMAL space, just enough for controls
const controlContainerStyle = {
backgroundColor: '#F4FAFC',
justifyContent: 'center',
alignItems: 'center',
// Use flex for space - different ratios for portrait vs landscape
flex: isPortrait ? 1 : 2, // Portrait: 7:1 (~12.5%), Landscape: 3:2 (40%)
width: isPortrait ? '100%' : undefined,
height: isPortrait ? undefined : '100%',
overflow: 'hidden',
// Orientation-aware padding with safe area handling
paddingTop: isPortrait ? 5 : Math.max(safeAreaInsets.top || 0, 5),
paddingBottom: isPortrait ? Math.max(safeAreaInsets.bottom || 0, 8) : Math.max(safeAreaInsets.bottom || 0, 5),
paddingLeft: isPortrait ? 5 : Math.max(safeAreaInsets.left || 0, 5),
paddingRight: isPortrait ? 5 : Math.max(safeAreaInsets.right || 0, 5),
// NO margins - fill remaining space completely
margin: 0,
// Reduced minimum height for controls - minimal space needed
minHeight: isPortrait ? 100 : 100,
// Use flexShrink to prevent expansion
flexShrink: 1,
};
// Floating button position - orientation-aware with safe area handling
const floatingButtonStyle = {
position: 'absolute',
top: isPortrait
? Math.max(safeAreaInsets.top || 0, 0) + 20
: Math.max(safeAreaInsets.top || 0, 0) + 10,
left: isPortrait
? Math.max(safeAreaInsets.left || 0, 0) + 20
: Math.max(safeAreaInsets.left || 0, 0) + 10,
backgroundColor: 'transparent',
padding: 10,
borderRadius: 25,
elevation: 5,
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.8,
shadowRadius: 2,
zIndex: 1000,
};
return (
<SafeAreaProvider>
<StatusBar
barStyle="dark-content"
backgroundColor={Platform.OS === 'android' ? 'transparent' : undefined}
translucent={Platform.OS === 'android'}
hidden={false}
/>
<SafeAreaView
style={{ flex: 1, backgroundColor: '#000' }}
edges={isPortrait ? ['top', 'left', 'right'] : ['top', 'bottom', 'left', 'right']}
>
<View style={containerStyle}>
{/* Floating Button */}
{this.state.muteVideoAtJoin ? null :
<TouchableHighlight
style={floatingButtonStyle}
underlayColor="transparent"
onPress={this.onSwitchCamera}>
<Image
source={this.state.cameraFlipImage}
style={{
width: 40,
alignSelf: "center",
height: 40,
}}/>
</TouchableHighlight>
}
{/* Video Part - FILL COMPLETE SPACE, NO BLACK AREA */}
<View style={videoContainerStyle}>
{/** Creating Preview for camera Position - FULL CONTAINER FILL */}
{(this.state.joinAsAudioOnly || this.state.muteVideoAtJoin) ?
<View style={{
width: '100%',
height: '100%',
backgroundColor: '#000',
alignItems: 'center',
justifyContent: 'center',
}}>
<Image
style={{
width: Math.min(dimensions.videoContainer.width * 0.5, 200),
height: Math.min(dimensions.videoContainer.height * 0.5, 200),
resizeMode: 'contain'
}}
source={this.state.previewImage}/>
</View> :
<EnxStream
key={`${this.state.orientation}-${dimensions.videoContainer.width}-${dimensions.videoContainer.height}`}
style={{
position: 'absolute',
top: 0,
left: 0,
right: 0,
bottom: 0,
width: '100%', // FILL CONTAINER WIDTH COMPLETELY
height: '100%', // FILL CONTAINER HEIGHT COMPLETELY
backgroundColor: '#000',
// Ensure no gaps or breaks
margin: 0,
padding: 0,
// Force fill entire container
flex: 1,
}}
isPreview={true}
isFrontCamera={this.state.useFontCamera}/>
}
</View>
{/* Control Part - 40% in portrait, 50% width + 100% height in landscape - BOTH with app header compensation */}
<View style={controlContainerStyle}>
<View style={{
...styles.bottomOptionContainerPrortare,
width: isPortrait ?
Math.min(dimensions.controlContainer.width * 0.9, 400) :
Math.min(dimensions.controlContainer.width * 0.8, 280),
maxWidth: isPortrait ? 400 : 280,
alignSelf: 'center',
}}>
<View style={isPortrait ? styles.bottomOptTopContainerPrortare : styles.bottomOptTopContainerlandScape}>
<View style={styles.bottonOptAudio}>
<TouchableHighlight underlayColor="transparent" onPress={this.onAudio}>
<Image
tintColor={this.state.muteAudioAtJoin?'#AA336A':'#444444'}
source={this.state.muteAudioAtJoin?this.state.audioMuteImage:this.state.audioUnmuteImage}
style={{width: 40,alignSelf: "center",height: 40}} />
</TouchableHighlight>
</View>
<View style={styles.bottonOptVideo}>
<TouchableHighlight underlayColor="transparent" onPress={this.onVideo}>
<Image
tintColor={this.state.muteVideoAtJoin?'#AA336A':'#444444'}
source={this.state.muteVideoAtJoin?this.state.videoMuteImage:this.state.videoUnmuteImage}
style={{width: 40,alignSelf: "center",height: 40,}}/>
</TouchableHighlight>
</View>
<View style={styles.bottonOptAudioOnly}>
<TouchableHighlight underlayColor="transparent" onPress={this.onAudioOnly}>
<View style={styles.bottonOptInSideAudioOnly}>
<Image
tintColor={this.state.joinAsAudioOnly?'#AA336A':'#444444'}
source={this.state.joinAsAudioOnly?this.state.audioMuteImage:this.state.audioUnmuteImage}
style={styles.bottonAudioOnlyInSide} />
<Text style={{
alignSelf: "center",
textAlign:"center",
color:this.state.joinAsAudioOnly?'#AA336A':'#444444',
fontSize: isPortrait ? 16 : 14,
}}>
{"Audio Only"}
</Text>
</View>
</TouchableHighlight>
</View>
</View>
<TouchableHighlight
style={{
...styles.joinNowContainerPortrate,
alignSelf: 'center',
marginTop: 5, // Reduced top margin
marginBottom: Math.max(safeAreaInsets.bottom || 0, 10), // Reduced bottom margin
minHeight: 40, // Ensure minimum button height
}}
underlayColor="#e60073"
onPress={() => {
// 🚀 ENHANCED: Log final settings and ensure they're saved before closing
console.log("🎯 Join button pressed - Saving final settings:");
console.log("📊 Settings being saved:", {
audioMuted: this.state.muteAudioAtJoin,
videoMuted: this.state.muteVideoAtJoin,
audioOnly: this.state.joinAsAudioOnly,
frontCamera: this.state.useFontCamera
});
// Ensure all settings are properly saved to EnxSetting
EnxSetting.joinAsAudioMute(this.state.muteAudioAtJoin);
EnxSetting.joinAsVideoMute(this.state.muteVideoAtJoin);
EnxSetting.joinAsAudioOnlyCall(this.state.joinAsAudioOnly);
EnxSetting.setFontCameraPosition(this.state.useFontCamera);
console.log("✅ Settings saved to EnxSetting - closing confirmation screen");
// Verify settings were saved correctly
console.log("🔍 Verifying saved settings:", {
audioMuted: EnxSetting.getIsAudioMuted(),
videoMuted: EnxSetting.getIsVideoMuted(),
audioOnly: EnxSetting.getIsAudioOnly(),
frontCamera: EnxSetting.getCameraPosition()
});
EnxSetting.setIsShowConfirmationScreen(false);
console.log("🚪 Confirmation screen flag set to false - triggering close");
this.props.onConfirm();
}}>
<Text style={styles.joinNowtext}>{EnxSetting.getJoinText()}</Text>
</TouchableHighlight>
</View>
</View>
</View>
</SafeAreaView>
</SafeAreaProvider>
);
}
onAudio = () =>{
this.setState(prevState => {
const updated = !prevState.muteAudioAtJoin;
EnxSetting.joinAsAudioMute(updated); // ✅ use correct updated value
return { muteAudioAtJoin: updated };
});
}
onVideo = () =>{
this.setState(prevState => {
const updated = !prevState.muteVideoAtJoin;
EnxSetting.joinAsVideoMute(updated); // ✅ use correct updated value
return { muteVideoAtJoin: updated };
});
}
onAudioOnly = () =>{
this.setState(prevState => {
const updated = !prevState.joinAsAudioOnly;
EnxSetting.joinAsAudioOnlyCall(updated);
return { joinAsAudioOnly: updated , muteVideoAtJoin : updated};
});
}
onSwitchCamera =() =>{
this.setState(prevState => {
const updated = !prevState.useFontCamera;
EnxSetting.setFontCameraPosition(updated); // ✅ use correct updated value
return { useFontCamera: updated };
});
Enx.switchCameraPreview();
}
componentDidMount() {
// Initialize safe area and device detection
this.initializeSafeArea();
// Fix stream preview timing issue: Ensure camera switch happens after stream initialization
if (!this.state.useFontCamera) {
// Add delay to ensure stream is properly initialized before camera switch
setTimeout(() => {
Enx.switchCameraPreview();
}, 150);
}
}
}
export default EnxConfirmationScreen;