diff --git a/android/sdk/build.gradle b/android/sdk/build.gradle index 755c4de3e428e..d473292822cd0 100644 --- a/android/sdk/build.gradle +++ b/android/sdk/build.gradle @@ -56,6 +56,7 @@ dependencies { implementation project(':react-native-background-timer') implementation project(':react-native-calendar-events') implementation project(':react-native-community-async-storage') + implementation project(':react-native-community_netinfo') implementation project(':react-native-immersive') implementation project(':react-native-keep-awake') implementation project(':react-native-linear-gradient') diff --git a/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java b/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java index 0ff120c8b3cf9..42cfd5dae2ec5 100644 --- a/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java +++ b/android/sdk/src/main/java/org/jitsi/meet/sdk/ReactInstanceManagerHolder.java @@ -193,6 +193,7 @@ static void initReactInstanceManager(Activity activity) { new com.oblador.vectoricons.VectorIconsPackage(), new com.ocetnik.timer.BackgroundTimerPackage(), new com.reactnativecommunity.asyncstorage.AsyncStoragePackage(), + new com.reactnativecommunity.netinfo.NetInfoPackage(), new com.reactnativecommunity.webview.RNCWebViewPackage(), new com.rnimmersive.RNImmersivePackage(), new com.zmxv.RNSound.RNSoundPackage(), diff --git a/android/settings.gradle b/android/settings.gradle index e123e0fb6737e..4aaedbdb1a6a2 100644 --- a/android/settings.gradle +++ b/android/settings.gradle @@ -7,6 +7,8 @@ include ':react-native-calendar-events' project(':react-native-calendar-events').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-calendar-events/android') include ':react-native-community-async-storage' project(':react-native-community-async-storage').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/async-storage/android') +include ':react-native-community_netinfo' +project(':react-native-community_netinfo').projectDir = new File(rootProject.projectDir, '../node_modules/@react-native-community/netinfo/android') include ':react-native-google-signin' project(':react-native-google-signin').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-google-signin/android') include ':react-native-immersive' diff --git a/ios/Podfile b/ios/Podfile index bd7fb705fcb2b..80165ca1628f8 100644 --- a/ios/Podfile +++ b/ios/Podfile @@ -47,6 +47,7 @@ target 'JitsiMeet' do pod 'react-native-background-timer', :path => '../node_modules/react-native-background-timer' pod 'react-native-calendar-events', :path => '../node_modules/react-native-calendar-events' pod 'react-native-keep-awake', :path => '../node_modules/react-native-keep-awake' + pod 'react-native-netinfo', :path => '../node_modules/@react-native-community/netinfo' pod 'react-native-webview', :path => '../node_modules/react-native-webview' pod 'react-native-webrtc', :path => '../node_modules/react-native-webrtc' pod 'BVLinearGradient', :path => '../node_modules/react-native-linear-gradient' diff --git a/ios/Podfile.lock b/ios/Podfile.lock index a2b7e1ec82458..2e1010f29163a 100644 --- a/ios/Podfile.lock +++ b/ios/Podfile.lock @@ -139,6 +139,8 @@ PODS: - React - react-native-keep-awake (4.0.0): - React + - react-native-netinfo (4.1.5): + - React - react-native-webrtc (1.69.2): - React - react-native-webview (5.8.1): @@ -203,6 +205,7 @@ DEPENDENCIES: - react-native-background-timer (from `../node_modules/react-native-background-timer`) - react-native-calendar-events (from `../node_modules/react-native-calendar-events`) - react-native-keep-awake (from `../node_modules/react-native-keep-awake`) + - react-native-netinfo (from `../node_modules/@react-native-community/netinfo`) - react-native-webrtc (from `../node_modules/react-native-webrtc`) - react-native-webview (from `../node_modules/react-native-webview`) - React-RCTActionSheet (from `../node_modules/react-native/Libraries/ActionSheetIOS`) @@ -271,6 +274,8 @@ EXTERNAL SOURCES: :path: "../node_modules/react-native-calendar-events" react-native-keep-awake: :path: "../node_modules/react-native-keep-awake" + react-native-netinfo: + :path: "../node_modules/@react-native-community/netinfo" react-native-webrtc: :path: "../node_modules/react-native-webrtc" react-native-webview: @@ -340,6 +345,7 @@ SPEC CHECKSUMS: react-native-background-timer: 0d34748e53a972507c66963490c775321a88f6f2 react-native-calendar-events: 2fe35a9294af05de0ed819d3a1b5dac048d2c010 react-native-keep-awake: eba3137546b10003361b37c761f6c429b59814ae + react-native-netinfo: 8d8db463bcc5db66a8ac5c48a7d86beb3b92f61a react-native-webrtc: 1415d2a54b2246dd85ba95eb3e4bf2b66533f951 react-native-webview: a95842e3f351a6d2c8bc8bcc9eab689c7e7e5ad4 React-RCTActionSheet: b0f1ea83f4bf75fb966eae9bfc47b78c8d3efd90 @@ -359,6 +365,6 @@ SPEC CHECKSUMS: RNWatch: 09738b339eceb66e4d80a2371633ca5fb380fa42 yoga: 312528f5bbbba37b4dcea5ef00e8b4033fdd9411 -PODFILE CHECKSUM: 6b6e260b4be4e86f9d05c0d7dab40f60118bb355 +PODFILE CHECKSUM: 0907bfe60b5b5f11dbdc6b4e65d40a248d000513 COCOAPODS: 1.7.2 diff --git a/package-lock.json b/package-lock.json index ccbcdfc89646b..63365fac08c7a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3055,6 +3055,11 @@ } } }, + "@react-native-community/netinfo": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@react-native-community/netinfo/-/netinfo-4.1.5.tgz", + "integrity": "sha512-lagdZr9UiVAccNXYfTEj+aUcPCx9ykbMe9puffeIyF3JsRuMmlu3BjHYx1klUHX7wNRmFNC8qVP0puxUt1sZ0A==" + }, "@segment/top-domain": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/@segment/top-domain/-/top-domain-3.0.0.tgz", diff --git a/package.json b/package.json index af0e7467af614..0c529339e7f0e 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "@atlaskit/tooltip": "12.1.13", "@microsoft/microsoft-graph-client": "1.1.0", "@react-native-community/async-storage": "1.3.4", + "@react-native-community/netinfo": "4.1.5", "@tensorflow-models/body-pix": "^1.0.1", "@tensorflow/tfjs": "^1.1.2", "@webcomponents/url": "0.7.1", diff --git a/react/features/analytics/AnalyticsEvents.js b/react/features/analytics/AnalyticsEvents.js index 1d4f92cb0949c..ed8b8214459de 100644 --- a/react/features/analytics/AnalyticsEvents.js +++ b/react/features/analytics/AnalyticsEvents.js @@ -271,6 +271,27 @@ export function createInviteDialogEvent( }; } +/** + * Creates an event which reports about the current network information reported by the operating system. + * + * @param {boolean} isOnline - Tells whether or not the internet is reachable. + * @param {string} [networkType] - Network type, see {@code NetworkInfo} type defined by the 'base/net-info' feature. + * @param {Object} [details] - Extra info, see {@code NetworkInfo} type defined by the 'base/net-info' feature. + * @returns {Object} + */ +export function createNetworkInfoEvent({ isOnline, networkType, details }) { + const attributes = { isOnline }; + + // Do no include optional stuff or Amplitude handler will log warnings. + networkType && (attributes.networkType = networkType); + details && (attributes.details = details); + + return { + action: 'network.info', + attributes + }; +} + /** * Creates an "offer/answer failure" event. * diff --git a/react/features/analytics/middleware.js b/react/features/analytics/middleware.js index 9267abce72478..59f9db4aa8f18 100644 --- a/react/features/analytics/middleware.js +++ b/react/features/analytics/middleware.js @@ -6,6 +6,7 @@ import { SET_ROOM } from '../base/conference'; import { SET_CONFIG } from '../base/config'; +import { SET_NETWORK_INFO } from '../base/net-info'; import { MiddlewareRegistry } from '../base/redux'; import { getLocalAudioTrack, @@ -16,7 +17,7 @@ import { } from '../base/tracks'; import { UPDATE_LOCAL_TRACKS_DURATION } from './actionTypes'; -import { createLocalTracksDurationEvent } from './AnalyticsEvents'; +import { createLocalTracksDurationEvent, createNetworkInfoEvent } from './AnalyticsEvents'; import { initAnalytics, resetAnalytics, sendAnalytics } from './functions'; /** @@ -127,6 +128,14 @@ MiddlewareRegistry.register(store => next => action => { }); break; } + case SET_NETWORK_INFO: + sendAnalytics( + createNetworkInfoEvent({ + isOnline: action.isOnline, + details: action.details, + networkType: action.networkType + })); + break; case SET_ROOM: { initAnalytics(store); break; diff --git a/react/features/base/net-info/NetworkInfoService.native.js b/react/features/base/net-info/NetworkInfoService.native.js new file mode 100644 index 0000000000000..b2ffd0ae2fcb1 --- /dev/null +++ b/react/features/base/net-info/NetworkInfoService.native.js @@ -0,0 +1,66 @@ +// @flow +import EventEmitter from 'events'; +import NetInfo from '@react-native-community/netinfo'; +import type { NetInfoState, NetInfoSubscription } from '@react-native-community/netinfo'; + +import { ONLINE_STATE_CHANGED_EVENT } from './events'; + +import type { NetworkInfo } from './types'; + +/** + * The network info service implementation for iOS and Android. 'react-native-netinfo' seems to support windows as well, + * but that has not been tested and is nto used by jitsi-meet. + */ +export default class NetworkInfoService extends EventEmitter { + /** + * Stores the native subscription for future cleanup. + */ + _subscription: NetInfoSubscription; + + /** + * Converts library's structure to {@link NetworkInfo} used by jitsi-meet. + * + * @param {NetInfoState} netInfoState - The new state given by the native library. + * @private + * @returns {NetworkInfo} + */ + static _convertNetInfoState(netInfoState: NetInfoState): NetworkInfo { + return { + isOnline: netInfoState.isInternetReachable, + details: netInfoState.details, + networkType: netInfoState.type + }; + } + + /** + * Checks for support. + * + * @returns {boolean} + */ + static isSupported() { + return Boolean(NetInfo); + } + + /** + * Starts the service. + * + * @returns {void} + */ + start() { + this._subscription = NetInfo.addEventListener(netInfoState => { + this.emit(ONLINE_STATE_CHANGED_EVENT, NetworkInfoService._convertNetInfoState(netInfoState)); + }); + } + + /** + * Stops the service. + * + * @returns {void} + */ + stop() { + if (this._subscription) { + this._subscription(); + this._subscription = undefined; + } + } +} diff --git a/react/features/base/net-info/NetworkInfoService.web.js b/react/features/base/net-info/NetworkInfoService.web.js new file mode 100644 index 0000000000000..50ac08fd3425b --- /dev/null +++ b/react/features/base/net-info/NetworkInfoService.web.js @@ -0,0 +1,58 @@ +import EventEmitter from 'events'; + +import { ONLINE_STATE_CHANGED_EVENT } from './events'; + +/** + * The network info service implementation for web (Chrome, Firefox and Safari). + */ +export default class NetworkInfoService extends EventEmitter { + + /** + * Creates new instance... + */ + constructor() { + super(); + this._onlineStateListener = this._handleOnlineStatusChange.bind(this, /* online */ true); + this._offlineStateListener = this._handleOnlineStatusChange.bind(this, /* offline */ false); + } + + /** + * Callback function to track the online state. + * + * @param {boolean} isOnline - Is the browser online or not. + * @private + * @returns {void} + */ + _handleOnlineStatusChange(isOnline) { + this.emit(ONLINE_STATE_CHANGED_EVENT, { isOnline }); + } + + /** + * Checks for support. + * + * @returns {boolean} + */ + static isSupported() { + return window.addEventListener && typeof navigator.onLine !== 'undefined'; + } + + /** + * Starts the service. + * + * @returns {void} + */ + start() { + window.addEventListener('online', this._onlineStateListener); + window.addEventListener('offline', this._offlineStateListener); + } + + /** + * Stops the service. + * + * @returns {void} + */ + stop() { + window.removeEventListener('online', this._onlineStateListener); + window.removeEventListener('offline', this._offlineStateListener); + } +} diff --git a/react/features/base/net-info/actionTypes.js b/react/features/base/net-info/actionTypes.js new file mode 100644 index 0000000000000..23e48a60b68ab --- /dev/null +++ b/react/features/base/net-info/actionTypes.js @@ -0,0 +1,13 @@ +/** + * The action dispatched when the {@link NetworkInfo} structure is being updated. + * + * @type {string} + */ +export const SET_NETWORK_INFO = 'SET_NETWORK_INFO'; + +/** + * Tha action dispatched by 'base/net-info' middleware in order to store the cleanup function for later use. + * @type {string} + * @private + */ +export const _STORE_NETWORK_INFO_CLEANUP = 'STORE_NETWORK_INFO_CLEANUP'; diff --git a/react/features/base/net-info/actions.js b/react/features/base/net-info/actions.js new file mode 100644 index 0000000000000..841db7bc0cf64 --- /dev/null +++ b/react/features/base/net-info/actions.js @@ -0,0 +1,42 @@ +// @flow + +import { SET_NETWORK_INFO, _STORE_NETWORK_INFO_CLEANUP } from './actionTypes'; + +import type { NetworkInfo } from './types'; + +/** + * Up[dates the network info state. + * + * @param {NetworkInfo} networkInfo - The new network state to be set. + * @returns {{ + * type: SET_NETWORK_INFO, + * isOnline: boolean, + * networkType: string, + * details: Object + * }} + */ +export function setNetworkInfo({ isOnline, networkType, details }: NetworkInfo): Object { + return { + type: SET_NETWORK_INFO, + isOnline, + networkType, + details + }; +} + +/** + * Stored the cleanup function used to shutdown the {@code NetworkInfoService}. + * + * @param {Function} cleanup - The cleanup function to be called on {@code APP_WILL_UNMOUNT}. + * @returns {{ + * type: _STORE_NETWORK_INFO_CLEANUP, + * cleanup: Function + * }} + * @private + */ +export function _storeNetworkInfoCleanup(cleanup: Function): Object { + return { + type: _STORE_NETWORK_INFO_CLEANUP, + cleanup + }; +} diff --git a/react/features/base/net-info/constants.js b/react/features/base/net-info/constants.js new file mode 100644 index 0000000000000..b150787712f0f --- /dev/null +++ b/react/features/base/net-info/constants.js @@ -0,0 +1,6 @@ +/** + * The name for Redux store key used by the 'base/net-info' feature. + * + * @type {string} + */ +export const STORE_NAME = 'features/base/net-info'; diff --git a/react/features/base/net-info/events.js b/react/features/base/net-info/events.js new file mode 100644 index 0000000000000..498e56cc7482e --- /dev/null +++ b/react/features/base/net-info/events.js @@ -0,0 +1 @@ +export const ONLINE_STATE_CHANGED_EVENT = 'network-info-online-status-change'; diff --git a/react/features/base/net-info/index.js b/react/features/base/net-info/index.js new file mode 100644 index 0000000000000..79da85d3d0582 --- /dev/null +++ b/react/features/base/net-info/index.js @@ -0,0 +1,4 @@ +export * from './actionTypes'; + +import './middleware'; +import './reducer'; diff --git a/react/features/base/net-info/logger.js b/react/features/base/net-info/logger.js new file mode 100644 index 0000000000000..98213b45aa5eb --- /dev/null +++ b/react/features/base/net-info/logger.js @@ -0,0 +1,5 @@ +// @flow + +import { getLogger } from '../logging/functions'; + +export default getLogger('features/base/net-info'); diff --git a/react/features/base/net-info/middleware.js b/react/features/base/net-info/middleware.js new file mode 100644 index 0000000000000..5341c4d147891 --- /dev/null +++ b/react/features/base/net-info/middleware.js @@ -0,0 +1,64 @@ +// @flow + +import { APP_WILL_MOUNT, APP_WILL_UNMOUNT } from '../app'; +import { MiddlewareRegistry } from '../redux'; + +import { _storeNetworkInfoCleanup, setNetworkInfo } from './actions'; +import { STORE_NAME } from './constants'; +import { ONLINE_STATE_CHANGED_EVENT } from './events'; +import logger from './logger'; +import NetworkInfoService from './NetworkInfoService'; +import type { NetworkInfo } from './types'; + +/** + * Middleware for 'base/net-info' feature. + * + * @param {Store} store - The redux store. + * @returns {Function} + */ +// eslint-disable-next-line no-unused-vars +MiddlewareRegistry.register(({ dispatch, getState }) => next => action => { + const result = next(action); + + switch (action.type) { + case APP_WILL_MOUNT: + if (NetworkInfoService.isSupported()) { + const networkInfoService = new NetworkInfoService(); + const stop = () => { + networkInfoService.stop(); + networkInfoService.removeAllListeners(); + }; + + networkInfoService.addListener( + ONLINE_STATE_CHANGED_EVENT, + ({ isOnline, networkType, details }: NetworkInfo) => { + logger.info('Network changed', JSON.stringify({ + isOnline, + details, + networkType + })); + dispatch(setNetworkInfo({ + isOnline, + networkType, + details + })); + }); + + dispatch(_storeNetworkInfoCleanup(stop)); + + networkInfoService.start(); + } + break; + case APP_WILL_UNMOUNT: { + const { _cleanup } = getState()[STORE_NAME]; + + if (_cleanup) { + _cleanup(); + dispatch(_storeNetworkInfoCleanup(undefined)); + } + } + break; + } + + return result; +}); diff --git a/react/features/base/net-info/reducer.js b/react/features/base/net-info/reducer.js new file mode 100644 index 0000000000000..ad7c69733265a --- /dev/null +++ b/react/features/base/net-info/reducer.js @@ -0,0 +1,30 @@ +// @flow +import { assign, ReducerRegistry } from '../redux'; + +import { SET_NETWORK_INFO, _STORE_NETWORK_INFO_CLEANUP } from './actionTypes'; +import { STORE_NAME } from './constants'; + +const DEFAULT_STATE = { + isOnline: true +}; + +/** + * The base/net-info feature's reducer. + */ +ReducerRegistry.register(STORE_NAME, (state = DEFAULT_STATE, action) => { + switch (action.type) { + case SET_NETWORK_INFO: + return assign(state, { + isOnline: action.isOnline, + networkType: action.networkType, + cellularGeneration: action.cellularGeneration, + details: action.details + }); + case _STORE_NETWORK_INFO_CLEANUP: + return assign(state, { + _cleanup: action.cleanup + }); + default: + return state; + } +}); diff --git a/react/features/base/net-info/types.js b/react/features/base/net-info/types.js new file mode 100644 index 0000000000000..1a3bad4784435 --- /dev/null +++ b/react/features/base/net-info/types.js @@ -0,0 +1,39 @@ +// @flow + +import { NetInfoCellularGeneration, NetInfoStateType } from '@react-native-community/netinfo'; + +/** + * Describes the structure which is used by jitsi-meet to store information about the current network type and + * conditions. + */ +export type NetworkInfo = { + + /** + * Tells whether or not the internet is reachable. + */ + isOnline: boolean, + + /** + * The network type. Currently reported only on Android/iOS. Can be one of the constants defined by + * the 'react-native-netinfo' library. + */ + networkType: ?NetInfoStateType, + + /** + * Any extra info provided by the OS. Should be JSON and is OS specific. Reported only by iOS and Android and + * the format is whatever comes out of the 'react-native-netinfo' library which is network type dependent. + */ + details: ?{ + + /** + * If {@link networkType} is {@link NetInfoStateType.cellular} then it may provide the info about the type of + * cellular network. + */ + cellularGeneration: ?NetInfoCellularGeneration; + + /** + * Indicates whether or not the connection is expensive. + */ + isConnectionExpensive: ?boolean; + } +}