UNPKG

enx-uikit-react-native

Version:
653 lines (596 loc) 27.7 kB
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;