UNPKG

enx-uikit-react-native

Version:
1,408 lines (1,281 loc) 182 kB
import React, { PureComponent } from "react"; import { EnxSetting, EnxPageSlideEventName } from ".."; import { View, Animated, Button, Dimensions, Easing, TouchableOpacity, Text, TouchableWithoutFeedback, TouchableHighlight, ToastAndroid, FlatList, Image, Switch, Alert, Modal, ScrollView, NativeModules, Platform, StatusBar } from "react-native"; // Safe area imports with fallback handling for EnxVideoView let SafeAreaView; let SafeAreaProvider; let Edge; let useSafeAreaInsets; let hasSafeArea = false; try { const safeAreaModule = require('react-native-safe-area-context'); SafeAreaView = safeAreaModule.SafeAreaView; SafeAreaProvider = safeAreaModule.SafeAreaProvider; Edge = safeAreaModule.Edge; useSafeAreaInsets = safeAreaModule.useSafeAreaInsets; hasSafeArea = true; console.log('✅ EnxVideoView: Safe area context available'); } catch (error) { console.warn('⚠️ EnxVideoView: Safe area context not available, using fallback'); SafeAreaView = View; SafeAreaProvider = ({ children }) => React.createElement(React.Fragment, {}, children); hasSafeArea = false; } import EnxParticipantScreen from "./EnxParticipantScreen"; import EnxChatScreen from "./EnxChatScreen"; import EnxAudioOnlyView from "./EnxAudioOnlyView"; import { styles } from "../style/EnxVideoViewStyle"; import "./ignoreWarnings" import Blink from './Blink' import Dialog from "react-native-dialog"; import EnxConfirmationScreen from "./EnxConfirmationScreen" import EnxBottomView from "./EnxBottomView"; import EnxMoreScreen from "./EnxMoreScreen"; import EnxRoomSetting from "./EnxRoomSetting"; import EnxLobbyView from "./EnxLobbyView"; import EnxQnaScreen from "./EnxQnaScreen"; import EnxPollingScreen from "./EnxPollingScreen"; import EnxCreatePollScreen from "./EnxCreatePollScreen" import PollAnswerDialog from './PollAnswerDialog'; import PieChart from '../commonClass/EnxPieChart' import EnxPollResultDialog from './EnxPollResultDialog' import AnswerDialog from "./AnswerDialog"; import { pick } from "underscore"; import { EnxRoom, Enx, EnxStream, EnxPlayerView, EnxToolBarView } from "enx-rtc-react-native"; import SelectDropdown from 'react-native-select-dropdown'; import { EnxPubMode, EnxPubType } from "enx-rtc-react-native/src/Enx"; import { Modalize } from 'react-native-modalize'; const AUDIO_OUTPUT_VOLUME_LEVEL = 0.9; const calculateRow = (data) => { if (data.length == 1) return 1; else if (data.length == 2 || data.length == 3 || data.length == 4) return 2 else if (data.length == 5 || data.length == 6) return 3 else if (data.length == 7 || data.length == 8) return 4 else if (data.length == 9 || data.length == 10 || data.length > 10) return 5 } const calculateColumn = data => { if (data.length == 1 || data.length == 2 || data.length === 3) return 1; else if (data.length >= 4 && data.length <= 6) return 2; else if (data.length >= 7 && data.length <= 9) return 3; else if (data.length >= 10) return 3; // Max 3 columns for better visibility else return 2; } const showAlert = (message) => { Alert.alert("Info", message, [{ text: "OK" }]); }; const { width, height } = Dimensions.get('window'); export default class EnxVideoView extends PureComponent { // Calculate optimal video dimensions based on participant count and orientation calculateVideoDimensions = () => { const isLandscape = this.state.windowWidth > this.state.windowHeight; const safeInsets = this.state.safeAreaInsets || { top: 0, bottom: 0, left: 0, right: 0 }; const streamCount = this.state.activeTalkerStreams.length; // Calculate usable screen space with minimal padding for full screen effect const topPadding = (safeInsets.top || 0) + 2; const bottomPadding = (safeInsets.bottom || 0) + (this.state.isToolBarsVisible && !this.state.showAudioOnlyView ? 75 : 5); const leftPadding = safeInsets.left || 0; const rightPadding = safeInsets.right || 0; const usableWidth = this.state.windowWidth - leftPadding - rightPadding; const usableHeight = this.state.windowHeight - topPadding - bottomPadding; let layoutConfig = this.getLayoutConfig(streamCount, isLandscape, usableWidth, usableHeight); return { ...layoutConfig, usableWidth, usableHeight }; }; // Get layout configuration for different stream counts and orientations getLayoutConfig = (streamCount, isLandscape, usableWidth, usableHeight) => { const margin = 1; // Minimal margin for full screen effect if (streamCount === 1) { return { videoWidth: usableWidth - (margin * 2), videoHeight: usableHeight - (margin * 2), rows: 1, columns: 1, isSpecialLayout: false }; } else if (streamCount === 2) { if (isLandscape) { // Landscape: side by side (width/2) return { videoWidth: Math.floor((usableWidth - (margin * 3)) / 2), videoHeight: usableHeight - (margin * 2), rows: 1, columns: 2, isSpecialLayout: false }; } else { // Portrait: top and bottom (height/2) return { videoWidth: usableWidth - (margin * 2), videoHeight: Math.floor((usableHeight - (margin * 3)) / 2), rows: 2, columns: 1, isSpecialLayout: false }; } } else if (streamCount === 3) { if (isLandscape) { // Landscape: 3 columns, full height return { videoWidth: Math.floor((usableWidth - (margin * 4)) / 3), videoHeight: usableHeight - (margin * 2), rows: 1, columns: 3, isSpecialLayout: false }; } else { // Portrait: 2 on top, 1 full width on bottom return { videoWidth: Math.floor((usableWidth - (margin * 3)) / 2), videoHeight: Math.floor((usableHeight - (margin * 3)) / 2), videoWidthSingle: usableWidth - (margin * 2), // Full width for 3rd video rows: 2, columns: 2, isSpecialLayout: true, specialLayoutType: 'three_portrait' }; } } else if (streamCount === 4) { // 2x2 grid in both orientations return { videoWidth: Math.floor((usableWidth - (margin * 3)) / 2), videoHeight: Math.floor((usableHeight - (margin * 3)) / 2), rows: 2, columns: 2, isSpecialLayout: false }; } else if (streamCount === 5) { if (isLandscape) { // Landscape: 5 videos in 2 rows, full height return { videoWidth: Math.floor((usableWidth - (margin * 4)) / 3), videoHeight: Math.floor((usableHeight - (margin * 3)) / 2), videoWidthTwo: Math.floor((usableWidth - (margin * 3)) / 2), // For 2 videos in second row rows: 2, columns: 3, isSpecialLayout: true, specialLayoutType: 'five_landscape' }; } else { // Portrait: 5 videos, full width return { videoWidth: Math.floor((usableWidth - (margin * 3)) / 2), videoHeight: Math.floor((usableHeight - (margin * 4)) / 3), videoWidthSingle: usableWidth - (margin * 2), // Full width for 5th video rows: 3, columns: 2, isSpecialLayout: true, specialLayoutType: 'five_portrait' }; } } else if (streamCount <= 6) { if (isLandscape) { // Landscape: 3x2 grid return { videoWidth: Math.floor((usableWidth - (margin * 4)) / 3), videoHeight: Math.floor((usableHeight - (margin * 3)) / 2), rows: 2, columns: 3, isSpecialLayout: false }; } else { // Portrait: 2x3 grid return { videoWidth: Math.floor((usableWidth - (margin * 3)) / 2), videoHeight: Math.floor((usableHeight - (margin * 4)) / 3), rows: 3, columns: 2, isSpecialLayout: false }; } } else { if (isLandscape) { // Landscape: 4x2 grid for more videos return { videoWidth: Math.floor((usableWidth - (margin * 5)) / 4), videoHeight: Math.floor((usableHeight - (margin * 3)) / 2), rows: 2, columns: 4, isSpecialLayout: false }; } else { // Portrait: 2x4 grid for more videos return { videoWidth: Math.floor((usableWidth - (margin * 3)) / 2), videoHeight: Math.floor((usableHeight - (margin * 5)) / 4), rows: 4, columns: 2, isSpecialLayout: false }; } } }; renderItem = ({ item, index }) => { // Use the same render method for consistency return this.renderVideoItem({ item, index }); }; renderVideoItem = ({ item, index }) => { const dimensions = this.calculateVideoDimensions(); const streamCount = this.state.activeTalkerStreams.length; let videoStyle = { backgroundColor: 'black', width: dimensions.videoWidth, height: dimensions.videoHeight, margin: 1, borderRadius: 4, }; // Handle special layouts if (dimensions.isSpecialLayout) { if (dimensions.specialLayoutType === 'three_portrait' && index === 2) { // 3rd video in portrait mode - full width videoStyle.width = dimensions.videoWidthSingle; } else if (dimensions.specialLayoutType === 'five_landscape') { if (index >= 3) { // Last 2 videos in landscape mode - different width videoStyle.width = dimensions.videoWidthTwo; } } else if (dimensions.specialLayoutType === 'five_portrait' && index === 4) { // 5th video in portrait mode - full width videoStyle.width = dimensions.videoWidthSingle; } } try { return ( <EnxPlayerView style={videoStyle} key={String(item.streamId)} streamId={String(item.streamId)} isLocal="remote" /> ); } catch (err) { console.log(err.message); return null; } }; // Render video grid with optimal layout renderVideoGrid = () => { const dimensions = this.calculateVideoDimensions(); const isLandscape = this.state.windowWidth > this.state.windowHeight; const streamCount = this.state.activeTalkerStreams.length; // Handle special layouts that need custom rendering if (dimensions.isSpecialLayout) { return this.renderSpecialLayout(dimensions, streamCount, isLandscape); } return ( <TouchableWithoutFeedback onPress={this.toggleToolbar}> <View style={styles.videoGridContainer}> <FlatList key={`grid-${dimensions.columns}-${isLandscape ? 'L' : 'P'}-${streamCount}`} extraData={this.state.refresh} data={this.state.activeTalkerStreams} renderItem={this.renderVideoItem} numColumns={dimensions.columns} showsHorizontalScrollIndicator={false} showsVerticalScrollIndicator={false} scrollEnabled={false} contentContainerStyle={styles.videoGridContent} style={styles.videoGridList} /> </View> </TouchableWithoutFeedback> ); } // Handle special layouts for 3 and 5 videos renderSpecialLayout = (dimensions, streamCount, isLandscape) => { if (dimensions.specialLayoutType === 'three_portrait') { // 2 videos on top, 1 full width on bottom return ( <TouchableWithoutFeedback onPress={this.toggleToolbar}> <View style={styles.videoGridContainer}> <View style={styles.specialLayoutRow}> {this.state.activeTalkerStreams.slice(0, 2).map((item, index) => this.renderVideoItem({ item, index }) )} </View> <View style={styles.specialLayoutRow}> {this.renderVideoItem({ item: this.state.activeTalkerStreams[2], index: 2 })} </View> </View> </TouchableWithoutFeedback> ); } else if (dimensions.specialLayoutType === 'five_landscape') { // 3 videos on top, 2 videos on bottom return ( <TouchableWithoutFeedback onPress={this.toggleToolbar}> <View style={styles.videoGridContainer}> <View style={styles.specialLayoutRow}> {this.state.activeTalkerStreams.slice(0, 3).map((item, index) => this.renderVideoItem({ item, index }) )} </View> <View style={styles.specialLayoutRow}> {this.state.activeTalkerStreams.slice(3, 5).map((item, index) => this.renderVideoItem({ item, index: index + 3 }) )} </View> </View> </TouchableWithoutFeedback> ); } else if (dimensions.specialLayoutType === 'five_portrait') { // 2x2 grid + 1 full width return ( <TouchableWithoutFeedback onPress={this.toggleToolbar}> <View style={styles.videoGridContainer}> <View style={styles.specialLayoutRow}> {this.state.activeTalkerStreams.slice(0, 2).map((item, index) => this.renderVideoItem({ item, index }) )} </View> <View style={styles.specialLayoutRow}> {this.state.activeTalkerStreams.slice(2, 4).map((item, index) => this.renderVideoItem({ item, index: index + 2 }) )} </View> <View style={styles.specialLayoutRow}> {this.renderVideoItem({ item: this.state.activeTalkerStreams[4], index: 4 })} </View> </View> </TouchableWithoutFeedback> ); } return null; } // Get adaptive spacing based on app header visibility getAdaptiveSpacing = () => { try { const headerStatus = this.getCompleteHeaderStatus(); const appHeaderHeight = headerStatus.appHeader.totalAppHeaderHeight; const isAppHeaderVisible = headerStatus.appHeader.isNavigationHeaderVisible || headerStatus.appHeader.hasCustomTopBar; return { isAppHeaderVisible, appHeaderHeight, topPadding: isAppHeaderVisible ? Math.max(appHeaderHeight - 20, 0) : 0, bottomPadding: this.state.isToolBarsVisible && !this.state.showAudioOnlyView ? 85 : 20, totalReservedSpace: (isAppHeaderVisible ? appHeaderHeight : 0) + (this.state.isToolBarsVisible && !this.state.showAudioOnlyView ? 85 : 20) }; } catch (error) { // Fallback spacing if header detection fails const fallbackHeaderHeight = Platform.OS === 'android' ? (StatusBar.currentHeight || 24) + 56 : 76; return { isAppHeaderVisible: true, appHeaderHeight: fallbackHeaderHeight, topPadding: fallbackHeaderHeight - 20, bottomPadding: this.state.isToolBarsVisible && !this.state.showAudioOnlyView ? 85 : 20, totalReservedSpace: fallbackHeaderHeight + (this.state.isToolBarsVisible && !this.state.showAudioOnlyView ? 85 : 20) }; } }; // Toggle toolbar visibility with layout recalculation toggleToolbar = () => { this.setState({ isToolBarsVisible: !this.state.isToolBarsVisible }, () => { // Force re-render to apply new spacing this.forceUpdate(); // Log spacing info for debugging const spacing = this.getAdaptiveSpacing(); console.log("🔄 Adaptive Spacing After Toggle:", spacing); }); }; createLandScapeView() { console.log("=======design landscape view =============") try { return ( <HorizontalList data={this.state.activeTalkerStreams}> </HorizontalList> ); } catch (err) { console.log(err.message) } } constructor(props) { super(props); // Create a ref to access EnxBottomView's methods this.bottomViewRef = React.createRef(); //Create a ref to access EnxParticipant Screen this.partRef = React.createRef(); //Create a ref to access EnxMore Screen this.moreRef = React.createRef(); this.pollRef = React.createRef(); this.qnaRef = React.createRef(); this.pollAnswerRef = React.createRef(); // Ref for audio-only view this.audioOnlyRef = React.createRef(); const screenHeight = Dimensions.get("window").height; this.state = { currentOverlay: null, // Screens that will be overlaid the pratent screen animationType: "horizontal", // Track the animation direction slideAnim: new Animated.Value(Dimensions.get("window").width), // Slide from left to right or vice versa slideAnimHeight: new Animated.Value(Dimensions.get("window").height), //// Slide animation from bottom upaniminationSize: 0, // Set position of transcation from up to down slideAnimationSize: Dimensions.get("window").width, //this will set X cordinate for animation bottomScreenHeight: new Animated.Value(85), // Initial height of EnxButtomScreen isBottomScreenExpanded: false, // Track whether the bottom screen is expanded windowHeight: screenHeight, // Store current screen height - This is for orientation windowWidth: Dimensions.get("window").width, // Store current screen width- This is for orientation animationDuration: 300, screenWindowHeight: Dimensions.get("window").height * 0.3, // Safe area state variables isPortrait: screenHeight >= Dimensions.get("window").width, statusBarHeight: Platform.OS === 'android' ? StatusBar.currentHeight || 24 : 0, hasNotch: false, safeAreaInsets: { top: 0, bottom: 0, left: 0, right: 0 }, role: 'moderator', localStreamId: '', confNumber: '', selfUserName: '', senderId: '', selfUserRef: '', clientID: '123-123', isConnected: false, activeTalkerStreams: [], awaitedParticipantList: [], audioMuteUnmuteCheck: true, videoMuteUnmuteCheck: true, rotateCamera: false, participantList: [], chatModelList: [],// store chat Model groupChatModel: [],// store group chat Model privateChatModel: [],// store private chat Model selfClientId: "", participantClientId: "", chatType: '', muteRoomCheck: false, recordingCheck: false, recordingImage: require("../image_asset/recording_on.png"), isRoomAwaited: false, awaitedParticipantList: [], isLobbyDialog: false, isRoomSetting: false, isModerator: false, shareModeSelected: "", screenShareMode: "", screenShareIndex: 0, screenShareState: "", shareClientId: "", requestClientId: "", screenShareModeGranted: false, isShareRequested: false, shareRequestList: [], floorRequestList: [], participantFloorAction: 0, // Equivalent to private var participantFloorAction = 0 currentClientPosition: -1, isRequest: false, mActiveStreamId: null, heightOfShareView: '0%', widthOfShareView: '0%', selectedDevice: "Earpiece", deviceList: [], // When true show the audio-only UI instead of video surface showAudioOnlyView: false, isSwitchMedia: false, isChatViewVisible: false, isMoreVisible: false, screenShareId: null, screenShareCheck: false, isToolBarsVisible: true, isShowAnswerDialog: false, isShowResultDialog: false, isShowTypeAnswerDialog: false, isRequestIntiatedForAnnotation: false, typeQnaObject: {}, pollResultData: {}, question: '', pollData: {}, answers: [], duration: 0, pollsList: [], qnaList: [] }; // Add orientation change listener this.addOrientationObserver(); } // Remove orientation listener componentWillUnmount() { this.removeOrientationObserver(); } // Check if app header is visible 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 static options // 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:', error); } return appHeaderInfo; }; // Get comprehensive header status including app header getCompleteHeaderStatus = () => { const appHeaderInfo = this.checkAppHeader(); const toolbarInfo = { isToolBarsVisible: this.state.isToolBarsVisible, recordingIndicator: this.state.recordingCheck, lobbyView: this.state.isRoomAwaited }; return { appHeader: appHeaderInfo, internalToolbars: toolbarInfo, combinedHeaderHeight: appHeaderInfo.totalAppHeaderHeight + (toolbarInfo.isToolBarsVisible ? 60 : 0), hasAnyHeader: appHeaderInfo.isNavigationHeaderVisible || appHeaderInfo.hasCustomTopBar || toolbarInfo.isToolBarsVisible || toolbarInfo.recordingIndicator }; }; // Initialize safe area detection and configuration initializeSafeArea = () => { const { width, height } = Dimensions.get('window'); const hasNotch = this.detectNotch(height, width); const statusBarHeight = Platform.OS === 'android' ? StatusBar.currentHeight || 24 : 0; this.setState({ hasNotch, statusBarHeight, isPortrait: height >= width, safeAreaInsets: this.calculateSafeAreaInsets(hasNotch, statusBarHeight) }); this.configureStatusBar(); // Log app header status after initialization console.log("🎯 App Header Status:", this.getCompleteHeaderStatus()); } // Detect if device has notch or punch hole detectNotch = (height, width) => { const maxDimension = Math.max(height, width); const minDimension = Math.min(height, width); const aspectRatio = maxDimension / minDimension; if (Platform.OS === 'ios') { return aspectRatio > 2.1 || maxDimension >= 812; } else { // Common Android notch/punch-hole screen heights const androidNotchHeight = [800, 822, 844, 846, 851, 854, 869, 896, 926, 915, 932]; return aspectRatio > 2.0 || androidNotchHeight.includes(maxDimension); } } // Calculate safe area insets for fallback calculateSafeAreaInsets = (hasNotch, statusBarHeight) => { const isPortrait = this.state ? this.state.isPortrait : true; if (Platform.OS === 'ios') { return { top: hasNotch ? 44 : 20, bottom: hasNotch ? (isPortrait ? 34 : 21) : 0, left: 0, right: 0 }; } else { return { top: statusBarHeight, bottom: hasNotch ? 24 : 0, left: 0, right: 0 }; } } // Configure status bar for optimal appearance configureStatusBar = () => { if (Platform.OS === 'android') { if (hasSafeArea) { StatusBar.setBackgroundColor('transparent', true); StatusBar.setTranslucent(true); } else { StatusBar.setBackgroundColor('rgba(0, 0, 0, 0.7)', true); StatusBar.setTranslucent(false); } } StatusBar.setBarStyle('light-content', true); } addOrientationObserver = () => { // Add orientation change listener this.orientationChangeListener = Dimensions.addEventListener("change", this.onOrientationChange); } //Handle notification lisner from EnxSetting class handleVideoEvent = (data) => { console.log('Received event:', data); }; //removeOrientation Decetation observer removeOrientationObserver = () => { if (this.orientationChangeListener && this.orientationChangeListener.remove) { this.orientationChangeListener.remove(); } EnxSetting.removeEventListener('videoEvent', this.handleVideoEvent); } // Handle orientation changes to adapt the floating view's height onOrientationChange = ({ window: { height, width } }) => { const { isBottomScreenExpanded } = this.state; console.log(`🔄 Orientation change detected: ${width}x${height}`); // Update safe area calculations for new orientation const hasNotch = this.detectNotch(height, width); const statusBarHeight = Platform.OS === 'android' ? StatusBar.currentHeight || 24 : 0; const isPortrait = height >= width; const safeAreaInsets = this.calculateSafeAreaInsets(hasNotch, statusBarHeight); // Update height and width based on new orientation this.setState({ windowHeight: height, windowWidth: width, isPortrait, hasNotch, statusBarHeight, safeAreaInsets, slideAnimationSize: this.state.currentOverlay == 'more' ? width + 10 : -width, slideAnim: new Animated.Value(width), // Reset slide animation based on new width slideAnimHeight: new Animated.Value(height), // Reset slide animation height animationDuration: 0, // Update screen window height for landscape calculations screenWindowHeight: height * 0.3, }); // Adjust bottom screen height based on orientation and device type const isLandscape = width > height; const targetHeight = isBottomScreenExpanded ? (isLandscape ? height * 0.6 : height * 0.75) // Smaller in landscape : (isLandscape ? 70 : 85); // Reduced bottom bar in landscape this.setState({ bottomScreenHeight: new Animated.Value(targetHeight) }); // Reconfigure status bar for new orientation this.configureStatusBar(); // Handle overlay repositioning if (this.state.currentOverlay != null) { setTimeout(() => { if (this.state.animationType == 'horizontal') { this.navigateToIn(this.state.currentOverlay) } else { this.navigateToUp(this.state.currentOverlay) } }, 50); } }; //Check flag for confirmation screen componentDidMount() { Enx.initRoom(); // Initialize safe area and check app header this.initializeSafeArea(); // Check EnxSetting to see if we should show the confirmation screen by default if (EnxSetting.getIsShowConfirmationScreen()) { // If the confirmation screen needs to be shown by default, navigate to it console.log("🎯 Showing confirmation screen first"); this.setState({ currentOverlay: "confirmation", animationType: "horizontal", slideAnimationSize: (this.state.currentOverlay == 'more' || this.state.currentOverlay == 'Room-Setting' || this.state.currentOverlay == 'QNA-Page' || this.state.currentOverlay == 'Polling-Page') ? Dimensions.get("window").width + 10 : - Dimensions.get("window").width }, this.runSlideInAnimation); } else { // 🚀 FIXED: If no confirmation screen needed, proceed directly with video view console.log("🚀 No confirmation screen needed - video view ready"); // Trigger refresh to ensure getLocalStream() and getRoomInfo() are ready with current settings this.refreshStreamAndRoomValues(); } //Add listener for notification EnxSetting.addEventListener('videoEvent', this.handleVideoEvent); // Log initial app header status setTimeout(() => { const headerStatus = this.getCompleteHeaderStatus(); console.log("📱 Initial App Header Check:", headerStatus); }, 1000); } //changes animationDuration if click to load page, not load page with orientation changeDuration = () => { this.setState({ animationDuration: 300 }) } //Set Click Action MEthod for slide Animination //Here we are changing the duration first and then animating setActionMethodForInAnimation = (screenName) => { this.changeDuration() // Delay the second method by 2 seconds (2000 milliseconds) setTimeout(() => { this.navigateToIn(screenName) }, 100); // Delay in milliseconds } //Set Click Action MEthod for slideOut Animination //Here we are changing the duration first and then animating setActionMethodForOutAnimation = () => { this.changeDuration() this.setState({ isMoreVisible: false }) // Delay the second method by 2 seconds (2000 milliseconds) setTimeout(() => { this.runSlideOutAnimation() }, 100); // Delay in milliseconds } //Set Click Action MEthod for Up Animination //Here we are changing the duration first and then animating setActionMethodForUpAnimation = (screenName) => { this.changeDuration() // Delay the second method by 2 seconds (2000 milliseconds) setTimeout(() => { this.navigateToUp(screenName) }, 100); // Delay in milliseconds } //Here we are changing the duration first and then animating setActionMethodForDownAnimation = () => { this.changeDuration() // Delay the second method by 2 seconds (2000 milliseconds) setTimeout(() => { this.runSlideDownAnimation() }, 100); // Delay in milliseconds } // Navigate and trigger the slide-in/out animation navigateToIn = (screenName) => { this.setState({ currentOverlay: screenName, animationType: "horizontal", slideAnimationSize: (screenName == 'more' || screenName == 'Room-Setting' || screenName == 'QNA-Page' || screenName == 'Polling-Page' || screenName == 'createPoll') ? Dimensions.get("window").width + 10 : - Dimensions.get("window").width }, this.runSlideInAnimation); } // Navigate and trigger the slide-up/Down animation navigateToUp = (screenName) => { this.setState({ currentOverlay: screenName, animationType: "vertical", upaniminationSize: 0 }, this.runSlideUpAnimation); } // Slide-in animation for overlay runSlideInAnimation = () => { this.state.slideAnim.setValue(this.state.slideAnimationSize); // Start off-screen Animated.timing(this.state.slideAnim, { toValue: 0, // Slide to center duration: this.state.animationDuration, useNativeDriver: true, easing: Easing.ease, }).start(() => { //this.removeOrientationObserver() if (this.state.currentOverlay == 'chat') { this.props.onPageSlide(EnxPageSlideEventName.EnxChat, true); } else if (this.state.currentOverlay == 'QNA-Page') { this.props.onPageSlide(EnxPageSlideEventName.EnxQnA, true); } else if (this.state.currentOverlay == 'Polling-Page') { this.props.onPageSlide(EnxPageSlideEventName.EnxPolling, true); } else if (this.state.currentOverlay == 'createPoll') { this.props.onPageSlide(EnxPageSlideEventName.EnxCreatePoll, true); } }); }; // Slide-out animation when closing the overlay runSlideOutAnimation = (callback) => { Animated.timing(this.state.slideAnim, { toValue: this.state.slideAnimationSize, // Slide off-screen duration: this.state.animationDuration, useNativeDriver: true, easing: Easing.ease, }).start(() => { //this.addOrientationObserver(); if (this.state.currentOverlay == 'chat') { this.props.onPageSlide(EnxPageSlideEventName.EnxChat, false); } else if (this.state.currentOverlay == 'QNA-Page') { this.props.onPageSlide(EnxPageSlideEventName.EnxQnA, false); } else if (this.state.currentOverlay == 'Polling-Page') { this.props.onPageSlide(EnxPageSlideEventName.EnxPolling, false); } else if (this.state.currentOverlay == 'createPoll') { this.props.onPageSlide(EnxPageSlideEventName.EnxCreatePoll, false); } else if (this.state.currentOverlay == 'confirmation') { // 🚀 FIXED: Refresh values when confirmation screen closes console.log("✅ Confirmation screen closed - refreshing stream and room values"); this.refreshStreamAndRoomValues(); // 🚀 ADDED: Trigger room connection after confirmation closes setTimeout(() => { console.log("🔗 Initiating room connection after confirmation close"); // The existing connection logic will use the refreshed getLocalStream() values if (this.props.token) { console.log("📞 Token available - connection can proceed"); // Notify parent that we're ready to connect (if needed) // The actual connection happens via the existing SDK flow } else { console.warn("⚠️ No token available for connection"); } }, 300); // Small delay to ensure state updates are completed } this.setState({ currentOverlay: null }); // Remove overlay after animation if (callback) callback(); }); }; // Slide-up animation for overlay runSlideUpAnimation = () => { this.state.slideAnimHeight.setValue(Dimensions.get("window").height); // Start up-screen Animated.timing(this.state.slideAnimHeight, { toValue: this.state.upaniminationSize, // Slide to up duration: this.state.animationDuration, useNativeDriver: true, easing: Easing.ease, }).start(() => { //this.removeOrientationObserver() }); }; // Slide-Down animation when closing the overlay runSlideDownAnimation = (callback) => { Animated.timing(this.state.slideAnimHeight, { toValue: Dimensions.get("window").height, // Slide Down-screen duration: this.state.animationDuration, useNativeDriver: true, easing: Easing.ease, }).start(() => { //this.addOrientationObserver(); this.setState({ currentOverlay: null }); // Remove overlay after animation if (callback) callback(); }); }; //Load confrence screen renderConfirmationScreen = () => ( <EnxConfirmationScreen onConfirm={() => this.setActionMethodForOutAnimation()} /> ); //Load Chat screen renderChatScreen = () => ( // <EnxChatScreen onBack={() => this.runSlideDownAnimation()} /> <EnxChatScreen onBack={() => this.setActionMethodForOutAnimation()} data={this.state.chatType === 'group' ? this.state.groupChatModel : this.state.privateChatModel} selfClientId={this.state.selfClientId} participantClientId={this.state.participantClientId} chatType={this.state.chatType} sendMessageInGroup={this.sendMessageInGroup} shareFile={this.shareFile} downloadFile={this.downloadFile} /> ); //Load participant screen renderDetailsScreen = () => ( <EnxParticipantScreen onBack={() => this.setActionMethodForOutAnimation()} /> ); //Load More screen renderMoreScreen = () => ( <EnxMoreScreen ref={this.moreRef} onAction={this.handleEventsAction} onBack={() => this.setActionMethodForOutAnimation()} /> ); //Load Room Setting Page renderRoomSetting = () => ( <EnxRoomSetting onBack={() => this.setActionMethodForOutAnimation()} /> ); renderQNAScreen = () => ( <EnxQnaScreen onBack={() => this.setActionMethodForOutAnimation()} ref={this.qnaRef} qnaData={this.state.qnaList} onQnaAction={this.qnaAction} /> ); renderPollingScreen = () => ( <EnxPollingScreen onBack={() => this.setActionMethodForOutAnimation()} ref={this.pollRef} pollData={this.state.pollsList} onNavigateToCreatePoll={() => this.setActionMethodForInAnimation('createPoll')} // Open Create Poll onPollAction={this.pollAction} /> ); updatePoll = (pollType, data) => { if (pollType == 'poll-response') { const updatedPollsList = this.updatePollList(this.state.pollsList, data); this.setState({ pollsList: updatedPollsList }, () => { if (this.pollRef.current) { this.pollRef.current.updateAction(this.state.pollsList); } }); } else if (pollType == 'poll-start') { console.log('stat poll1', data) let updatedData = this.convertPollData(data) this.setState({ isShowAnswerDialog: true, question: data.question, duration: Number(data.duration), answers: updatedData.answers, pollData: updatedData }) } else if (pollType == 'poll-extend') { if (this.pollAnswerRef.current) { this.pollAnswerRef.current.increaseTime(Number(data.extended_duration)); } } else if (pollType == 'poll-stop') { this.handleCancel() } else if (pollType == 'poll-data') { let resultData = this.getStructuredData(data) let pollData = { question: data.question, data: resultData }; this.setState({ pollResultData: pollData, isShowResultDialog: true }) } } convertPollData(input) { const answers = Object.keys(input.options).map((key, index) => ({ id: `opt${index + 1}`, option: input.options[key], })); return { question: input.question, answers: answers, duration: Number(input.duration), id: input.id, timestamp: input.timestamp, userName: input.user_name, userRef: input.user_ref, }; } // Function to update poll list updatePollList(pollsList, response) { return pollsList.map((poll) => { if (poll.id == response.id) { const updatedData = poll.data.map((item) => { console.log("matchID", ` "${item.option}" ${response.optionSelectedValue}`) if (item.option === response.optionSelectedValue) { return { ...item, votes: item.votes + 1 }; } return item; }); const totalVotes = updatedData.reduce((sum, item) => sum + item.votes, 0); const dataWithPercentages = updatedData.map((item) => ({ ...item, percentage: totalVotes > 0 ? ((item.votes / totalVotes) * 100).toFixed(2) : 0, })); return { ...poll, data: dataWithPercentages, total_result: totalVotes, }; } return poll; }); } pollAction = (data, polltype) => { if (polltype == 'poll-start') { // {"type":"poll-start","data":{"duration":"50","question":"Android","id":-584306009,"options":{"opt1":"java","opt2":"kotlin"},"result":{"opt1":0,"opt2":0},"total_result":0,"initialDuration":"50"}} // {"question":"Bzzg","data":[{"option":"Bxzv","votes":0,"percentage":0},{"option":"Dbxb","votes":0,"percentage":0}],"duration":"585","expanded":true} //Generate dynamic keys (opt1, opt2, opt3, etc.) and map to result let transformedData = { result: {}, options: {} }; const originalData = data.data; // Loop through the data and dynamically create "optX" keys originalData.forEach((item, index) => { // Create dynamic option names: opt1, opt2, opt3, etc. const optionKey = `opt${index + 1}`; transformedData.options[optionKey] = item.option; // Assign option name (java, kotlin, etc.) transformedData.result[optionKey] = item.votes; // Assign votes for each option }); transformedData['duration'] = data.duration transformedData['question'] = data.question transformedData['total_result'] = 0 transformedData['initialDuration'] = data.duration transformedData['id'] = data.id let pollData = { type: polltype, data: transformedData }; console.log(pollData); Enx.sendUserData(pollData, true, []) this.setState(prevState => { const updatedList = prevState.pollsList.map(poll => poll.id === data.id ? { ...poll, status: "stopPoll" } : poll ); return { pollsList: updatedList }; }, () => { // This callback ensures state is updated if (this.pollRef.current) { this.pollRef.current.updateAction(this.state.pollsList); } }); } else if (polltype == 'poll-stop') { let pollData = { type: polltype, data: { id: data.id } }; Enx.sendUserData(pollData, true, []) this.setState(prevState => { const updatedList = prevState.pollsList.map(poll => poll.id === data.id ? { ...poll, status: "Completed" } : poll ); return { pollsList: updatedList }; }, () => { // This callback ensures state is updated if (this.pollRef.current) { this.pollRef.current.updateAction(this.state.pollsList); } }); } else if (polltype == 'extend-duration') { let pollData = { type: 'poll-extend', id: data.id, extended_duration: 10 }; Enx.sendUserData(pollData, true, []) } else if (polltype == 'publish-result') { const transformedPollData = this.transformPollForResultData(data); let pollData = { type: 'poll-data', data: transformedPollData, }; Enx.sendUserData(pollData, true, []) } else if (polltype == 'repoll') { const newPoll = { ...data, // Replace with your current poll data id: Date.now(), // Generate a new unique ID status: "stopPoll", // Reset status to default data: data.data.map((item) => ({ ...item, votes: 0, // Reset votes percentage: "0.00", // Reset percentage })), total_result: 0, // Reset total votes count expanded: true, // Mark this poll as expanded by default }; let transformedData = { result: {}, options: {} }; console.log("start poll :", ` "${JSON.stringify(newPoll.data)}" ${polltype}`) // Loop through the data and dynamically create "optX" keys newPoll.data.forEach((item, index) => { // Create dynamic option names: opt1, opt2, opt3, etc. const optionKey = `opt${index + 1}`; transformedData.options[optionKey] = item.option; // Assign option name (java, kotlin, etc.) transformedData.result[optionKey] = 0; // Assign votes for each option }); transformedData['duration'] = newPoll.duration transformedData['question'] = newPoll.question transformedData['total_result'] = 0 transformedData['initialDuration'] = newPoll.duration transformedData['id'] = newPoll.id let pollData = { type: 'poll-start', data: transformedData }; console.log(pollData); Enx.sendUserData(pollData, true, []) this.setState((prevState) => ({ pollsList: prevState.pollsList .map((poll, index) => ({ ...poll, expanded: false, // Collapse all existing polls })) .concat(newPoll), // Add the new poll at the end }), () => { // This callback ensures state is updated if (this.pollRef.current) { this.pollRef.current.updateAction(this.state.pollsList); } }); // this.setState(prevState => { // const updatedList = prevState.pollsList.map(poll => // poll.id === data.id ? { ...poll, status: "stopPoll" } : poll // ); // return { pollsList: updatedList }; // }, () => { // // This callback ensures state is updated // if (this.pollRef.current) { // this.pollRef.current.updateAction(this.state.pollsList); // } // }); } } qnaAction = (qnadata, qnatype) => { switch (qnatype) { case 'new-question': qnadata.qna.clientID = this.state.selfClientId qnadata.qna.username = this.state.selfUserName qnadata.qna.user_ref = this.state.selfUserRef //console.log("qnaType3", JSON.stringify(data)); console.log("qnaType2", "" + JSON.stringify(qnadata) + "") let clientIds = []; Enx.getUserList(data => { for (var i = 0; i < data.length; i++) { if (data[i].role === 'moderator') { if (data[i].clientId !== this.state.selfClientId) { clientIds.push(data[i].clientId); } } } if (clientIds.length > 0) { this.saveLocallyQnAData(qnadata) Enx.sendUserData(qnadata, false, clientIds) } // this.state.localStreamId = status; }); break; case 'answer_declined': let qnaDeclined = { type: 'answer_declined', qna: { id: qnadata.id, private: false, status: 'D', ans: { id: Date.now().toString(), timestamp: Date.now().toString(), private: false, note: 'Declined', clientID: this.state.selfClientId, username: this.state.selfUserName, user_ref: this.state.selfUserRef, } } } Enx.sendUserData(qnaDeclined, true, []); this.updateLocallyQnAData(qnaDeclined, 'answer_declined') break; case 'answered_live': let qnaObject = { type: 'answered_live', qna: { id: qnadata.id, private: false, status: 'A', ans: { id: Date.now().toString(), timestamp: Date.now().toString(), private: false, note: 'Answered on live call', clientID: this.state.selfClientId, username: this.state.selfUserName, user_ref: this.state.selfUserRef, } } } Enx.sendUserData(qnaObject, true, []); this.updateLocallyQnAData(qnaObject, "answered_live") break; case 'answered_typed': this.setState({ isShowTypeAnswerDialog: true, typeQnaObject: qnadata }) break; case 'delete-question': Enx.sendUserData(qnadata, true, []) let qnaId = qnadata.qnaId; console.log("ide", qnadata) this.removeItemById(qnaId) break; } } removeItemById = (idToRemove) => { const idToRemoveString = String(idToRemove); this.setState((prevState) => { // Create a new copy of the qnaList const qnaList = [...prevState.qnaList]; const index = qnaList.findIndex(item => item.qna.id === idToRemoveString); if (index !== -1) { // Remove the item at the found index qnaList.splice(index, 1); } // After updating state, call the reference method if (this.qnaRef.current) { this.qnaRef.current.updateAction(qnaList, this.state.isModerator); // Pass the updated qnaList } // Return the updated state return { qnaList }; }); }; updateLocallyQnAData = (qnaData, type) => { console.log("updateQna", JSON.stringify(qnaData), type); // Use the spread operator to create a new array for immutability const updatedQnaList = this.state.qnaList.map((qnaItem) => { if (qnaItem.qna.id === qnaData.qna.id) { // Conditionally rename `note` to `ans` only if the type is `answer_declined` const answerEntry = { ...qnaData.qna.ans, ...(type === 'answer_declined' || type === 'answered_live' ? { ans: qnaData.qna.ans.note, note: undefined } : {}) }; let finalobject = { id: qnaData.qna.id, timestamp: qnaItem.qna.timestamp, question: qnaItem.qna.question, status: qnaItem.qna.status, clientID: qnaItem.qna.clientID, username: qnaItem.qna.username, user_ref: qnaItem.qna.user_ref, isInitiator: false, ans: [...qnaItem.qna.answer, answerEntry] } this.setCustomData(true, finalobject, qnaData.qna.id) // Return updated item with new answer and status return { ...qnaItem, qna: { ...qnaItem.qna, status: qnaData.qna.status, // Update status answer: [...qnaItem.qna.answer, answerEntry] // Append new answer } }; } return qnaItem; // Return unmodified item if ID doesn't match }); // Update the state with the modified list this.setState({ qnaList: updatedQnaList }, () => { // Use the updated qnaList directly after state update if (this.qnaRef.current) { this.qnaRef.current.updateAction(updatedQnaList, this.state.isModerator); } }); }; saveLocallyQnAData = (qnaData) => { // Convert input data to the required format and add to the list this.setState((prevState) => ({ qnaList: [ ...prevState.qnaList, { "type": qnaData.type, "qna": { ...qnaData.qna, "answer": [] // Add 'answer' key as an empty array } } ] })); let qna = { id: qnaData.qna.id, timestamp: qnaData.qna.timestamp, question: qnaData.qna.question, s