[NEW] Basic support for Video Conf (#4307)

* create livechat video conf feature

* add handler to call url

* remove webview and change to openUrl

* Checking settings

* stash

* add action sheet on click init and ad more handlers

* fix logic and call to create a video conf

* change JitsiMeetView from InsideStack to ChatStack to remove modal animation

* fix error logic

* fix stack

* fix comma

Co-authored-by: Diego Mello <diegolmello@gmail.com>
This commit is contained in:
Gleidson Daniel Silva 2022-06-27 15:04:20 -03:00 committed by GitHub
parent fc0d7e2ed3
commit 58a15b23b5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 149 additions and 16 deletions

View File

@ -3,6 +3,9 @@ import React, { useContext, useState } from 'react';
import { BlockContext } from '@rocket.chat/ui-kit'; import { BlockContext } from '@rocket.chat/ui-kit';
import { IText } from './interfaces'; import { IText } from './interfaces';
import { videoConfJoin } from '../../lib/methods/videoConf';
import { TActionSheetOptionsItem, useActionSheet } from '../ActionSheet';
import i18n from '../../i18n';
export const textParser = ([{ text }]: IText[]) => text; export const textParser = ([{ text }]: IText[]) => text;
@ -32,12 +35,14 @@ interface IUseBlockContext {
actionId: string; actionId: string;
appId?: string; appId?: string;
initialValue?: string; initialValue?: string;
url?: string;
} }
export const useBlockContext = ({ blockId, actionId, appId, initialValue }: IUseBlockContext, context: BlockContext): TReturn => { export const useBlockContext = ({ blockId, actionId, appId, initialValue }: IUseBlockContext, context: BlockContext): TReturn => {
const { action, appId: appIdFromContext, viewId, state, language, errors, values = {} } = useContext(KitContext); const { action, appId: appIdFromContext, viewId, state, language, errors, values = {} } = useContext(KitContext);
const { value = initialValue } = values[actionId] || {}; const { value = initialValue } = values[actionId] || {};
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const { showActionSheet } = useActionSheet();
const error = errors && actionId && errors[actionId]; const error = errors && actionId && errors[actionId];
@ -53,6 +58,23 @@ export const useBlockContext = ({ blockId, actionId, appId, initialValue }: IUse
async ({ value }: any) => { async ({ value }: any) => {
setLoading(true); setLoading(true);
try { try {
if (appId === 'videoconf-core' && blockId) {
setLoading(false);
const options: TActionSheetOptionsItem[] = [
{
title: i18n.t('Video_call'),
icon: 'camera',
onPress: () => videoConfJoin(blockId, true)
},
{
title: i18n.t('Voice_call'),
icon: 'microphone',
onPress: () => videoConfJoin(blockId, false)
}
];
showActionSheet({ options });
return;
}
await action({ await action({
blockId, blockId,
appId: appId || appIdFromContext, appId: appId || appIdFromContext,

View File

@ -2,4 +2,10 @@ export type VideoConferenceEndpoints = {
'video-conference/jitsi.update-timeout': { 'video-conference/jitsi.update-timeout': {
POST: (params: { roomId: string }) => void; POST: (params: { roomId: string }) => void;
}; };
'video-conference.join': {
POST: (params: { callId: string; state: { cam: boolean } }) => { url: string; providerName: string };
};
'video-conference.start': {
POST: (params: { roomId: string }) => { url: string };
};
}; };

View File

@ -839,5 +839,6 @@
"Mark_as_unread_Info": "Display room as unread when there are unread messages", "Mark_as_unread_Info": "Display room as unread when there are unread messages",
"Show_badge_for_mentions": "Show badge for mentions", "Show_badge_for_mentions": "Show badge for mentions",
"Show_badge_for_mentions_Info": "Display badge for direct mentions only", "Show_badge_for_mentions_Info": "Display badge for direct mentions only",
"error-init-video-conf": "Error starting video call",
"totp-invalid": "Code or password invalid" "totp-invalid": "Code or password invalid"
} }

View File

@ -759,6 +759,7 @@
"Unsupported_format": "Formato não suportado", "Unsupported_format": "Formato não suportado",
"Downloaded_file": "Arquivo baixado", "Downloaded_file": "Arquivo baixado",
"Error_Download_file": "Erro ao baixar o arquivo", "Error_Download_file": "Erro ao baixar o arquivo",
"error-init-video-conf": "Erro ao iniciar chamada de video"
"added__roomName__to_team": "#{{roomName}} adicionada a esta equipe", "added__roomName__to_team": "#{{roomName}} adicionada a esta equipe",
"Added__username__to_team": "@{{user_added}} adicionado a esta equipe", "Added__username__to_team": "@{{user_added}} adicionado a esta equipe",
"Converted__roomName__to_team": "#{{roomName}} convertida em equipe", "Converted__roomName__to_team": "#{{roomName}} convertida em equipe",

View File

@ -212,6 +212,18 @@ export const defaultSettings = {
Accounts_AvatarExternalProviderUrl: { Accounts_AvatarExternalProviderUrl: {
type: 'valueAsString' type: 'valueAsString'
}, },
VideoConf_Enable_DMs: {
type: 'valueAsBoolean'
},
VideoConf_Enable_Channels: {
type: 'valueAsBoolean'
},
VideoConf_Enable_Groups: {
type: 'valueAsBoolean'
},
VideoConf_Enable_Teams: {
type: 'valueAsBoolean'
},
Accounts_AllowDeleteOwnAccount: { Accounts_AllowDeleteOwnAccount: {
type: 'valueAsBoolean' type: 'valueAsBoolean'
} }

View File

@ -351,6 +351,10 @@ export default {
TC_TOGGLE_AUTOJOIN: 'tc_toggle_autojoin', TC_TOGGLE_AUTOJOIN: 'tc_toggle_autojoin',
TC_TOGGLE_AUTOJOIN_F: 'tc_toggle_autojoin_f', TC_TOGGLE_AUTOJOIN_F: 'tc_toggle_autojoin_f',
// LIVECHAT VIDEOCONF
LIVECHAT_VIDEOCONF_JOIN: 'livechat_videoconf_join',
LIVECHAT_VIDEOCONF_TERMINATE: 'livechat_videoconf_terminate',
// DELETE OWN ACCOUNT ACCOUNT // DELETE OWN ACCOUNT ACCOUNT
DELETE_OWN_ACCOUNT: 'delete_own_account', DELETE_OWN_ACCOUNT: 'delete_own_account',
DELETE_OWN_ACCOUNT_F: 'delete_own_account_f' DELETE_OWN_ACCOUNT_F: 'delete_own_account_f'

View File

@ -0,0 +1,35 @@
import navigation from '../navigation/appNavigation';
import openLink from './helpers/openLink';
import { Services } from '../services';
import log from './helpers/log';
import { showErrorAlert } from './helpers';
import i18n from '../../i18n';
export const videoConfJoin = async (callId: string, cam: boolean) => {
try {
const result = await Services.videoConferenceJoin(callId, cam);
if (result.success) {
const { url, providerName } = result;
if (providerName === 'jitsi') {
navigation.navigate('JitsiMeetView', { url, onlyAudio: !cam, videoConf: true });
} else {
openLink(url);
}
}
} catch (e) {
showErrorAlert(i18n.t('error-init-video-conf'));
log(e);
}
};
export const videoConfStartAndJoin = async (rid: string, cam: boolean) => {
try {
const videoConfResponse: any = await Services.videoConferenceStart(rid);
if (videoConfResponse.success) {
videoConfJoin(videoConfResponse.data.callId, cam);
}
} catch (e) {
showErrorAlert(i18n.t('error-init-video-conf'));
log(e);
}
};

View File

@ -918,6 +918,12 @@ export function getUserInfo(userId: string) {
export const toggleFavorite = (roomId: string, favorite: boolean) => sdk.post('rooms.favorite', { roomId, favorite }); export const toggleFavorite = (roomId: string, favorite: boolean) => sdk.post('rooms.favorite', { roomId, favorite });
export const videoConferenceJoin = (callId: string, cam: boolean) =>
sdk.post('video-conference.join', { callId, state: { cam } });
export const videoConferenceStart = (roomId: string) => sdk.post('video-conference.start', { roomId });
export const saveUserProfileMethod = ( export const saveUserProfileMethod = (
params: IProfileParams, params: IProfileParams,
customFields = {}, customFields = {},
@ -930,3 +936,4 @@ export const saveUserProfileMethod = (
export const deleteOwnAccount = (password: string, confirmRelinquish = false): any => export const deleteOwnAccount = (password: string, confirmRelinquish = false): any =>
// RC 0.67.0 // RC 0.67.0
sdk.post('users.deleteOwnAccount', { password, confirmRelinquish }); sdk.post('users.deleteOwnAccount', { password, confirmRelinquish });

View File

@ -136,6 +136,7 @@ const ChatsStackNavigator = () => {
<ChatsStack.Screen name='QueueListView' component={QueueListView} /> <ChatsStack.Screen name='QueueListView' component={QueueListView} />
<ChatsStack.Screen name='CannedResponsesListView' component={CannedResponsesListView} /> <ChatsStack.Screen name='CannedResponsesListView' component={CannedResponsesListView} />
<ChatsStack.Screen name='CannedResponseDetail' component={CannedResponseDetail} /> <ChatsStack.Screen name='CannedResponseDetail' component={CannedResponseDetail} />
<ChatsStack.Screen name='JitsiMeetView' component={JitsiMeetView} options={{ headerShown: false }} />
</ChatsStack.Navigator> </ChatsStack.Navigator>
); );
}; };
@ -325,7 +326,6 @@ const InsideStackNavigator = () => {
<InsideStack.Screen name='StatusView' component={StatusView} /> <InsideStack.Screen name='StatusView' component={StatusView} />
<InsideStack.Screen name='ShareView' component={ShareView} /> <InsideStack.Screen name='ShareView' component={ShareView} />
<InsideStack.Screen name='ModalBlockView' component={ModalBlockView} options={ModalBlockView.navigationOptions} /> <InsideStack.Screen name='ModalBlockView' component={ModalBlockView} options={ModalBlockView.navigationOptions} />
<InsideStack.Screen name='JitsiMeetView' component={JitsiMeetView} options={{ headerShown: false }} />
</InsideStack.Navigator> </InsideStack.Navigator>
); );
}; };

View File

@ -163,6 +163,12 @@ export type ChatsStackParamList = {
cannedResponse: ICannedResponse; cannedResponse: ICannedResponse;
room: ISubscription; room: ISubscription;
}; };
JitsiMeetView: {
rid: string;
url: string;
onlyAudio?: boolean;
videoConf?: boolean;
};
}; };
export type ProfileStackParamList = { export type ProfileStackParamList = {
@ -259,11 +265,6 @@ export type InsideStackParamList = {
ModalBlockView: { ModalBlockView: {
data: any; // TODO: Change; data: any; // TODO: Change;
}; };
JitsiMeetView: {
rid: string;
url: string;
onlyAudio?: boolean;
};
}; };
export type OutsideParamList = { export type OutsideParamList = {

View File

@ -9,7 +9,7 @@ import ActivityIndicator from '../containers/ActivityIndicator';
import { events, logEvent } from '../lib/methods/helpers/log'; import { events, logEvent } from '../lib/methods/helpers/log';
import { isAndroid, isIOS } from '../lib/methods/helpers'; import { isAndroid, isIOS } from '../lib/methods/helpers';
import { withTheme } from '../theme'; import { withTheme } from '../theme';
import { InsideStackParamList } from '../stacks/types'; import { ChatsStackParamList } from '../stacks/types';
import { IApplicationState, IUser, IBaseScreen } from '../definitions'; import { IApplicationState, IUser, IBaseScreen } from '../definitions';
import { Services } from '../lib/services'; import { Services } from '../lib/services';
@ -24,7 +24,7 @@ interface IJitsiMeetViewState {
loading: boolean; loading: boolean;
} }
interface IJitsiMeetViewProps extends IBaseScreen<InsideStackParamList, 'JitsiMeetView'> { interface IJitsiMeetViewProps extends IBaseScreen<ChatsStackParamList, 'JitsiMeetView'> {
baseUrl: string; baseUrl: string;
user: IUser; user: IUser;
} }
@ -32,12 +32,14 @@ interface IJitsiMeetViewProps extends IBaseScreen<InsideStackParamList, 'JitsiMe
class JitsiMeetView extends React.Component<IJitsiMeetViewProps, IJitsiMeetViewState> { class JitsiMeetView extends React.Component<IJitsiMeetViewProps, IJitsiMeetViewState> {
private rid: string; private rid: string;
private url: string; private url: string;
private videoConf: boolean;
private jitsiTimeout: number | null; private jitsiTimeout: number | null;
constructor(props: IJitsiMeetViewProps) { constructor(props: IJitsiMeetViewProps) {
super(props); super(props);
this.rid = props.route.params?.rid; this.rid = props.route.params?.rid;
this.url = props.route.params?.url; this.url = props.route.params?.url;
this.videoConf = !!props.route.params?.videoConf;
this.jitsiTimeout = null; this.jitsiTimeout = null;
const { user, baseUrl } = props; const { user, baseUrl } = props;
@ -71,7 +73,7 @@ class JitsiMeetView extends React.Component<IJitsiMeetViewProps, IJitsiMeetViewS
componentWillUnmount() { componentWillUnmount() {
logEvent(events.JM_CONFERENCE_TERMINATE); logEvent(events.JM_CONFERENCE_TERMINATE);
if (this.jitsiTimeout) { if (this.jitsiTimeout && !this.videoConf) {
BackgroundTimer.clearInterval(this.jitsiTimeout); BackgroundTimer.clearInterval(this.jitsiTimeout);
this.jitsiTimeout = null; this.jitsiTimeout = null;
BackgroundTimer.stopBackgroundTimer(); BackgroundTimer.stopBackgroundTimer();
@ -86,8 +88,8 @@ class JitsiMeetView extends React.Component<IJitsiMeetViewProps, IJitsiMeetViewS
// Jitsi Update Timeout needs to be called every 10 seconds to make sure // Jitsi Update Timeout needs to be called every 10 seconds to make sure
// call is not ended and is available to web users. // call is not ended and is available to web users.
onConferenceJoined = () => { onConferenceJoined = () => {
logEvent(events.JM_CONFERENCE_JOIN); logEvent(this.videoConf ? events.LIVECHAT_VIDEOCONF_JOIN : events.JM_CONFERENCE_JOIN);
if (this.rid) { if (this.rid && !this.videoConf) {
Services.updateJitsiTimeout(this.rid).catch((e: unknown) => console.log(e)); Services.updateJitsiTimeout(this.rid).catch((e: unknown) => console.log(e));
if (this.jitsiTimeout) { if (this.jitsiTimeout) {
BackgroundTimer.clearInterval(this.jitsiTimeout); BackgroundTimer.clearInterval(this.jitsiTimeout);
@ -101,7 +103,7 @@ class JitsiMeetView extends React.Component<IJitsiMeetViewProps, IJitsiMeetViewS
}; };
onConferenceTerminated = () => { onConferenceTerminated = () => {
logEvent(events.JM_CONFERENCE_TERMINATE); logEvent(this.videoConf ? events.LIVECHAT_VIDEOCONF_TERMINATE : events.JM_CONFERENCE_TERMINATE);
const { navigation } = this.props; const { navigation } = this.props;
navigation.pop(); navigation.pop();
}; };

View File

@ -44,6 +44,7 @@ import {
} from '../../lib/methods/helpers'; } from '../../lib/methods/helpers';
import { Services } from '../../lib/services'; import { Services } from '../../lib/services';
import { getSubscriptionByRoomId } from '../../lib/database/services/Subscription'; import { getSubscriptionByRoomId } from '../../lib/database/services/Subscription';
import { videoConfStartAndJoin } from '../../lib/methods/videoConf';
interface IOnPressTouch { interface IOnPressTouch {
<T extends keyof ChatsStackParamList>(item: { route?: T; params?: ChatsStackParamList[T]; event?: Function }): void; <T extends keyof ChatsStackParamList>(item: { route?: T; params?: ChatsStackParamList[T]; event?: Function }): void;
@ -68,6 +69,10 @@ interface IRoomActionsViewProps extends IBaseScreen<ChatsStackParamList, 'RoomAc
addTeamChannelPermission?: string[]; addTeamChannelPermission?: string[];
convertTeamPermission?: string[]; convertTeamPermission?: string[];
viewCannedResponsesPermission?: string[]; viewCannedResponsesPermission?: string[];
videoConf_Enable_DMs: boolean;
videoConf_Enable_Channels: boolean;
videoConf_Enable_Groups: boolean;
videoConf_Enable_Teams: boolean;
} }
interface IRoomActionsViewState { interface IRoomActionsViewState {
@ -734,6 +739,16 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
} }
}; };
startVideoConf = ({ video }: { video: boolean }): void => {
const { room } = this.state;
const { serverVersion } = this.props;
if (compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '5.0.0')) {
videoConfStartAndJoin(room.rid, video);
} else {
callJitsi(room, !video);
}
};
renderRoomInfo = () => { renderRoomInfo = () => {
const { room, member } = this.state; const { room, member } = this.state;
const { rid, name, t, topic, source } = room; const { rid, name, t, topic, source } = room;
@ -810,12 +825,35 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
renderJitsi = () => { renderJitsi = () => {
const { room } = this.state; const { room } = this.state;
const { jitsiEnabled, jitsiEnableTeams, jitsiEnableChannels } = this.props; const {
jitsiEnabled,
jitsiEnableTeams,
jitsiEnableChannels,
serverVersion,
videoConf_Enable_DMs,
videoConf_Enable_Channels,
videoConf_Enable_Groups,
videoConf_Enable_Teams
} = this.props;
const isJitsiDisabledForTeams = room.teamMain && !jitsiEnableTeams; const isJitsiDisabledForTeams = room.teamMain && !jitsiEnableTeams;
const isJitsiDisabledForChannels = !room.teamMain && (room.t === 'p' || room.t === 'c') && !jitsiEnableChannels; const isJitsiDisabledForChannels = !room.teamMain && (room.t === 'p' || room.t === 'c') && !jitsiEnableChannels;
if (!jitsiEnabled || isJitsiDisabledForTeams || isJitsiDisabledForChannels) { const isVideoConfDisabledForTeams = !!room.teamMain && !videoConf_Enable_Teams;
const isVideoConfDisabledForChannels = !room.teamMain && room.t === 'c' && !videoConf_Enable_Channels;
const isVideoConfDisabledForGroups = !room.teamMain && room.t === 'p' && !videoConf_Enable_Groups;
const isVideoConfDisabledForDirect = !room.teamMain && room.t === 'd' && !videoConf_Enable_DMs;
if (compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '5.0.0')) {
if (
isVideoConfDisabledForTeams ||
isVideoConfDisabledForChannels ||
isVideoConfDisabledForGroups ||
isVideoConfDisabledForDirect
) {
return null;
}
} else if (!jitsiEnabled || isJitsiDisabledForTeams || isJitsiDisabledForChannels) {
return null; return null;
} }
@ -824,7 +862,7 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
<List.Separator /> <List.Separator />
<List.Item <List.Item
title='Voice_call' title='Voice_call'
onPress={() => callJitsi(room, true)} onPress={() => this.startVideoConf({ video: false })}
testID='room-actions-voice' testID='room-actions-voice'
left={() => <List.Icon name='phone' />} left={() => <List.Icon name='phone' />}
showActionIndicator showActionIndicator
@ -832,7 +870,7 @@ class RoomActionsView extends React.Component<IRoomActionsViewProps, IRoomAction
<List.Separator /> <List.Separator />
<List.Item <List.Item
title='Video_call' title='Video_call'
onPress={() => callJitsi(room)} onPress={() => this.startVideoConf({ video: true })}
testID='room-actions-video' testID='room-actions-video'
left={() => <List.Icon name='camera' />} left={() => <List.Icon name='camera' />}
showActionIndicator showActionIndicator
@ -1309,6 +1347,10 @@ const mapStateToProps = (state: IApplicationState) => ({
jitsiEnabled: (state.settings.Jitsi_Enabled || false) as boolean, jitsiEnabled: (state.settings.Jitsi_Enabled || false) as boolean,
jitsiEnableTeams: (state.settings.Jitsi_Enable_Teams || false) as boolean, jitsiEnableTeams: (state.settings.Jitsi_Enable_Teams || false) as boolean,
jitsiEnableChannels: (state.settings.Jitsi_Enable_Channels || false) as boolean, jitsiEnableChannels: (state.settings.Jitsi_Enable_Channels || false) as boolean,
videoConf_Enable_DMs: (state.settings.VideoConf_Enable_DMs ?? true) as boolean,
videoConf_Enable_Channels: (state.settings.VideoConf_Enable_Channels ?? true) as boolean,
videoConf_Enable_Groups: (state.settings.VideoConf_Enable_Groups ?? true) as boolean,
videoConf_Enable_Teams: (state.settings.VideoConf_Enable_Teams ?? true) as boolean,
encryptionEnabled: state.encryption.enabled, encryptionEnabled: state.encryption.enabled,
serverVersion: state.server.version, serverVersion: state.server.version,
isMasterDetail: state.app.isMasterDetail, isMasterDetail: state.app.isMasterDetail,