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.
- package/Readme.md +2 -0
- package/package.json +1 -1
- package/template/Gulpfile.js +34 -0
- package/template/_package-lock.json +2491 -3084
- package/template/android/app/build.gradle +33 -23
- package/template/android/app/src/main/AndroidManifest.xml +5 -3
- package/template/android/app/src/main/java/com/helloworld/MainApplication.java +14 -2
- package/template/android/build.gradle +42 -7
- package/template/android/gradle/wrapper/gradle-wrapper.properties +3 -3
- package/template/android/gradle.properties +0 -3
- package/template/android/settings.gradle +4 -2
- package/template/babel.config.js +1 -1
- package/template/defaultConfig.js +2 -2
- package/template/ios/HelloWorld.xcodeproj/project.pbxproj +8 -6
- package/template/ios/Podfile +3 -45
- package/template/ios/Podfile.lock +1806 -524
- package/template/metro.config.js +6 -1
- package/template/package.json +28 -20
- package/template/patches/@supersami+rn-foreground-service+1.1.1.patch +12 -0
- package/template/scripts/verify-android-native-libs.py +106 -0
- package/template/src/AppWrapper.tsx +2 -7
- package/template/src/atoms/Carousel.native.tsx +22 -5
- package/template/src/atoms/CustomSwitch.tsx +0 -1
- package/template/src/atoms/Dropdown.tsx +0 -1
- package/template/src/atoms/MeetingLink.tsx +5 -12
- package/template/src/atoms/Popup.tsx +26 -2
- package/template/src/components/AppSafeAreaView.android.tsx +15 -0
- package/template/src/components/AppSafeAreaView.tsx +1 -0
- package/template/src/components/Precall.native.tsx +40 -10
- package/template/src/components/Share.tsx +27 -6
- package/template/src/components/meeting-info-invite/MeetingInfo.tsx +21 -3
- package/template/src/components/meeting-info-invite/MeetingInfoGridTile.tsx +11 -2
- package/template/src/components/useVideoCall.tsx +35 -1
- package/template/src/language/default-labels/joinScreenLabels.ts +2 -2
- package/template/src/pages/Create.tsx +26 -3
- package/template/src/pages/video-call/ActionSheet.native.tsx +11 -11
- package/template/src/pages/video-call/VideoCallScreen.native.tsx +34 -5
- package/template/src/pages/video-call/__tests__/VideoCallScreen.native.test.tsx +95 -0
- package/template/src/subComponents/Clipboard.native.tsx +1 -1
- package/template/src/subComponents/caption/CaptionIcon.tsx +11 -14
- package/template/src/subComponents/caption/TranscriptIcon.tsx +10 -14
- package/template/src/utils/useIsTablet.ts +10 -0
- package/template/android/app/src/debug/java/com/helloworld/ReactNativeFlipper.java +0 -75
- 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={
|
|
57
|
-
|
|
58
|
-
|
|
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
|
|
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' &&
|
|
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
|
|
319
|
-
|
|
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<
|
|
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?.
|
|
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
|
|
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
|
-
<
|
|
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={
|
|
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
|
-
</
|
|
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 {
|
|
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
|
|
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-
|
|
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
|
|
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
|
|
38
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
|
35
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
+
}
|