Compare commits

...

13 Commits

Author SHA1 Message Date
GleidsonDaniel 14481b368d fix TS target 2023-03-08 17:02:01 -03:00
GleidsonDaniel 1d144cf19c change videoConf saga to TS 2023-03-08 17:01:04 -03:00
GleidsonDaniel 60defec633 fix return 2023-03-08 17:00:27 -03:00
GleidsonDaniel 4c4a0ba9b9 add typed-redux-saga lib 2023-03-08 17:00:05 -03:00
GleidsonDaniel 76c41474a6 create videoConf reducer 2023-03-08 14:36:49 -03:00
GleidsonDaniel c6658d570b create action to handle video-conf calls 2023-03-07 18:25:50 -03:00
GleidsonDaniel 0cb9897bdf move to useWindowDimensions 2023-03-07 16:36:12 -03:00
GleidsonDaniel d47de6193b update @react-native-community/hooks lib 2023-03-07 16:35:56 -03:00
GleidsonDaniel 1b3796ef17 Merge branch 'develop' into start-a-new-call 2023-03-07 09:48:21 -03:00
GleidsonDaniel 9722550c9f set colors when calling 2023-03-03 17:16:35 -03:00
GleidsonDaniel d36d5fd4e3 Merge branch 'develop' into start-a-new-call 2023-03-03 13:45:41 -03:00
GleidsonDaniel 7a1aa58925 fix permissions 2023-03-02 11:24:39 -03:00
GleidsonDaniel 1fb060a982 add expo camera and use camera on call init action sheet 2023-03-02 11:15:47 -03:00
19 changed files with 388 additions and 47 deletions

View File

@ -62,6 +62,11 @@ allprojects {
url "$rootDir/../node_modules/detox/Detox-android"
}
maven {
// expo-camera bundles a custom com.google.android:cameraview
url "$rootDir/../node_modules/expo-camera/android/maven"
}
mavenCentral {
content {
excludeGroup "com.facebook.react"

View File

@ -84,3 +84,10 @@ export const ENCRYPTION = createRequestTypes('ENCRYPTION', ['INIT', 'STOP', 'DEC
export const PERMISSIONS = createRequestTypes('PERMISSIONS', ['SET', 'UPDATE']);
export const ROLES = createRequestTypes('ROLES', ['SET', 'UPDATE', 'REMOVE']);
export const VIDEO_CONF = createRequestTypes('VIDEO_CONF', [
'HANDLE_INCOMING_WEBSOCKET_MESSAGES',
'SET',
'REMOVE',
'CLEAR',
'INIT_CALL'
]);

50
app/actions/videoConf.ts Normal file
View File

@ -0,0 +1,50 @@
import { Action } from 'redux';
import { ICallInfo } from '../reducers/videoConf';
import { VIDEO_CONF } from './actionsTypes';
interface IHandleVideoConfIncomingWebsocketMessages extends Action {
data: any;
}
export type TCallProps = { mic: boolean; cam: boolean; direct: boolean; roomId: string };
export interface IVideoConfGenericAction extends Action {
payload: ICallInfo;
}
export type TActionVideoConf = IHandleVideoConfIncomingWebsocketMessages & IVideoConfGenericAction & Action;
export function handleVideoConfIncomingWebsocketMessages(data: any): IHandleVideoConfIncomingWebsocketMessages {
return {
type: VIDEO_CONF.HANDLE_INCOMING_WEBSOCKET_MESSAGES,
data
};
}
export function setVideoConfCall(payload: ICallInfo): IVideoConfGenericAction {
return {
type: VIDEO_CONF.SET,
payload
};
}
export function removeVideoConfCall(payload: ICallInfo): IVideoConfGenericAction {
return {
type: VIDEO_CONF.REMOVE,
payload
};
}
export function clearVideoConfCalls(): Action {
return {
type: VIDEO_CONF.CLEAR
};
}
export function initVideoCall(payload: TCallProps): Action & { payload: TCallProps } {
return {
type: VIDEO_CONF.INIT_CALL,
payload
};
}

View File

@ -1,3 +1,4 @@
import { Camera, CameraType } from 'expo-camera';
import React, { useEffect, useState } from 'react';
import { Text, View } from 'react-native';
import Touchable from 'react-native-platform-touchable';
@ -7,7 +8,6 @@ import { getSubscriptionByRoomId } from '../../../../lib/database/services/Subsc
import { useAppSelector } from '../../../../lib/hooks';
import { getRoomAvatar, getUidDirectMessage } from '../../../../lib/methods/helpers';
import { useTheme } from '../../../../theme';
import { useActionSheet } from '../../../ActionSheet';
import AvatarContainer from '../../../Avatar';
import Button from '../../../Button';
import { CustomIcon } from '../../../CustomIcon';
@ -15,16 +15,20 @@ import { BUTTON_HIT_SLOP } from '../../../message/utils';
import StatusContainer from '../../../Status';
import useStyle from './styles';
const CAM_SIZE = { height: 220, width: 148 };
// fixed colors, do not change with theme change.
const gray300 = '#5f656e';
const gray100 = '#CBCED1';
export default function StartACallActionSheet({ rid, initCall }: { rid: string; initCall: Function }): React.ReactElement {
const style = useStyle();
const { colors } = useTheme();
const [user, setUser] = useState({ username: '', avatar: '', uid: '' });
const [mic, setMic] = useState(true);
const [cam, setCam] = useState(false);
const [calling, setCalling] = useState(true);
const username = useAppSelector(state => state.login.user.username);
const { hideActionSheet } = useActionSheet();
useEffect(() => {
(async () => {
const room = await getSubscriptionByRoomId(rid);
@ -34,26 +38,37 @@ export default function StartACallActionSheet({ rid, initCall }: { rid: string;
})();
}, [rid]);
const handleColor = (enabled: boolean) => (enabled ? colors.conferenceCallEnabledIcon : colors.conferenceCallDisabledIcon);
const handleColors = (enabled: boolean) => {
if (calling) {
if (enabled) {
return { button: colors.conferenceCallCallBackButton, icon: gray300 };
}
return { button: 'transparent', icon: gray100 };
}
if (enabled) {
return { button: colors.conferenceCallEnabledIconBackground, icon: colors.conferenceCallEnabledIcon };
}
return { button: 'transparent', icon: colors.conferenceCallDisabledIcon };
};
return (
<View style={style.actionSheetContainer}>
<View style={style.actionSheetHeader}>
<Text style={style.actionSheetHeaderTitle}>{i18n.t('Start_a_call')}</Text>
<Text style={style.actionSheetHeaderTitle}>{calling ? i18n.t('Calling') : i18n.t('Start_a_call')}</Text>
<View style={style.actionSheetHeaderButtons}>
<Touchable
onPress={() => setCam(!cam)}
style={[style.iconCallContainer, cam && style.enabledBackground, { marginRight: 6 }]}
style={[style.iconCallContainer, { backgroundColor: handleColors(cam).button }, { marginRight: 6 }]}
hitSlop={BUTTON_HIT_SLOP}
>
<CustomIcon name={cam ? 'camera' : 'camera-disabled'} size={20} color={handleColor(cam)} />
<CustomIcon name={cam ? 'camera' : 'camera-disabled'} size={20} color={handleColors(cam).icon} />
</Touchable>
<Touchable
onPress={() => setMic(!mic)}
style={[style.iconCallContainer, mic && style.enabledBackground]}
style={[style.iconCallContainer, { backgroundColor: handleColors(mic).button }]}
hitSlop={BUTTON_HIT_SLOP}
>
<CustomIcon name={mic ? 'microphone' : 'microphone-disabled'} size={20} color={handleColor(mic)} />
<CustomIcon name={mic ? 'microphone' : 'microphone-disabled'} size={20} color={handleColors(mic).icon} />
</Touchable>
</View>
</View>
@ -64,17 +79,27 @@ export default function StartACallActionSheet({ rid, initCall }: { rid: string;
{user.username}
</Text>
</View>
<View style={style.actionSheetPhotoContainer}>
<AvatarContainer size={62} text={username} />
<View
style={[
style.actionSheetPhotoContainer,
CAM_SIZE,
{ backgroundColor: cam ? undefined : colors.conferenceCallPhotoBackground }
]}
>
{cam ? <Camera style={CAM_SIZE} type={CameraType.front} /> : <AvatarContainer size={62} text={username} />}
</View>
<Button
backgroundColor={calling ? colors.conferenceCallCallBackButton : colors.actionTintColor}
color={calling ? gray300 : colors.conferenceCallEnabledIcon}
onPress={() => {
hideActionSheet();
setTimeout(() => {
if (!calling) {
setCalling(true);
initCall({ cam, mic });
}, 100);
} else {
setCalling(false);
}
}}
title={i18n.t('Call')}
title={calling ? i18n.t('Cancel') : i18n.t('Call')}
/>
</View>
);

View File

@ -116,12 +116,12 @@ export default function useStyle() {
actionSheetPhotoContainer: {
height: 220,
width: 148,
backgroundColor: colors.conferenceCallPhotoBackground,
borderRadius: 8,
margin: 24,
alignSelf: 'center',
justifyContent: 'center',
alignItems: 'center'
alignItems: 'center',
overflow: 'hidden'
}
});
}

View File

@ -1,10 +1,11 @@
import database from '..';
import { TSubscriptionModel } from '../../../definitions';
import { TAppDatabase } from '../interfaces';
import { SUBSCRIPTIONS_TABLE } from '../model/Subscription';
const getCollection = (db: TAppDatabase) => db.get(SUBSCRIPTIONS_TABLE);
export const getSubscriptionByRoomId = async (rid: string) => {
export const getSubscriptionByRoomId = async (rid: string): Promise<TSubscriptionModel | null> => {
const db = database.active;
const subCollection = getCollection(db);
try {

View File

@ -1,5 +1,10 @@
import { TypedUseSelectorHook, useSelector } from 'react-redux';
import { select } from 'redux-saga/effects';
import { IApplicationState } from '../../definitions';
export const useAppSelector: TypedUseSelectorHook<IApplicationState> = useSelector;
export function* appSelector<TSelected>(selector: (state: IApplicationState) => TSelected): Generator<any, TSelected, TSelected> {
return yield select(selector);
}

View File

@ -1,4 +1,4 @@
import { useDimensions } from '@react-native-community/hooks';
import { useWindowDimensions } from 'react-native';
import { useSafeAreaInsets } from 'react-native-safe-area-context';
// Not sure if it's worth adding this here in the context of the actionSheet
@ -8,7 +8,7 @@ import { useSafeAreaInsets } from 'react-native-safe-area-context';
*/
export const useSnaps = (snaps: number[]): string[] => {
const insets = useSafeAreaInsets();
const { screen } = useDimensions();
const { height, scale } = useWindowDimensions();
const percentage = insets.bottom + insets.top > 75 ? 110 : 100;
return snaps.map(snap => `${((percentage * snap) / (screen.height * screen.scale)).toFixed(2)}%`);
return snaps.map(snap => `${((percentage * snap) / (height * scale)).toFixed(2)}%`);
};

View File

@ -1,5 +1,6 @@
import React, { useEffect, useState } from 'react';
import { Q } from '@nozbe/watermelondb';
import { Camera } from 'expo-camera';
import React, { useEffect, useState } from 'react';
import { useActionSheet } from '../../containers/ActionSheet';
import StartACallActionSheet from '../../containers/UIKit/VideoConferenceBlock/components/StartACallActionSheet';
@ -10,7 +11,7 @@ import database from '../database';
import { getSubscriptionByRoomId } from '../database/services/Subscription';
import { callJitsi } from '../methods';
import { compareServerVersion, showErrorAlert } from '../methods/helpers';
import { videoConfStartAndJoin } from '../methods/videoConf';
import { handleAndroidBltPermission, videoConfStartAndJoin } from '../methods/videoConf';
import { Services } from '../services';
import { useAppSelector } from './useAppSelector';
import { useSnaps } from './useSnaps';
@ -35,6 +36,8 @@ export const useVideoConf = (rid: string): { showInitCallActionSheet: () => Prom
const jitsiEnableChannels = useAppSelector(state => state.settings.Jitsi_Enable_Channels);
const user = useAppSelector(state => getUserSelector(state));
const [permission, requestPermission] = Camera.useCameraPermissions();
const isServer5OrNewer = compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '5.0.0');
const { showActionSheet } = useActionSheet();
@ -87,6 +90,10 @@ export const useVideoConf = (rid: string): { showInitCallActionSheet: () => Prom
children: <StartACallActionSheet rid={rid} initCall={initCall} />,
snaps
});
if (!permission?.granted) {
requestPermission();
handleAndroidBltPermission();
}
}
};

View File

@ -34,6 +34,7 @@ import { E2E_MESSAGE_TYPE } from '../../constants';
import { getRoom } from '../getRoom';
import { merge } from '../helpers/mergeSubscriptionsRooms';
import { getRoomAvatar, getRoomTitle, getSenderName, random } from '../helpers';
import { handleVideoConfIncomingWebsocketMessages } from '../../../actions/videoConf';
const removeListener = (listener: { stop: () => void }) => listener.stop();
@ -402,6 +403,11 @@ export default function subscribeRooms() {
log(e);
}
}
if (/video-conference/.test(ev)) {
const [action, params] = ddpMessage.fields.args;
store.dispatch(handleVideoConfIncomingWebsocketMessages({ action, params }));
}
});
const stop = () => {

View File

@ -19,18 +19,17 @@ const handleBltPermission = async (): Promise<Permission[]> => {
return [PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION];
};
export const handleAndroidBltPermission = async (): Promise<void> => {
if (isAndroid) {
const bltPermission = await handleBltPermission();
await PermissionsAndroid.requestMultiple(bltPermission);
}
};
export const videoConfJoin = async (callId: string, cam?: boolean, mic?: boolean): Promise<void> => {
try {
const result = await Services.videoConferenceJoin(callId, cam, mic);
if (result.success) {
if (isAndroid) {
const bltPermission = await handleBltPermission();
await PermissionsAndroid.requestMultiple([
PermissionsAndroid.PERMISSIONS.CAMERA,
PermissionsAndroid.PERMISSIONS.RECORD_AUDIO,
...bltPermission
]);
}
const { url, providerName } = result;
if (providerName === 'jitsi') {
navigation.navigate('JitsiMeetView', { url, onlyAudio: !cam, videoConf: true });

View File

@ -21,6 +21,7 @@ import enterpriseModules from './enterpriseModules';
import encryption from './encryption';
import permissions from './permissions';
import roles from './roles';
import videoConf from './videoConf';
export default combineReducers({
settings,
@ -43,5 +44,6 @@ export default combineReducers({
enterpriseModules,
encryption,
permissions,
roles
roles,
videoConf
});

View File

@ -0,0 +1,48 @@
import { clearVideoConfCalls, removeVideoConfCall, setVideoConfCall } from '../actions/videoConf';
import { mockedStore } from './mockedStore';
import { initialState, ICallInfo } from './videoConf';
describe('test videoConf reducer', () => {
it('should return initial state', () => {
const state = mockedStore.getState().settings;
expect(state).toEqual(initialState);
});
const call1: ICallInfo = {
callId: '123',
rid: '123',
type: 'accepted',
uid: '123'
};
const call2: ICallInfo = {
callId: '321',
rid: '321',
type: 'accepted',
uid: '321'
};
it('should return call1 after call addSettings action with call1 as parameter', () => {
mockedStore.dispatch(setVideoConfCall(call1));
const state = mockedStore.getState().videoConf;
expect(state[call1.callId]).toEqual(call1);
});
it('should return call2 after call addSettings action with call2 as parameter', () => {
mockedStore.dispatch(setVideoConfCall(call2));
const state = mockedStore.getState().videoConf;
expect(state[call2.callId]).toEqual(call2);
});
it('should remove call1 after call removeVideoConfCall action with call1 as parameter', () => {
mockedStore.dispatch(removeVideoConfCall(call1));
const state = mockedStore.getState().videoConf;
expect(state[call1.callId]).toEqual(undefined);
});
it('should return initial state after clearSettings', () => {
mockedStore.dispatch(clearVideoConfCalls());
const state = mockedStore.getState().videoConf;
expect(state).toEqual({});
});
});

33
app/reducers/videoConf.ts Normal file
View File

@ -0,0 +1,33 @@
import { VIDEO_CONF } from '../actions/actionsTypes';
import { TActionVideoConf } from '../actions/videoConf';
export type TSupportedCallStatus = 'call' | 'canceled' | 'accepted' | 'rejected' | 'confirmed' | 'join' | 'end';
export interface ICallInfo {
callId: string;
rid: string;
uid: string;
type: TSupportedCallStatus;
}
interface ICallInfoRecord {
[key: string]: ICallInfo;
}
export const initialState: ICallInfoRecord = {};
export default (state = initialState, action: TActionVideoConf): ICallInfoRecord => {
switch (action.type) {
case VIDEO_CONF.SET:
return {
...state,
[action.payload.callId]: action.payload
};
case VIDEO_CONF.REMOVE:
return Object.fromEntries(Object.entries(state).filter(([key]) => key !== action.payload.callId));
case VIDEO_CONF.CLEAR:
return initialState;
default:
return state;
}
};

View File

@ -13,6 +13,7 @@ import deepLinking from './deepLinking';
import inviteLinks from './inviteLinks';
import createDiscussion from './createDiscussion';
import encryption from './encryption';
import videoConf from './videoConf';
const root = function* root() {
yield all([
@ -28,7 +29,8 @@ const root = function* root() {
inviteLinks(),
createDiscussion(),
inquiry(),
encryption()
encryption(),
videoConf()
]);
};

120
app/sagas/videoConf.ts Normal file
View File

@ -0,0 +1,120 @@
import { Action } from 'redux';
import { call, takeLatest } from 'typed-redux-saga';
import { VIDEO_CONF } from '../actions/actionsTypes';
import { IVideoConfGenericAction, TCallProps } from '../actions/videoConf';
import i18n from '../i18n';
import { getSubscriptionByRoomId } from '../lib/database/services/Subscription';
import { appSelector } from '../lib/hooks';
import { callJitsi } from '../lib/methods';
import { compareServerVersion, showErrorAlert } from '../lib/methods/helpers';
import log from '../lib/methods/helpers/log';
import { videoConfJoin } from '../lib/methods/videoConf';
import { Services } from '../lib/services';
import { ICallInfo } from '../reducers/videoConf';
type TGenerator = Generator<IVideoConfGenericAction>;
function* onDirectCall(payload: ICallInfo): TGenerator {
return null;
}
function* onDirectCallCanceled(payload: ICallInfo): TGenerator {
return null;
}
function* onDirectCallAccepted(payload: ICallInfo): TGenerator {
return null;
}
function* onDirectCallRejected(payload: ICallInfo): TGenerator {
return null;
}
function* onDirectCallConfirmed(payload: ICallInfo): TGenerator {
return null;
}
function* onDirectCallJoined(payload: ICallInfo): TGenerator {
return null;
}
function* onDirectCallEnded(payload: ICallInfo): TGenerator {
return null;
}
function* handleVideoConfIncomingWebsocketMessages({ data }: { data: any }) {
const { action, params } = data.action;
if (!action || typeof action !== 'string') {
return;
}
if (!params || typeof params !== 'object' || !params.callId || !params.uid || !params.rid) {
return;
}
const prop = { ...params, action };
switch (action) {
case 'call':
yield call(onDirectCall, prop);
break;
case 'canceled':
yield call(onDirectCallCanceled, prop);
break;
case 'accepted':
yield call(onDirectCallAccepted, prop);
break;
case 'rejected':
yield call(onDirectCallRejected, prop);
break;
case 'confirmed':
yield call(onDirectCallConfirmed, prop);
break;
case 'join':
yield call(onDirectCallJoined, prop);
break;
case 'end':
yield call(onDirectCallEnded, prop);
break;
}
}
function* initCall({ payload: { mic, cam, direct, roomId } }: { payload: TCallProps }) {
const serverVersion = yield* appSelector(state => state.server.version);
const isServer5OrNewer = compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '5.0.0');
if (isServer5OrNewer) {
try {
const videoConfResponse = yield* call(Services.videoConferenceStart, roomId);
if (videoConfResponse.success) {
if (direct) {
// callUser({ uid: data.calleeId, rid: roomId, callId: data.callId });
} else {
videoConfJoin(videoConfResponse.data.callId, cam, mic);
}
// setCalling(false);
}
} catch (e) {
// setCalling(false);
showErrorAlert(i18n.t('error-init-video-conf'));
log(e);
}
} else {
const sub = yield* call(getSubscriptionByRoomId, roomId);
if (sub) {
callJitsi({ room: sub, cam });
// setCalling(false);
}
}
}
interface IGenericAction extends Action {
type: string;
}
export default function* root(): Generator {
yield takeLatest<
IGenericAction & {
data: any;
}
>(VIDEO_CONF.HANDLE_INCOMING_WEBSOCKET_MESSAGES, handleVideoConfIncomingWebsocketMessages);
yield takeLatest<IGenericAction & { payload: TCallProps }>(VIDEO_CONF.INIT_CALL, initCall);
}

View File

@ -47,7 +47,7 @@
"@react-native-community/blur": "^4.1.0",
"@react-native-community/cameraroll": "4.1.2",
"@react-native-community/datetimepicker": "3.5.2",
"@react-native-community/hooks": "2.6.0",
"@react-native-community/hooks": "3.0.0",
"@react-native-community/netinfo": "6.0.0",
"@react-native-community/picker": "^1.8.1",
"@react-native-community/slider": "4.2.2",
@ -73,6 +73,7 @@
"expo": "^46.0.9",
"expo-apple-authentication": "4.2.1",
"expo-av": "11.2.3",
"expo-camera": "12.5.0",
"expo-file-system": "14.0.0",
"expo-haptics": "11.2.0",
"expo-keep-awake": "10.1.1",
@ -145,6 +146,7 @@
"rn-fetch-blob": "^0.12.0",
"rn-root-view": "RocketChat/rn-root-view",
"semver": "^7.3.8",
"typed-redux-saga": "^1.5.0",
"ua-parser-js": "^1.0.32",
"uri-js": "^4.4.1",
"url-parse": "1.5.10",

View File

@ -4,7 +4,7 @@
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "esnext" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */,
"target": "ESNEXT" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', 'ES2021', or 'ESNEXT'. */,
"module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */,
// "lib": [], /* Specify library files to be included in the compilation. */
"allowJs": true /* Allow javascript files to be compiled. */,

View File

@ -801,6 +801,13 @@
dependencies:
"@babel/types" "^7.14.5"
"@babel/helper-module-imports@^7.14.5", "@babel/helper-module-imports@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e"
integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==
dependencies:
"@babel/types" "^7.18.6"
"@babel/helper-module-imports@^7.16.7":
version "7.16.7"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.16.7.tgz#25612a8091a999704461c8a222d0efec5d091437"
@ -808,13 +815,6 @@
dependencies:
"@babel/types" "^7.16.7"
"@babel/helper-module-imports@^7.18.6":
version "7.18.6"
resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz#1e3ebdbbd08aad1437b428c50204db13c5a3ca6e"
integrity sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==
dependencies:
"@babel/types" "^7.18.6"
"@babel/helper-module-transforms@^7.10.4":
version "7.10.4"
resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.10.4.tgz#ca1f01fdb84e48c24d7506bb818c961f1da8805d"
@ -4831,6 +4831,13 @@
"@jridgewell/resolve-uri" "^3.0.3"
"@jridgewell/sourcemap-codec" "^1.4.10"
"@koale/useworker@^4.0.2":
version "4.0.2"
resolved "https://registry.yarnpkg.com/@koale/useworker/-/useworker-4.0.2.tgz#cb540a2581cd6025307c3ca6685bc60748773e58"
integrity sha512-xPIPADtom8/3/4FLNj7MvNcBM/Z2FleH85Fdx2O869eoKW8+PoEgtSVvoxWjCWMA46Sm9A5/R1TyzNGc+yM0wg==
dependencies:
dequal "^1.0.0"
"@mdx-js/mdx@^1.6.22":
version "1.6.22"
resolved "https://registry.yarnpkg.com/@mdx-js/mdx/-/mdx-1.6.22.tgz#8a723157bf90e78f17dc0f27995398e6c731f1ba"
@ -5176,10 +5183,10 @@
dependencies:
invariant "^2.2.4"
"@react-native-community/hooks@2.6.0":
version "2.6.0"
resolved "https://registry.yarnpkg.com/@react-native-community/hooks/-/hooks-2.6.0.tgz#dd5f19601eb3684c6bcdd3df3d0c04cf44c24cff"
integrity sha512-emBGKvhJ0h++lLJQ5ejsj+od9G67nEaihjvfSx7/JWvNrQGAhP9U0OZqgb9dkKzor9Ufaj9SGt8RNY97cGzttw==
"@react-native-community/hooks@3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@react-native-community/hooks/-/hooks-3.0.0.tgz#af5f2ca32eea59b792ce9e3d9a4cf0354f9b195f"
integrity sha512-g2OyxXHfwIytXUJitBR6Z/ISoOfp0WKx5FOv+NqJ/CrWjRDcTw6zXE5I1C9axfuh30kJqzWchVfCDrkzZYTxqg==
"@react-native-community/netinfo@6.0.0":
version "6.0.0"
@ -7698,7 +7705,7 @@ babel-plugin-macros@^2.0.0, babel-plugin-macros@^2.8.0:
cosmiconfig "^6.0.0"
resolve "^1.12.0"
babel-plugin-macros@^3.0.1:
babel-plugin-macros@^3.0.1, babel-plugin-macros@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz#9ef6dc74deb934b4db344dc973ee851d148c50c1"
integrity sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==
@ -9762,6 +9769,11 @@ deprecated-react-native-prop-types@^2.3.0:
invariant "*"
prop-types "*"
dequal@^1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/dequal/-/dequal-1.0.1.tgz#dbbf9795ec626e9da8bd68782f4add1d23700d8b"
integrity sha512-Fx8jxibzkJX2aJgyfSdLhr9tlRoTnHKrRJuu2XHlAgKioN2j19/Bcbe0d4mFXYZ3+wpE2KVobUVTfDutcD17xQ==
dequal@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.2.tgz#85ca22025e3a87e65ef75a7a437b35284a7e319d"
@ -10878,6 +10890,15 @@ expo-av@11.2.3:
dependencies:
"@expo/config-plugins" "^4.0.14"
expo-camera@12.5.0:
version "12.5.0"
resolved "https://registry.yarnpkg.com/expo-camera/-/expo-camera-12.5.0.tgz#aac8441ea661ed21becf064e715dd50d5448aaae"
integrity sha512-oiQccJ9d2FnGdlLKaRphZ2DpwI9C1yD4HUZDacsAR+8904xv3Mhf6u78mh9yBPs6Z7dbtgSjOFtFu5s29PeTxA==
dependencies:
"@expo/config-plugins" "~5.0.0"
"@koale/useworker" "^4.0.2"
invariant "^2.2.4"
expo-constants@~13.2.2, expo-constants@~13.2.4:
version "13.2.4"
resolved "https://registry.yarnpkg.com/expo-constants/-/expo-constants-13.2.4.tgz#eab4a553f074b2c60ad7a158d3b82e3484a94606"
@ -20114,6 +20135,14 @@ type-is@~1.6.17, type-is@~1.6.18:
media-typer "0.3.0"
mime-types "~2.1.24"
typed-redux-saga@^1.5.0:
version "1.5.0"
resolved "https://registry.yarnpkg.com/typed-redux-saga/-/typed-redux-saga-1.5.0.tgz#f70b47c92c6e29e0184d0c30d563c18d6ad0ae54"
integrity sha512-XHKliNtRNUegYAAztbVDb5Q+FMqYNQPaed6Xq2N8kz8AOmiOCVxW3uIj7TEptR1/ms6M9u3HEDfJr4qqz/PYrw==
optionalDependencies:
"@babel/helper-module-imports" "^7.14.5"
babel-plugin-macros "^3.1.0"
typedarray-to-buffer@^3.1.5:
version "3.1.5"
resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080"