agora-appbuilder-core 4.1.29 → 4.1.30-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/Readme.md +2 -0
  2. package/package.json +1 -1
  3. package/template/Gulpfile.js +34 -0
  4. package/template/_package-lock.json +2491 -3084
  5. package/template/android/app/build.gradle +33 -23
  6. package/template/android/app/src/main/AndroidManifest.xml +5 -3
  7. package/template/android/app/src/main/java/com/helloworld/MainApplication.java +14 -2
  8. package/template/android/build.gradle +42 -7
  9. package/template/android/gradle/wrapper/gradle-wrapper.properties +3 -3
  10. package/template/android/gradle.properties +0 -3
  11. package/template/android/settings.gradle +4 -2
  12. package/template/babel.config.js +1 -1
  13. package/template/defaultConfig.js +2 -2
  14. package/template/ios/HelloWorld.xcodeproj/project.pbxproj +8 -6
  15. package/template/ios/Podfile +3 -45
  16. package/template/ios/Podfile.lock +1806 -524
  17. package/template/metro.config.js +6 -1
  18. package/template/package.json +28 -20
  19. package/template/patches/@supersami+rn-foreground-service+1.1.1.patch +12 -0
  20. package/template/scripts/verify-android-native-libs.py +106 -0
  21. package/template/src/AppWrapper.tsx +2 -7
  22. package/template/src/atoms/Carousel.native.tsx +22 -5
  23. package/template/src/atoms/CustomSwitch.tsx +0 -1
  24. package/template/src/atoms/Dropdown.tsx +0 -1
  25. package/template/src/atoms/MeetingLink.tsx +5 -12
  26. package/template/src/atoms/Popup.tsx +26 -2
  27. package/template/src/components/AppSafeAreaView.android.tsx +15 -0
  28. package/template/src/components/AppSafeAreaView.tsx +1 -0
  29. package/template/src/components/Precall.native.tsx +40 -10
  30. package/template/src/components/Share.tsx +27 -6
  31. package/template/src/components/meeting-info-invite/MeetingInfo.tsx +21 -3
  32. package/template/src/components/meeting-info-invite/MeetingInfoGridTile.tsx +11 -2
  33. package/template/src/components/useVideoCall.tsx +35 -1
  34. package/template/src/language/default-labels/joinScreenLabels.ts +2 -2
  35. package/template/src/pages/Create.tsx +26 -3
  36. package/template/src/pages/video-call/ActionSheet.native.tsx +11 -11
  37. package/template/src/pages/video-call/VideoCallScreen.native.tsx +34 -5
  38. package/template/src/pages/video-call/__tests__/VideoCallScreen.native.test.tsx +95 -0
  39. package/template/src/subComponents/Clipboard.native.tsx +1 -1
  40. package/template/src/subComponents/caption/CaptionIcon.tsx +11 -14
  41. package/template/src/subComponents/caption/TranscriptIcon.tsx +10 -14
  42. package/template/src/utils/useIsTablet.ts +10 -0
  43. package/template/android/app/src/debug/java/com/helloworld/ReactNativeFlipper.java +0 -75
  44. package/template/android/app/src/release/java/com/helloworld/ReactNativeFlipper.java +0 -20
@@ -16,6 +16,7 @@ import {isMobileUA} from '../../utils/common';
16
16
  import Card from '../../atoms/Card';
17
17
  import Spacer from '../../atoms/Spacer';
18
18
  import IDPLogoutComponent from '../../auth/IDPLogoutComponent';
19
+ import useIsTablet from '../../utils/useIsTablet';
19
20
 
20
21
  interface MeetingInfoProps {
21
22
  margin?: 'dense' | 'normal';
@@ -25,6 +26,7 @@ interface MeetingInfoProps {
25
26
  }
26
27
 
27
28
  const MeetingInfo = (props: MeetingInfoProps) => {
29
+ const isTabletView = useIsTablet();
28
30
  const {margin, padding, children, cardContainerStyle} = props;
29
31
  const {FpeShareComponent} = useCustomization(data => {
30
32
  let components: {
@@ -53,9 +55,12 @@ const MeetingInfo = (props: MeetingInfoProps) => {
53
55
  <Card
54
56
  margin={margin}
55
57
  padding={padding}
56
- cardContainerStyle={cardContainerStyle}>
57
- <View
58
- style={{display: 'flex', flex: 1, justifyContent: 'space-between'}}>
58
+ cardContainerStyle={
59
+ isTabletView
60
+ ? {...cardContainerStyle, ...style.tabletCard}
61
+ : cardContainerStyle
62
+ }>
63
+ <View style={[style.content, isTabletView && style.tabletContent]}>
59
64
  {children}
60
65
  </View>
61
66
  </Card>
@@ -74,4 +79,17 @@ export const style = StyleSheet.create({
74
79
  flexDirection: 'row',
75
80
  justifyContent: 'center',
76
81
  },
82
+ content: {
83
+ display: 'flex',
84
+ flex: 1,
85
+ justifyContent: 'space-between',
86
+ },
87
+ tabletContent: {
88
+ flex: 0,
89
+ justifyContent: 'flex-start',
90
+ },
91
+ tabletCard: {
92
+ flex: 0,
93
+ alignSelf: 'center',
94
+ },
77
95
  });
@@ -8,6 +8,7 @@ import Spacer from '../../atoms/Spacer';
8
8
  import TertiaryButton from '../../atoms/TertiaryButton';
9
9
  import {SHARE_LINK_CONTENT_TYPE, useShareLink} from '../useShareLink';
10
10
  import useGetName from '../../utils/useGetName';
11
+ import useIsTablet from '../../utils/useIsTablet';
11
12
  import hexadecimalTransparency from '../../utils/hexadecimalTransparency';
12
13
  import {isMobileUA, isValidReactComponent} from '../../utils/common';
13
14
  import Avatar from '../../atoms/Avatar';
@@ -23,6 +24,7 @@ const waveHandEmoji = '👋';
23
24
 
24
25
  export default function MeetingInfoGridTile() {
25
26
  const isMobile = isMobileUA();
27
+ const isTabletView = useIsTablet();
26
28
  const welcomeLabel = useString(inviteTileWelcomeText)();
27
29
  const copyInviteButtonLabel = useString(inviteTileCopyInviteBtnText)();
28
30
  const copyInviteButton = copyInviteButtonLabel;
@@ -67,7 +69,11 @@ export default function MeetingInfoGridTile() {
67
69
  style.scroll,
68
70
  !isMobile && {paddingLeft: 0, paddingRight: 0},
69
71
  ]}>
70
- <View style={style.inviteContainerTile}>
72
+ <View
73
+ style={[
74
+ style.inviteContainerTile,
75
+ isTabletView && style.tabletInviteContainerTile,
76
+ ]}>
71
77
  <View style={style.inviteTile}>
72
78
  <MeetingInfo
73
79
  padding="dense"
@@ -117,7 +123,7 @@ export default function MeetingInfoGridTile() {
117
123
  ) : (
118
124
  <>
119
125
  {isMobile ? <Spacer size={20} /> : <Spacer size={30} />}
120
- {!isMobile && (
126
+ {(!isMobile || isTabletView) && (
121
127
  <MeetingInfoLinks variant="secondary" size="tiny" />
122
128
  )}
123
129
  <View>
@@ -181,6 +187,9 @@ const style = StyleSheet.create({
181
187
  paddingHorizontal: isMobileUA() ? 0 : 35,
182
188
  borderRadius: 2,
183
189
  },
190
+ tabletInviteContainerTile: {
191
+ paddingVertical: 20,
192
+ },
184
193
  flexRow: {
185
194
  display: 'flex',
186
195
  flexDirection: 'row',
@@ -51,6 +51,8 @@ import {
51
51
  import StorageContext from './StorageContext';
52
52
  import {useString} from '../utils/useString';
53
53
  import {videoRoomUserFallbackText} from '../language/default-labels/videoCallScreenLabels';
54
+ import LanguageSelectorPopup from '../subComponents/caption/LanguageSelectorPopup';
55
+ import {LanguageType} from '../subComponents/caption/utils';
54
56
 
55
57
  interface InViewPortState {
56
58
  [key: number]: boolean;
@@ -58,6 +60,10 @@ interface InViewPortState {
58
60
  export interface VideoCallContextInterface {
59
61
  showInvitePopup: boolean;
60
62
  setShowInvitePopup: React.Dispatch<SetStateAction<boolean>>;
63
+ openSpokenLanguagePopup: (
64
+ onConfirm: (language: LanguageType) => void,
65
+ ) => void;
66
+ closeSpokenLanguagePopup: () => void;
61
67
  showStopRecordingPopup: boolean;
62
68
  setShowStopRecordingPopup: React.Dispatch<SetStateAction<boolean>>;
63
69
  showLayoutOption: boolean;
@@ -80,6 +86,8 @@ export interface VideoCallContextInterface {
80
86
  const VideoCallContext = React.createContext<VideoCallContextInterface>({
81
87
  showInvitePopup: false,
82
88
  setShowInvitePopup: () => {},
89
+ openSpokenLanguagePopup: () => {},
90
+ closeSpokenLanguagePopup: () => {},
83
91
  showStopRecordingPopup: false,
84
92
  setShowStopRecordingPopup: () => {},
85
93
  showLayoutOption: false,
@@ -108,6 +116,21 @@ const VideoCallProvider = (props: VideoCallProviderProps) => {
108
116
  const [enablePinForMe, setEnablePinForMe] = useState(true);
109
117
  const [showLayoutOption, setShowLayoutOption] = useState(false);
110
118
  const [showInvitePopup, setShowInvitePopup] = useState(false);
119
+ const [showSpokenLanguagePopup, setShowSpokenLanguagePopup] = useState(false);
120
+ const spokenLanguageConfirmRef = useRef<
121
+ ((language: LanguageType) => void) | null
122
+ >(null);
123
+ const openSpokenLanguagePopup = useCallback(
124
+ (onConfirm: (language: LanguageType) => void) => {
125
+ spokenLanguageConfirmRef.current = onConfirm;
126
+ setShowSpokenLanguagePopup(true);
127
+ },
128
+ [],
129
+ );
130
+ const closeSpokenLanguagePopup = useCallback(() => {
131
+ setShowSpokenLanguagePopup(false);
132
+ spokenLanguageConfirmRef.current = null;
133
+ }, []);
111
134
  const [showStopRecordingPopup, setShowStopRecordingPopup] = useState(false);
112
135
  const [showStartScreenSharePopup, setShowStartScreenSharePopup] =
113
136
  useState(false);
@@ -260,7 +283,8 @@ const VideoCallProvider = (props: VideoCallProviderProps) => {
260
283
  const senderDisplayName = getReactionSenderName(String(localUser.uid));
261
284
  const tonePreference = store.liveReactionSkinTone ?? 'default';
262
285
  const skinTone: SkinToneCode | undefined =
263
- tonePreference !== 'default' && reaction.skinToneVariants?.[tonePreference]
286
+ tonePreference !== 'default' &&
287
+ reaction.skinToneVariants?.[tonePreference]
264
288
  ? tonePreference
265
289
  : undefined;
266
290
  const emoji = applySkinToneToEmoji(reaction, tonePreference);
@@ -356,6 +380,8 @@ const VideoCallProvider = (props: VideoCallProviderProps) => {
356
380
  value={{
357
381
  showInvitePopup,
358
382
  setShowInvitePopup,
383
+ openSpokenLanguagePopup,
384
+ closeSpokenLanguagePopup,
359
385
  showStopRecordingPopup,
360
386
  setShowStopRecordingPopup,
361
387
  showLayoutOption,
@@ -379,6 +405,14 @@ const VideoCallProvider = (props: VideoCallProviderProps) => {
379
405
  <StopScreenSharePopup />
380
406
  <StopRecordingPopup />
381
407
  <InvitePopup />
408
+ {/* Keep toolbar dialogs outside the carousel and its translated pages. */}
409
+ {showSpokenLanguagePopup && (
410
+ <LanguageSelectorPopup
411
+ modalVisible={showSpokenLanguagePopup}
412
+ setModalVisible={setShowSpokenLanguagePopup}
413
+ onConfirm={language => spokenLanguageConfirmRef.current?.(language)}
414
+ />
415
+ )}
382
416
  <WhiteboardClearAllPopup />
383
417
  {props.children}
384
418
  </VideoCallContext.Provider>
@@ -23,9 +23,9 @@ export interface I18nJoinScreenLabelsInterface {
23
23
  export const JoinScreenLabels: I18nJoinScreenLabelsInterface = {
24
24
  [joinRoomHeading]: ({eventMode}) => {
25
25
  if (eventMode) {
26
- return 'Join a Room';
27
- } else {
28
26
  return 'Join a Stream';
27
+ } else {
28
+ return 'Join a Room';
29
29
  }
30
30
  },
31
31
  [joinRoomInputLabel]: ({eventMode}) => {
@@ -28,6 +28,7 @@ import {
28
28
  import {useCustomization} from 'customization-implementation';
29
29
  import {useString} from '../utils/useString';
30
30
  import useCreateRoom from '../utils/useCreateRoom';
31
+ import useIsTablet from '../utils/useIsTablet';
31
32
  import {CreateProvider} from './create/useCreate';
32
33
  import {
33
34
  RoomInfoDefaultValue,
@@ -64,6 +65,7 @@ import {LogSource, logger} from '../logger/AppBuilderLogger';
64
65
  import SDKEvents from '../utils/SdkEvents';
65
66
 
66
67
  const Create = () => {
68
+ const isTabletView = useIsTablet();
67
69
  const {CreateComponent, CreateHeaderRightSlotComponent} = useCustomization(
68
70
  data => {
69
71
  let components: {
@@ -315,8 +317,15 @@ const Create = () => {
315
317
  ) : (
316
318
  <></>
317
319
  )}
318
- <ScrollView contentContainerStyle={style.main}>
319
- <Card>
320
+ <ScrollView
321
+ contentContainerStyle={[
322
+ style.main,
323
+ isTabletView && style.tabletMain,
324
+ ]}>
325
+ <Card
326
+ cardContainerStyle={
327
+ isTabletView ? style.tabletCard : undefined
328
+ }>
320
329
  <View>
321
330
  <View style={style.logoContainerStyle}>
322
331
  <Logo />
@@ -435,7 +444,7 @@ const Create = () => {
435
444
  ) : (
436
445
  <></>
437
446
  )}
438
- <Spacer size={isDesktop ? 60 : 125} />
447
+ <Spacer size={isDesktop || isTabletView ? 60 : 125} />
439
448
  </View>
440
449
  <View style={[style.btnContainer]}>
441
450
  <PrimaryButton
@@ -499,6 +508,20 @@ const style = StyleSheet.create({
499
508
  flexDirection: 'row',
500
509
  justifyContent: 'center',
501
510
  },
511
+ tabletMain: {
512
+ alignItems: 'center',
513
+ padding: 24,
514
+ },
515
+ tabletCard: {
516
+ flex: 0,
517
+ alignSelf: 'center',
518
+ justifyContent: 'flex-start',
519
+ paddingHorizontal: 40,
520
+ paddingVertical: 40,
521
+ borderRadius: ThemeConfig.BorderRadius.extraLarge,
522
+ borderWidth: 1,
523
+ borderColor: $config.CARD_LAYER_3_COLOR,
524
+ },
502
525
  heading: {
503
526
  fontSize: ThemeConfig.FontSize.extraLarge,
504
527
  fontWeight: '700',
@@ -5,6 +5,7 @@ import {
5
5
  View,
6
6
  Modal,
7
7
  TouchableWithoutFeedback,
8
+ useWindowDimensions,
8
9
  } from 'react-native';
9
10
  import React, {useCallback, useRef, useState} from 'react';
10
11
  import BottomSheet, {
@@ -31,6 +32,7 @@ import {useControlPermissionMatrix} from '../../components/controls/useControlPe
31
32
  //topbar btn template is used to show icons without label text (as in desktop : bottomBar)
32
33
 
33
34
  const ActionSheet = props => {
35
+ const {width, height} = useWindowDimensions();
34
36
  const [showCustomSidePanel, setShowCustomSidePanel] = useState(false);
35
37
  const [customSidePanelIndex, setCustomSidePanelIndex] = useState<
36
38
  undefined | number
@@ -52,7 +54,7 @@ const ActionSheet = props => {
52
54
  const {snapPointsMinMax = [100, 400]} = props;
53
55
  const [isExpanded, setIsExpanded] = React.useState(false);
54
56
  const {sidePanel, setSidePanel} = useSidePanel();
55
- const bottomSheetRef = useRef<BottomSheetModal>(null);
57
+ const bottomSheetRef = useRef<BottomSheet>(null);
56
58
  const chatSheetRef = useRef<BottomSheetModal>(null);
57
59
  const participantsSheetRef = useRef<BottomSheetModal>(null);
58
60
  const settingsSheetRef = useRef<BottomSheetModal>(null);
@@ -65,10 +67,6 @@ const ActionSheet = props => {
65
67
  index === 0 ? setIsExpanded(false) : setIsExpanded(true);
66
68
  }, []);
67
69
 
68
- React.useEffect(() => {
69
- bottomSheetRef?.current.present();
70
- }, []);
71
-
72
70
  React.useEffect(() => {
73
71
  if (showCustomSidePanel) {
74
72
  customActionSheetRef?.current?.present();
@@ -117,7 +115,7 @@ const ActionSheet = props => {
117
115
  settingsSheetRef?.current?.close();
118
116
  transcriptSheetRef?.current?.close();
119
117
  customActionSheetRef?.current?.close();
120
- bottomSheetRef?.current?.present();
118
+ bottomSheetRef?.current?.snapToIndex(0);
121
119
  }, 200);
122
120
  } else {
123
121
  // Code to be executed immediately without a timer
@@ -132,7 +130,7 @@ const ActionSheet = props => {
132
130
  break;
133
131
  }
134
132
  default:
135
- bottomSheetRef?.current.present();
133
+ bottomSheetRef?.current?.snapToIndex(0);
136
134
  }
137
135
  } else {
138
136
  setShowCustomSidePanel(true);
@@ -167,14 +165,16 @@ const ActionSheet = props => {
167
165
  </TouchableWithoutFeedback>
168
166
  )}
169
167
  {/* Controls Action Sheet*/}
170
- <BottomSheetModal
168
+ <BottomSheet
169
+ // Reinitialize the animated position after rotation or window resizing.
170
+ key={`${width}-${height}`}
171
+ index={isExpanded ? 1 : 0}
171
172
  snapPoints={snapPointsMinMax}
172
173
  ref={bottomSheetRef}
173
- onChange={handleSheetChanges}
174
+ onChange={index => setIsExpanded(index > 0)}
174
175
  enablePanDownToClose={false}
175
176
  style={styles.container}
176
177
  backgroundStyle={styles.backgroundStyle}
177
- stackBehavior="push"
178
178
  handleComponent={() => (
179
179
  <>
180
180
  <ActionSheetHandle sidePanel={SidePanelType.None} />
@@ -190,7 +190,7 @@ const ActionSheet = props => {
190
190
  {...props}
191
191
  />
192
192
  </BottomSheetView>
193
- </BottomSheetModal>
193
+ </BottomSheet>
194
194
 
195
195
  {/* Chat Action Sheet */}
196
196
  {canAccessChat && (
@@ -2,13 +2,34 @@ import React from 'react';
2
2
  import {GestureHandlerRootView} from 'react-native-gesture-handler';
3
3
  import VideoCallMobileView from './VideoCallMobileView';
4
4
  import ReactNativeForegroundService from '@supersami/rn-foreground-service';
5
- import {AppRegistry, Platform} from 'react-native';
5
+ import {
6
+ AppRegistry,
7
+ AppState,
8
+ PermissionsAndroid,
9
+ Platform,
10
+ } from 'react-native';
11
+ import {RtcContext} from '../../../agora-rn-uikit';
6
12
  import {isValidReactComponent} from '../../utils/common';
7
13
  import {useCustomization} from 'customization-implementation';
8
14
 
9
15
  const VideoCallleScreen = () => {
16
+ const {rtcTracksReady} = React.useContext(RtcContext);
17
+
10
18
  React.useEffect(() => {
11
- if (Platform.OS === 'android') {
19
+ if (Platform.OS !== 'android' || !rtcTracksReady) {
20
+ return;
21
+ }
22
+
23
+ let cancelled = false;
24
+ const startCallService = async () => {
25
+ // RTC initialization requests microphone access. Android requires it to be
26
+ // granted before starting a microphone foreground service.
27
+ const canRecord = await PermissionsAndroid.check(
28
+ PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
29
+ );
30
+ if (cancelled || !canRecord || AppState.currentState !== 'active') {
31
+ return;
32
+ }
12
33
  ReactNativeForegroundService.register();
13
34
  AppRegistry.registerComponent($config.APP_NAME, () => VideoCallleScreen);
14
35
  ReactNativeForegroundService.add_task(
@@ -22,13 +43,21 @@ const VideoCallleScreen = () => {
22
43
  onError: e => console.log(`Error logging:`, e),
23
44
  },
24
45
  );
25
- ReactNativeForegroundService.start({
46
+ await ReactNativeForegroundService.start({
26
47
  id: 145,
27
48
  title: $config.APP_NAME,
28
49
  message: 'Call is active',
29
50
  });
30
- }
31
- }, []);
51
+ };
52
+
53
+ startCallService().catch(error => {
54
+ console.warn('Unable to start the call foreground service', error);
55
+ });
56
+
57
+ return () => {
58
+ cancelled = true;
59
+ };
60
+ }, [rtcTracksReady]);
32
61
 
33
62
  const {VideocallWrapper} = useCustomization(data => {
34
63
  let components: {
@@ -0,0 +1,95 @@
1
+ import React from 'react';
2
+ import TestRenderer, {act} from 'react-test-renderer';
3
+ import {AppState, PermissionsAndroid} from 'react-native';
4
+ import ForegroundService from '@supersami/rn-foreground-service';
5
+ import {RtcContext} from '../../../../agora-rn-uikit';
6
+ import VideoCallScreen from '../VideoCallScreen.native';
7
+
8
+ jest.mock('react-native', () => ({
9
+ AppRegistry: {registerComponent: jest.fn()},
10
+ AppState: {currentState: 'active'},
11
+ Platform: {OS: 'android'},
12
+ PermissionsAndroid: {
13
+ PERMISSIONS: {RECORD_AUDIO: 'android.permission.RECORD_AUDIO'},
14
+ check: jest.fn(),
15
+ },
16
+ }));
17
+ jest.mock('@supersami/rn-foreground-service', () => ({
18
+ register: jest.fn(),
19
+ add_task: jest.fn(),
20
+ start: jest.fn(async () => undefined),
21
+ }));
22
+ jest.mock('react-native-gesture-handler', () => ({
23
+ GestureHandlerRootView: ({children}: {children: React.ReactNode}) => children,
24
+ }));
25
+ jest.mock('../../../../agora-rn-uikit', () => ({
26
+ RtcContext: require('react').createContext({rtcTracksReady: false}),
27
+ }));
28
+ jest.mock('customization-implementation', () => ({
29
+ useCustomization: () => ({VideocallWrapper: require('react').Fragment}),
30
+ }));
31
+ jest.mock('../../../utils/common', () => ({isValidReactComponent: jest.fn()}));
32
+ jest.mock('../VideoCallMobileView', () => () => null);
33
+
34
+ const permissionCheck = PermissionsAndroid.check as jest.Mock;
35
+
36
+ async function renderScreen(ready: boolean) {
37
+ let renderer!: TestRenderer.ReactTestRenderer;
38
+ await act(async () => {
39
+ renderer = TestRenderer.create(
40
+ <RtcContext.Provider value={{rtcTracksReady: ready} as any}>
41
+ <VideoCallScreen />
42
+ </RtcContext.Provider>,
43
+ );
44
+ });
45
+ return renderer;
46
+ }
47
+
48
+ beforeEach(() => {
49
+ jest.clearAllMocks();
50
+ AppState.currentState = 'active';
51
+ permissionCheck.mockResolvedValue(true);
52
+ });
53
+
54
+ it('waits for RTC initialization before starting the microphone service', async () => {
55
+ const renderer = await renderScreen(false);
56
+ expect(permissionCheck).not.toHaveBeenCalled();
57
+ expect(ForegroundService.start).not.toHaveBeenCalled();
58
+ act(() => renderer.unmount());
59
+ });
60
+
61
+ it('starts the service only after microphone permission has been checked', async () => {
62
+ const renderer = await renderScreen(true);
63
+ expect(permissionCheck).toHaveBeenCalledWith(
64
+ 'android.permission.RECORD_AUDIO',
65
+ );
66
+ expect(ForegroundService.start).toHaveBeenCalledTimes(1);
67
+ act(() => renderer.unmount());
68
+ });
69
+
70
+ it('does not start a microphone service when permission is denied', async () => {
71
+ permissionCheck.mockResolvedValue(false);
72
+ const renderer = await renderScreen(true);
73
+ expect(ForegroundService.start).not.toHaveBeenCalled();
74
+ act(() => renderer.unmount());
75
+ });
76
+
77
+ it('does not start a microphone service from the background', async () => {
78
+ AppState.currentState = 'background';
79
+ const renderer = await renderScreen(true);
80
+ expect(ForegroundService.start).not.toHaveBeenCalled();
81
+ act(() => renderer.unmount());
82
+ });
83
+
84
+ it('does not start the service after the call screen has unmounted', async () => {
85
+ let resolvePermission!: (granted: boolean) => void;
86
+ permissionCheck.mockReturnValue(
87
+ new Promise(resolve => {
88
+ resolvePermission = resolve;
89
+ }),
90
+ );
91
+ const renderer = await renderScreen(true);
92
+ act(() => renderer.unmount());
93
+ await act(async () => resolvePermission(true));
94
+ expect(ForegroundService.start).not.toHaveBeenCalled();
95
+ });
@@ -13,6 +13,6 @@
13
13
  * On mobile, the clipboard is imported from the community module since react-native
14
14
  * has deprecated clipboard API as a part of the lean core effort
15
15
  */
16
- import Clipboard from '@react-native-community/clipboard';
16
+ import Clipboard from '@react-native-clipboard/clipboard';
17
17
 
18
18
  export default Clipboard;
@@ -2,7 +2,7 @@ import {View} from 'react-native';
2
2
  import React from 'react';
3
3
  import IconButton, {IconButtonProps} from '../../atoms/IconButton';
4
4
  import {useCaption} from './useCaption';
5
- import LanguageSelectorPopup from './LanguageSelectorPopup';
5
+ import {useVideoCall} from '../../components/useVideoCall';
6
6
  import {useString} from '../../utils/useString';
7
7
  import {toolbarItemCaptionText} from '../../language/default-labels/videoCallScreenLabels';
8
8
  import {useToolbarProps} from '../../atoms/ToolbarItem';
@@ -23,6 +23,7 @@ const CaptionIcon = (props: CaptionIconProps) => {
23
23
  useToolbarProps();
24
24
  const {
25
25
  showLabel = $config.ICON_TEXT,
26
+ disabled = false,
26
27
  isOnActionSheet = false,
27
28
  closeActionSheet,
28
29
  } = props;
@@ -34,8 +35,8 @@ const CaptionIcon = (props: CaptionIconProps) => {
34
35
  confirmSpokenLanguageChange,
35
36
  } = useCaption();
36
37
 
37
- const [isLanguagePopupOpen, setLanguagePopup] =
38
- React.useState<boolean>(false);
38
+ const {openSpokenLanguagePopup, closeSpokenLanguagePopup} = useVideoCall();
39
+ const isDisabled = disabled || !sttDepsReady;
39
40
 
40
41
  // const isFirstTimePopupOpen = React.useRef(false);
41
42
  // const {start, restart, isAuthorizedSTTUser} = useSTTAPI();
@@ -49,7 +50,7 @@ const CaptionIcon = (props: CaptionIconProps) => {
49
50
  setIsCaptionON(false);
50
51
  closeActionSheet?.();
51
52
  } else if (!isSTTActive) {
52
- setLanguagePopup(true);
53
+ openSpokenLanguagePopup(onConfirm);
53
54
  } else {
54
55
  setIsCaptionON(true);
55
56
  closeActionSheet?.();
@@ -65,13 +66,15 @@ const CaptionIcon = (props: CaptionIconProps) => {
65
66
  };
66
67
  const iconButtonProps: IconButtonProps = {
67
68
  onPress: onPressCustom || onPress,
68
- disabled: !sttDepsReady,
69
+ disabled: isDisabled,
69
70
  iconProps: {
70
71
  name: isCaptionON ? 'captions-off' : 'captions',
71
72
  iconBackgroundColor: isCaptionON
72
73
  ? $config.PRIMARY_ACTION_BRAND_COLOR
73
74
  : '',
74
- tintColor: isCaptionON
75
+ tintColor: isDisabled
76
+ ? $config.SEMANTIC_NEUTRAL
77
+ : isCaptionON
75
78
  ? $config.PRIMARY_ACTION_TEXT_COLOR
76
79
  : $config.SECONDARY_ACTION_COLOR,
77
80
  },
@@ -81,7 +84,7 @@ const CaptionIcon = (props: CaptionIconProps) => {
81
84
  ? labelCustom || label?.replace(' ', '\n')
82
85
  : labelCustom || label
83
86
  : '',
84
- textColor: $config.FONT_COLOR,
87
+ textColor: isDisabled ? $config.SEMANTIC_NEUTRAL : $config.FONT_COLOR,
85
88
  numberOfLines: 2,
86
89
  },
87
90
  };
@@ -93,7 +96,7 @@ const CaptionIcon = (props: CaptionIconProps) => {
93
96
  const onConfirm = async (newSpokenLang: LanguageType) => {
94
97
  try {
95
98
  closeActionSheet?.();
96
- setLanguagePopup(false);
99
+ closeSpokenLanguagePopup();
97
100
  setIsCaptionON(true);
98
101
  await confirmSpokenLanguageChange(newSpokenLang);
99
102
  } catch (error) {
@@ -106,12 +109,6 @@ const CaptionIcon = (props: CaptionIconProps) => {
106
109
  return (
107
110
  <View>
108
111
  <IconButton {...iconButtonProps} />
109
- <LanguageSelectorPopup
110
- modalVisible={isLanguagePopupOpen}
111
- setModalVisible={setLanguagePopup}
112
- onConfirm={onConfirm}
113
- // isFirstTimePopupOpen={isFirstTimePopupOpen.current}
114
- />
115
112
  </View>
116
113
  );
117
114
  };
@@ -2,7 +2,7 @@ import {View} from 'react-native';
2
2
  import React from 'react';
3
3
  import {SidePanelType, useSidePanel} from 'customization-api';
4
4
  import IconButton, {IconButtonProps} from '../../atoms/IconButton';
5
- import LanguageSelectorPopup from './LanguageSelectorPopup';
5
+ import {useVideoCall} from '../../components/useVideoCall';
6
6
  import {useCaption} from './useCaption';
7
7
  import {useString} from '../../utils/useString';
8
8
  import {toolbarItemTranscriptText} from '../../language/default-labels/videoCallScreenLabels';
@@ -31,8 +31,8 @@ const TranscriptIcon = (props: TranscriptIconProps) => {
31
31
  // const {start, restart, isAuthorizedTranscriptUser} = useSTTAPI();
32
32
  const {isSTTActive, sttDepsReady, confirmSpokenLanguageChange} = useCaption();
33
33
  // const isDisabled = !isAuthorizedTranscriptUser();
34
- const [isLanguagePopupOpen, setLanguagePopup] =
35
- React.useState<boolean>(false);
34
+ const {openSpokenLanguagePopup, closeSpokenLanguagePopup} = useVideoCall();
35
+ const isDisabled = disabled || !sttDepsReady;
36
36
  // const isFirstTimePopupOpen = React.useRef(false);
37
37
 
38
38
  const isTranscriptON = sidePanel === SidePanelType.Transcript;
@@ -42,7 +42,7 @@ const TranscriptIcon = (props: TranscriptIconProps) => {
42
42
  if (isTranscriptON) {
43
43
  setSidePanel(SidePanelType.None);
44
44
  } else if (!isSTTActive) {
45
- setLanguagePopup(true);
45
+ openSpokenLanguagePopup(onConfirm);
46
46
  } else {
47
47
  setSidePanel(SidePanelType.Transcript);
48
48
  }
@@ -56,18 +56,20 @@ const TranscriptIcon = (props: TranscriptIconProps) => {
56
56
  iconBackgroundColor: isTranscriptON
57
57
  ? $config.PRIMARY_ACTION_BRAND_COLOR
58
58
  : '',
59
- tintColor: isTranscriptON
59
+ tintColor: isDisabled
60
+ ? $config.SEMANTIC_NEUTRAL
61
+ : isTranscriptON
60
62
  ? $config.PRIMARY_ACTION_TEXT_COLOR
61
63
  : $config.SECONDARY_ACTION_COLOR,
62
64
  },
63
- disabled: disabled || !sttDepsReady,
65
+ disabled: isDisabled,
64
66
  btnTextProps: {
65
67
  text: showLabel
66
68
  ? isOnActionSheet
67
69
  ? labelCustom || label(isTranscriptON)?.replace(' ', '\n')
68
70
  : labelCustom || label(isTranscriptON)
69
71
  : '',
70
- textColor: $config.FONT_COLOR,
72
+ textColor: isDisabled ? $config.SEMANTIC_NEUTRAL : $config.FONT_COLOR,
71
73
  numberOfLines: 2,
72
74
  },
73
75
  };
@@ -81,7 +83,7 @@ const TranscriptIcon = (props: TranscriptIconProps) => {
81
83
  // const method = isTranscriptON ? 'stop' : 'start';
82
84
  // if (method === 'stop') return; // not closing the stt service as it will stop for whole channel
83
85
  // if (method === 'start' && isSTTActive === true) return; // not triggering the start service if STT Service already started by anyone else in the channel
84
- setLanguagePopup(false);
86
+ closeSpokenLanguagePopup();
85
87
  try {
86
88
  // const res = await start(language, userOwnLanguages);
87
89
  // if (res?.message.includes('STARTED')) {
@@ -98,12 +100,6 @@ const TranscriptIcon = (props: TranscriptIconProps) => {
98
100
  return (
99
101
  <View>
100
102
  <IconButton {...iconButtonProps} />
101
- <LanguageSelectorPopup
102
- modalVisible={isLanguagePopupOpen}
103
- setModalVisible={setLanguagePopup}
104
- onConfirm={onConfirm}
105
- // isFirstTimePopupOpen={isFirstTimePopupOpen.current}
106
- />
107
103
  </View>
108
104
  );
109
105
  };
@@ -0,0 +1,10 @@
1
+ import {Platform, useWindowDimensions} from 'react-native';
2
+
3
+ const TABLET_MIN_DIMENSION = 600;
4
+
5
+ export default function useIsTablet() {
6
+ const {width, height} = useWindowDimensions();
7
+ return (
8
+ Platform.OS !== 'web' && Math.min(width, height) >= TABLET_MIN_DIMENSION
9
+ );
10
+ }