enx-uikit-react-native
Version:
It is a react native component for Enablex users.
1,408 lines (1,281 loc) • 182 kB
JavaScript
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