Rocket.Chat.ReactNative/app/sagas/videoConf.ts

253 lines
8.8 KiB
TypeScript
Raw Normal View History

feat: Add caller and ringer to video conf calls (#5046) * add expo camera and use camera on call init action sheet * fix permissions * set colors when calling * update @react-native-community/hooks lib * move to useWindowDimensions * create action to handle video-conf calls * create videoConf reducer * add typed-redux-saga lib * fix return * change videoConf saga to TS * fix TS target * update action and types * create actionSheetRef * add notifyUser api * export video conf types * add action prop * use new reducer prop * add videoConferenceCancel and add allowRinging to videoConferenceStart * temp-patch * add locales * add handler to videoconf message * fix rest types * add message types * path to remove component from dom * remove notification when is videoconf * create sound hook * create dots loader * update call translation * the end is near * move to confirmed * better code reading * fix call type * fix tests * update podfile * wip * fix call order * move colors * move to jsx * fix colors * add pt-br * remove patch and point * fix colors * fix expo camera * move to style * remove unused styles * update types and style * wip * rename IncomingCallComponent * add custom notification * wip * fix naming * fix styles * fix import * fix styles * change colors * fixa ringing * fix import * organize * fix sizes * use realName * fix spacing * fix icon size * fix header gap * changeColor * fix safeArea * set calling only on direct calls * change ringer to be a component * cancel call on swipe * remove join on direct calls * add props * update package
2023-07-04 00:03:39 +00:00
import { Action } from 'redux';
import { delay, put, takeEvery } from 'redux-saga/effects';
import { call } from 'typed-redux-saga';
import { VIDEO_CONF } from '../actions/actionsTypes';
import { removeVideoConfCall, setCalling, setVideoConfCall, TCallProps } from '../actions/videoConf';
import { hideActionSheetRef } from '../containers/ActionSheet';
import { INAPP_NOTIFICATION_EMITTER } from '../containers/InAppNotification';
import IncomingCallNotification from '../containers/InAppNotification/IncomingCallNotification';
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 EventEmitter from '../lib/methods/helpers/events';
import log from '../lib/methods/helpers/log';
import { hideNotification } from '../lib/methods/helpers/notifications';
import { showToast } from '../lib/methods/helpers/showToast';
import { videoConfJoin } from '../lib/methods/videoConf';
import { Services } from '../lib/services';
import { notifyUser } from '../lib/services/restApi';
import { ICallInfo } from '../reducers/videoConf';
interface IGenericAction extends Action {
type: string;
}
type THandleGeneric = IGenericAction & {
data: any;
};
type TInitCallGeneric = IGenericAction & {
payload: TCallProps;
};
type TCancelCallGeneric = IGenericAction & {
payload?: { callId?: string };
};
type TAcceptCallGeneric = IGenericAction & {
payload: { callId: string };
};
// The interval between attempts to call the remote user
const CALL_INTERVAL = 3000;
// How many attempts to call we're gonna make
const CALL_ATTEMPT_LIMIT = 10;
function* onDirectCall(payload: ICallInfo) {
const calls = yield* appSelector(state => state.videoConf.calls);
const currentCall = calls.find(c => c.callId === payload.callId);
const hasAnotherCall = calls.find(c => c.action === 'call');
if (hasAnotherCall && hasAnotherCall.callId !== payload.callId) return;
const foreground = yield* appSelector(state => state.app.foreground);
if (!currentCall && foreground) {
feat: Add caller and ringer to video conf calls (#5046) * add expo camera and use camera on call init action sheet * fix permissions * set colors when calling * update @react-native-community/hooks lib * move to useWindowDimensions * create action to handle video-conf calls * create videoConf reducer * add typed-redux-saga lib * fix return * change videoConf saga to TS * fix TS target * update action and types * create actionSheetRef * add notifyUser api * export video conf types * add action prop * use new reducer prop * add videoConferenceCancel and add allowRinging to videoConferenceStart * temp-patch * add locales * add handler to videoconf message * fix rest types * add message types * path to remove component from dom * remove notification when is videoconf * create sound hook * create dots loader * update call translation * the end is near * move to confirmed * better code reading * fix call type * fix tests * update podfile * wip * fix call order * move colors * move to jsx * fix colors * add pt-br * remove patch and point * fix colors * fix expo camera * move to style * remove unused styles * update types and style * wip * rename IncomingCallComponent * add custom notification * wip * fix naming * fix styles * fix import * fix styles * change colors * fixa ringing * fix import * organize * fix sizes * use realName * fix spacing * fix icon size * fix header gap * changeColor * fix safeArea * set calling only on direct calls * change ringer to be a component * cancel call on swipe * remove join on direct calls * add props * update package
2023-07-04 00:03:39 +00:00
yield put(setVideoConfCall(payload));
EventEmitter.emit(INAPP_NOTIFICATION_EMITTER, {
// @ts-ignore - Component props do not match Event emitter props
customComponent: IncomingCallNotification,
customTime: 30000,
customNotification: true,
hideOnPress: false,
swipeEnabled: false,
...payload
});
}
}
function* onDirectCallCanceled(payload: ICallInfo) {
const calls = yield* appSelector(state => state.videoConf.calls);
const currentCall = calls.find(c => c.callId === payload.callId);
if (currentCall) {
yield put(removeVideoConfCall(currentCall));
hideNotification();
}
}
function* onDirectCallAccepted({ callId, rid, uid, action }: ICallInfo) {
const calls = yield* appSelector(state => state.videoConf.calls);
const userId = yield* appSelector(state => state.login.user.id);
const currentCall = calls.find(c => c.callId === callId);
if (currentCall && currentCall.action === 'calling') {
yield call(notifyUser, `${uid}/video-conference`, { action: 'confirmed', params: { uid: userId, rid, callId } });
yield put(setVideoConfCall({ callId, rid, uid, action }));
}
}
function* onDirectCallRejected() {
yield call(cancelCall, {});
showToast(i18n.t('Call_rejected'));
yield call(hideActionSheetRef);
}
function* onDirectCallConfirmed(payload: ICallInfo) {
const calls = yield* appSelector(state => state.videoConf.calls);
const currentCall = calls.find(c => c.callId === payload.callId);
if (currentCall) {
yield put(removeVideoConfCall(currentCall));
yield call(hideActionSheetRef);
videoConfJoin(payload.callId, false, true);
}
}
function* onDirectCallJoined(payload: ICallInfo) {
const calls = yield* appSelector(state => state.videoConf.calls);
const currentCall = calls.find(c => c.callId === payload.callId);
if (currentCall && (currentCall.action === 'accepted' || currentCall.action === 'calling')) {
yield put(setCalling(false));
yield put(removeVideoConfCall(currentCall));
yield call(hideActionSheetRef);
videoConfJoin(payload.callId, false, true);
}
}
function* onDirectCallEnded(payload: ICallInfo) {
const calls = yield* appSelector(state => state.videoConf.calls);
const currentCall = calls.find(c => c.callId === payload.callId);
if (currentCall) {
yield put(removeVideoConfCall(currentCall));
hideNotification();
}
}
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, rid } }: { payload: TCallProps }) {
yield put(setCalling(true));
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, rid);
if (videoConfResponse.success) {
if (direct && videoConfResponse.data.type === 'direct') {
yield call(callUser, { rid, uid: videoConfResponse.data.calleeId, callId: videoConfResponse.data.callId });
} else {
videoConfJoin(videoConfResponse.data.callId, cam, mic);
yield call(hideActionSheetRef);
yield put(setCalling(false));
}
}
} catch (e) {
yield put(setCalling(false));
showErrorAlert(i18n.t('error-init-video-conf'));
log(e);
}
} else {
const sub = yield* call(getSubscriptionByRoomId, rid);
if (sub) {
callJitsi({ room: sub, cam });
yield put(setCalling(false));
}
}
}
function* giveUp({ rid, uid, callId, rejected }: { rid: string; uid: string; callId: string; rejected?: boolean }) {
yield put(removeVideoConfCall({ rid, uid, callId }));
yield call(notifyUser, `${uid}/video-conference`, { action: rejected ? 'rejected' : 'canceled', params: { uid, rid, callId } });
if (!rejected) {
yield put(setCalling(false));
yield call(Services.videoConferenceCancel, callId);
}
}
function* cancelCall({ payload }: { payload?: { callId?: string } }) {
const calls = yield* appSelector(state => state.videoConf.calls);
if (payload?.callId) {
const currentCall = calls.find(c => c.callId === payload.callId);
if (currentCall) {
yield call(giveUp, { ...currentCall, rejected: true });
}
} else {
const currentCall = calls.find(c => c.action === 'calling');
if (currentCall && currentCall.callId) {
yield call(giveUp, currentCall);
}
}
}
function* callUser({ rid, uid, callId }: { rid: string; uid: string; callId: string }) {
const userId = yield* appSelector(state => state.login.user.id);
yield put(setVideoConfCall({ rid, uid, callId, action: 'calling' }));
for (let attempt = 1; attempt <= CALL_ATTEMPT_LIMIT; attempt++) {
if (attempt < CALL_ATTEMPT_LIMIT) {
const calls = yield* appSelector(state => state.videoConf.calls);
const currentCall = calls.find(c => c.callId === callId);
if (!currentCall || currentCall.action !== 'calling') {
break;
}
yield call(notifyUser, `${uid}/video-conference`, { action: 'call', params: { uid: userId, rid, callId } });
yield delay(CALL_INTERVAL);
} else {
hideActionSheetRef();
yield call(giveUp, { uid, rid, callId });
break;
}
}
}
function* acceptCall({ payload: { callId } }: { payload: { callId: string } }) {
const calls = yield* appSelector(state => state.videoConf.calls);
const currentCall = calls.find(c => c.callId === callId);
if (currentCall && currentCall.action === 'call') {
const userId = yield* appSelector(state => state.login.user.id);
yield call(notifyUser, `${currentCall.uid}/video-conference`, {
action: 'accepted',
params: { uid: userId, rid: currentCall.rid, callId: currentCall.callId }
});
yield put(setVideoConfCall({ ...currentCall, action: 'accepted' }));
hideNotification();
}
}
export default function* root(): Generator {
yield takeEvery<THandleGeneric>(VIDEO_CONF.HANDLE_INCOMING_WEBSOCKET_MESSAGES, handleVideoConfIncomingWebsocketMessages);
yield takeEvery<TInitCallGeneric>(VIDEO_CONF.INIT_CALL, initCall);
yield takeEvery<TCancelCallGeneric>(VIDEO_CONF.CANCEL_CALL, cancelCall);
yield takeEvery<TAcceptCallGeneric>(VIDEO_CONF.ACCEPT_CALL, acceptCall);
}