diff --git a/.eslintrc.js b/.eslintrc.js index 1f0105d18..963e50abf 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -2,7 +2,7 @@ module.exports = { settings: { 'import/resolver': { node: { - extensions: ['.ts', '.tsx', '.js', '.ios.js', '.android.js', '.native.js'] + extensions: ['.ts', '.tsx', '.js', '.ios.js', '.android.js', '.native.js', '.ios.tsx', '.android.tsx'] } } }, diff --git a/android/app/build.gradle b/android/app/build.gradle index 493b9874d..fe11088a0 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -357,9 +357,6 @@ dependencies { playImplementation project(':@react-native-firebase_app') playImplementation project(':@react-native-firebase_analytics') playImplementation project(':@react-native-firebase_crashlytics') - implementation(project(':react-native-jitsi-meet')) { // https://github.com/skrafft/react-native-jitsi-meet#side-note - exclude group: 'com.facebook.react',module:'react-native-svg' - } implementation fileTree(dir: "libs", include: ["*.jar"]) //noinspection GradleDynamicVersion diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 9c72e6b32..0765164fb 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -5,6 +5,12 @@ + + + + + + - + + + + + + diff --git a/android/build.gradle b/android/build.gradle index 8680080c2..b05e6a6f1 100644 --- a/android/build.gradle +++ b/android/build.gradle @@ -26,8 +26,6 @@ buildscript { kotlinVersion = '1.6.10' supportLibVersion = "28.0.0" libre_build = !(isPlay.toBoolean()) - jitsi_url = "https://github.com/RocketChat/jitsi-maven-repository/raw/master/releases" - jitsi_version = "3.7.0" } repositories { @@ -68,9 +66,6 @@ allprojects { url "$rootDir/../node_modules/detox/Detox-android" } - maven { - url jitsi_url - } mavenCentral { content { excludeGroup "com.facebook.react" diff --git a/app/lib/methods/videoConf.ts b/app/lib/methods/videoConf.ts index f10b2ad14..13c604164 100644 --- a/app/lib/methods/videoConf.ts +++ b/app/lib/methods/videoConf.ts @@ -1,14 +1,22 @@ -import navigation from '../navigation/appNavigation'; -import openLink from './helpers/openLink'; -import { Services } from '../services'; -import log from './helpers/log'; -import { showErrorAlert } from './helpers'; +import { PermissionsAndroid } from 'react-native'; + import i18n from '../../i18n'; +import navigation from '../navigation/appNavigation'; +import { Services } from '../services'; +import { isAndroid, showErrorAlert } from './helpers'; +import log from './helpers/log'; +import openLink from './helpers/openLink'; export const videoConfJoin = async (callId: string, cam: boolean) => { try { const result = await Services.videoConferenceJoin(callId, cam); if (result.success) { + if (isAndroid) { + await PermissionsAndroid.requestMultiple([ + PermissionsAndroid.PERMISSIONS.CAMERA, + PermissionsAndroid.PERMISSIONS.RECORD_AUDIO + ]); + } const { url, providerName } = result; if (providerName === 'jitsi') { navigation.navigate('JitsiMeetView', { url, onlyAudio: !cam, videoConf: true }); diff --git a/app/stacks/InsideStack.tsx b/app/stacks/InsideStack.tsx index 0b6475640..9d9899393 100644 --- a/app/stacks/InsideStack.tsx +++ b/app/stacks/InsideStack.tsx @@ -80,6 +80,7 @@ import { ProfileStackParamList, SettingsStackParamList } from './types'; +import { isIOS } from '../lib/methods/helpers'; // ChatsStackNavigator const ChatsStack = createStackNavigator(); @@ -135,7 +136,11 @@ const ChatsStackNavigator = () => { - + ); }; diff --git a/app/stacks/MasterDetailStack/index.tsx b/app/stacks/MasterDetailStack/index.tsx index 695efc3b9..0b315a284 100644 --- a/app/stacks/MasterDetailStack/index.tsx +++ b/app/stacks/MasterDetailStack/index.tsx @@ -73,6 +73,7 @@ import { MasterDetailInsideStackParamList, ModalStackParamList } from './types'; +import { isIOS } from '../../lib/methods/helpers'; // ChatsStackNavigator const ChatsStack = createStackNavigator(); @@ -222,7 +223,11 @@ const InsideStackNavigator = React.memo(() => { - + ); diff --git a/app/views/JitsiMeetView.android.tsx b/app/views/JitsiMeetView.android.tsx new file mode 100644 index 000000000..344a656af --- /dev/null +++ b/app/views/JitsiMeetView.android.tsx @@ -0,0 +1,102 @@ +import React from 'react'; +import { BackHandler, NativeEventSubscription } from 'react-native'; +import BackgroundTimer from 'react-native-background-timer'; +import { isAppInstalled, openAppWithUri } from 'react-native-send-intent'; +import WebView from 'react-native-webview'; +import { WebViewMessage, WebViewNavigation } from 'react-native-webview/lib/WebViewTypes'; + +import { IBaseScreen } from '../definitions'; +import { events, logEvent } from '../lib/methods/helpers/log'; +import { Services } from '../lib/services'; +import { ChatsStackParamList } from '../stacks/types'; +import { withTheme } from '../theme'; + +const JITSI_INTENT = 'org.jitsi.meet'; + +type TJitsiMeetViewProps = IBaseScreen; + +class JitsiMeetView extends React.Component { + private rid: string; + private url: string; + private videoConf: boolean; + private jitsiTimeout: number | null; + private backHandler!: NativeEventSubscription; + + constructor(props: TJitsiMeetViewProps) { + super(props); + this.rid = props.route.params?.rid; + this.url = props.route.params?.url; + this.videoConf = !!props.route.params?.videoConf; + this.jitsiTimeout = null; + } + + componentDidMount() { + const { route, navigation } = this.props; + isAppInstalled(JITSI_INTENT) + .then(function (isInstalled) { + if (isInstalled) { + const callUrl = route.params.url.replace(/^https?:\/\//, '').split('#')[0]; + openAppWithUri(`intent://${callUrl}#Intent;scheme=${JITSI_INTENT};package=${JITSI_INTENT};end`) + .then(() => navigation.pop()) + .catch(() => {}); + } + }) + .catch(() => {}); + this.onConferenceJoined(); + this.backHandler = BackHandler.addEventListener('hardwareBackPress', () => true); + } + + componentWillUnmount() { + logEvent(this.videoConf ? events.LIVECHAT_VIDEOCONF_TERMINATE : events.JM_CONFERENCE_TERMINATE); + if (this.jitsiTimeout && !this.videoConf) { + BackgroundTimer.clearInterval(this.jitsiTimeout); + this.jitsiTimeout = null; + BackgroundTimer.stopBackgroundTimer(); + } + this.backHandler.remove(); + } + + // Jitsi Update Timeout needs to be called every 10 seconds to make sure + // call is not ended and is available to web users. + onConferenceJoined = () => { + logEvent(this.videoConf ? events.LIVECHAT_VIDEOCONF_JOIN : events.JM_CONFERENCE_JOIN); + if (this.rid && !this.videoConf) { + Services.updateJitsiTimeout(this.rid).catch((e: unknown) => console.log(e)); + if (this.jitsiTimeout) { + BackgroundTimer.clearInterval(this.jitsiTimeout); + BackgroundTimer.stopBackgroundTimer(); + this.jitsiTimeout = null; + } + this.jitsiTimeout = BackgroundTimer.setInterval(() => { + Services.updateJitsiTimeout(this.rid).catch((e: unknown) => console.log(e)); + }, 10000); + } + }; + + onNavigationStateChange = (webViewState: WebViewNavigation | WebViewMessage) => { + const { navigation, route } = this.props; + const jitsiRoomId = route.params.url + ?.split(/^https?:\/\//)[1] + ?.split('#')[0] + ?.split('/')[1]; + if ((jitsiRoomId && !webViewState.url.includes(jitsiRoomId)) || webViewState.url.includes('close')) { + navigation.pop(); + } + }; + + render() { + return ( + this.onNavigationStateChange(nativeEvent)} + onNavigationStateChange={this.onNavigationStateChange} + style={{ flex: 1 }} + javaScriptEnabled + domStorageEnabled + mediaPlaybackRequiresUserAction={false} + /> + ); + } +} + +export default withTheme(JitsiMeetView); diff --git a/app/views/JitsiMeetView.d.ts b/app/views/JitsiMeetView.d.ts new file mode 100644 index 000000000..c1ad42e90 --- /dev/null +++ b/app/views/JitsiMeetView.d.ts @@ -0,0 +1,5 @@ +import React from 'react'; + +declare const JitsiMeetView: React.SFC<>; + +export default JitsiMeetView; diff --git a/app/views/JitsiMeetView.tsx b/app/views/JitsiMeetView.ios.tsx similarity index 78% rename from app/views/JitsiMeetView.tsx rename to app/views/JitsiMeetView.ios.tsx index a4af6b835..60cf7198a 100644 --- a/app/views/JitsiMeetView.tsx +++ b/app/views/JitsiMeetView.ios.tsx @@ -1,17 +1,16 @@ import React from 'react'; -import { BackHandler, StyleSheet } from 'react-native'; -import JitsiMeet, { JitsiMeetView as RNJitsiMeetView } from 'react-native-jitsi-meet'; +import { StyleSheet } from 'react-native'; import BackgroundTimer from 'react-native-background-timer'; +import JitsiMeet, { JitsiMeetView as RNJitsiMeetView } from 'react-native-jitsi-meet'; import { connect } from 'react-redux'; -import { getUserSelector } from '../selectors/login'; -import ActivityIndicator from '../containers/ActivityIndicator'; +import RCActivityIndicator from '../containers/ActivityIndicator'; +import { IApplicationState, IBaseScreen, IUser } from '../definitions'; import { events, logEvent } from '../lib/methods/helpers/log'; -import { isAndroid, isIOS } from '../lib/methods/helpers'; -import { withTheme } from '../theme'; -import { ChatsStackParamList } from '../stacks/types'; -import { IApplicationState, IUser, IBaseScreen } from '../definitions'; import { Services } from '../lib/services'; +import { getUserSelector } from '../selectors/login'; +import { ChatsStackParamList } from '../stacks/types'; +import { withTheme } from '../theme'; const formatUrl = (url: string, baseUrl: string, uriSize: number, avatarAuthURLFragment: string) => `${baseUrl}/avatar/${url}?format=png&width=${uriSize}&height=${uriSize}${avatarAuthURLFragment}`; @@ -60,20 +59,14 @@ class JitsiMeetView extends React.Component { + const onlyAudio = route.params?.onlyAudio ?? false; + if (onlyAudio) { + JitsiMeet.audioCall(this.url, userInfo); + } else { + JitsiMeet.call(this.url, userInfo); + } this.setState({ loading: false }); }, 1000); - - if (isIOS) { - setTimeout(() => { - const onlyAudio = route.params?.onlyAudio ?? false; - if (onlyAudio) { - JitsiMeet.audioCall(this.url, userInfo); - } else { - JitsiMeet.call(this.url, userInfo); - } - }, 1000); - } - BackHandler.addEventListener('hardwareBackPress', () => null); } componentWillUnmount() { @@ -83,16 +76,8 @@ class JitsiMeetView extends React.Component null); - if (isIOS) { - JitsiMeet.endCall(); - } - } - - endCall = () => { JitsiMeet.endCall(); - return null; - }; + } onConferenceWillJoin = () => { this.setState({ loading: false }); @@ -117,8 +102,8 @@ class JitsiMeetView extends React.Component { - logEvent(this.videoConf ? events.LIVECHAT_VIDEOCONF_TERMINATE : events.JM_CONFERENCE_TERMINATE); const { navigation } = this.props; + logEvent(this.videoConf ? events.LIVECHAT_VIDEOCONF_TERMINATE : events.JM_CONFERENCE_TERMINATE); // fix to go back when the call ends setTimeout(() => { JitsiMeet.endCall(); @@ -127,10 +112,8 @@ class JitsiMeetView extends React.Component - {loading ? : null} + {loading ? : null} ); } diff --git a/package.json b/package.json index c2c2dc2ef..04b433fc5 100644 --- a/package.json +++ b/package.json @@ -118,6 +118,7 @@ "react-native-safe-area-context": "3.2.0", "react-native-screens": "3.13.1", "react-native-scrollable-tab-view": "ptomasroos/react-native-scrollable-tab-view", + "react-native-send-intent": "^1.3.0", "react-native-simple-crypto": "RocketChat/react-native-simple-crypto#0.5.1", "react-native-skeleton-placeholder": "^5.2.3", "react-native-slowlog": "^1.0.2", diff --git a/react-native.config.js b/react-native.config.js index 9e78b41ad..1dbd95952 100644 --- a/react-native.config.js +++ b/react-native.config.js @@ -14,6 +14,11 @@ module.exports = { platforms: { android: null } + }, + 'react-native-jitsi-meet': { + platforms: { + android: null + } } } }; diff --git a/yarn.lock b/yarn.lock index 124d75a28..bc9ac843c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17330,6 +17330,11 @@ react-native-scrollable-tab-view@ptomasroos/react-native-scrollable-tab-view: prop-types "^15.6.0" react-timer-mixin "^0.13.3" +react-native-send-intent@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/react-native-send-intent/-/react-native-send-intent-1.3.0.tgz#d8c7898827da1b8b10e25a645ce6802d1a0b440c" + integrity sha512-ODTX7BHITFxdcAL0K2iHfa3qVYnqG8GPcv1NbLBNC1DyCaOSJiiGtVH6Kc5YBqzQ8+1pV9uN5nfQ5wyFgiq74g== + react-native-simple-crypto@RocketChat/react-native-simple-crypto#0.5.1: version "0.5.1" resolved "https://codeload.github.com/RocketChat/react-native-simple-crypto/tar.gz/dcf6eef5359c739d521371918e13a73f2ea6cb42"