diff --git a/app/containers/UIKit/VideoConferenceBlock/components/CallAgainActionSheet.tsx b/app/containers/UIKit/VideoConferenceBlock/components/StartACallActionSheet.tsx similarity index 70% rename from app/containers/UIKit/VideoConferenceBlock/components/CallAgainActionSheet.tsx rename to app/containers/UIKit/VideoConferenceBlock/components/StartACallActionSheet.tsx index 781c5b284..2158e3680 100644 --- a/app/containers/UIKit/VideoConferenceBlock/components/CallAgainActionSheet.tsx +++ b/app/containers/UIKit/VideoConferenceBlock/components/StartACallActionSheet.tsx @@ -6,7 +6,6 @@ import i18n from '../../../../i18n'; import { getSubscriptionByRoomId } from '../../../../lib/database/services/Subscription'; import { useAppSelector } from '../../../../lib/hooks'; import { getRoomAvatar, getUidDirectMessage } from '../../../../lib/methods/helpers'; -import { videoConfStartAndJoin } from '../../../../lib/methods/videoConf'; import { useTheme } from '../../../../theme'; import { useActionSheet } from '../../../ActionSheet'; import AvatarContainer from '../../../Avatar'; @@ -16,12 +15,12 @@ import { BUTTON_HIT_SLOP } from '../../../message/utils'; import StatusContainer from '../../../Status'; import useStyle from './styles'; -export default function CallAgainActionSheet({ rid }: { rid: string }): React.ReactElement { +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: '', rid: '' }); - const [phone, setPhone] = useState(true); - const [camera, setCamera] = useState(false); + const [user, setUser] = useState({ username: '', avatar: '', uid: '' }); + const [mic, setMic] = useState(true); + const [cam, setCam] = useState(false); const username = useAppSelector(state => state.login.user.username); const { hideActionSheet } = useActionSheet(); @@ -31,7 +30,7 @@ export default function CallAgainActionSheet({ rid }: { rid: string }): React.Re const room = await getSubscriptionByRoomId(rid); const uid = (await getUidDirectMessage(room)) as string; const avt = getRoomAvatar(room); - setUser({ uid, username: room?.name || '', avatar: avt, rid: room?.id || '' }); + setUser({ uid, username: room?.name || '', avatar: avt }); })(); }, [rid]); @@ -43,25 +42,27 @@ export default function CallAgainActionSheet({ rid }: { rid: string }): React.Re {i18n.t('Start_a_call')} setCamera(!camera)} - style={[style.iconCallContainer, camera && style.enabledBackground, { marginRight: 6 }]} + onPress={() => setCam(!cam)} + style={[style.iconCallContainer, cam && style.enabledBackground, { marginRight: 6 }]} hitSlop={BUTTON_HIT_SLOP} > - + setPhone(!phone)} - style={[style.iconCallContainer, phone && style.enabledBackground]} + onPress={() => setMic(!mic)} + style={[style.iconCallContainer, mic && style.enabledBackground]} hitSlop={BUTTON_HIT_SLOP} > - + - {user.username} + + {user.username} + @@ -70,7 +71,7 @@ export default function CallAgainActionSheet({ rid }: { rid: string }): React.Re onPress={() => { hideActionSheet(); setTimeout(() => { - videoConfStartAndJoin(user.rid, camera); + initCall({ cam, mic }); }, 100); }} title={i18n.t('Call')} diff --git a/app/containers/UIKit/VideoConferenceBlock/components/VideoConferenceDirect.tsx b/app/containers/UIKit/VideoConferenceBlock/components/VideoConferenceDirect.tsx index 2526a190f..d6fc28662 100644 --- a/app/containers/UIKit/VideoConferenceBlock/components/VideoConferenceDirect.tsx +++ b/app/containers/UIKit/VideoConferenceBlock/components/VideoConferenceDirect.tsx @@ -3,17 +3,16 @@ import { Text } from 'react-native'; import Touchable from 'react-native-platform-touchable'; import i18n from '../../../../i18n'; -import { useVideoConf } from '../../../../lib/hooks/useVideoConf'; +import { videoConfJoin } from '../../../../lib/methods/videoConf'; import useStyle from './styles'; import { VideoConferenceBaseContainer } from './VideoConferenceBaseContainer'; const VideoConferenceDirect = React.memo(({ blockId }: { blockId: string }) => { const style = useStyle(); - const { joinCall } = useVideoConf(); return ( - joinCall(blockId)}> + videoConfJoin(blockId)}> {i18n.t('Join')} {i18n.t('Waiting_for_answer')} diff --git a/app/containers/UIKit/VideoConferenceBlock/components/VideoConferenceEnded.tsx b/app/containers/UIKit/VideoConferenceBlock/components/VideoConferenceEnded.tsx index 8c8171b3a..b4c95fa8e 100644 --- a/app/containers/UIKit/VideoConferenceBlock/components/VideoConferenceEnded.tsx +++ b/app/containers/UIKit/VideoConferenceBlock/components/VideoConferenceEnded.tsx @@ -6,9 +6,7 @@ import { IUser } from '../../../../definitions'; import { VideoConferenceType } from '../../../../definitions/IVideoConference'; import i18n from '../../../../i18n'; import { useAppSelector } from '../../../../lib/hooks'; -import { useSnaps } from '../../../../lib/hooks/useSnaps'; -import { useActionSheet } from '../../../ActionSheet'; -import CallAgainActionSheet from './CallAgainActionSheet'; +import { useVideoConf } from '../../../../lib/hooks/useVideoConf'; import { CallParticipants, TCallUsers } from './CallParticipants'; import useStyle from './styles'; import { VideoConferenceBaseContainer } from './VideoConferenceBaseContainer'; @@ -26,8 +24,7 @@ export default function VideoConferenceEnded({ }): React.ReactElement { const style = useStyle(); const username = useAppSelector(state => state.login.user.username); - const { showActionSheet } = useActionSheet(); - const snaps = useSnaps([1250]); + const { showInitCallActionSheet } = useVideoConf(rid); const onlyAuthorOnCall = users.length === 1 && users.some(user => user.username === createdBy.username); @@ -35,15 +32,7 @@ export default function VideoConferenceEnded({ {type === 'direct' ? ( <> - - showActionSheet({ - children: , - snaps - }) - } - > + {createdBy.username === username ? i18n.t('Call_back') : i18n.t('Call_again')} diff --git a/app/containers/UIKit/VideoConferenceBlock/components/VideoConferenceOutgoing.tsx b/app/containers/UIKit/VideoConferenceBlock/components/VideoConferenceOutgoing.tsx index 164e9b3ab..bebf26da1 100644 --- a/app/containers/UIKit/VideoConferenceBlock/components/VideoConferenceOutgoing.tsx +++ b/app/containers/UIKit/VideoConferenceBlock/components/VideoConferenceOutgoing.tsx @@ -3,18 +3,17 @@ import { Text } from 'react-native'; import Touchable from 'react-native-platform-touchable'; import i18n from '../../../../i18n'; -import { useVideoConf } from '../../../../lib/hooks/useVideoConf'; +import { videoConfJoin } from '../../../../lib/methods/videoConf'; import { CallParticipants, TCallUsers } from './CallParticipants'; import useStyle from './styles'; import { VideoConferenceBaseContainer } from './VideoConferenceBaseContainer'; export default function VideoConferenceOutgoing({ users, blockId }: { users: TCallUsers; blockId: string }): React.ReactElement { const style = useStyle(); - const { joinCall } = useVideoConf(); return ( - joinCall(blockId)}> + videoConfJoin(blockId)}> {i18n.t('Join')} diff --git a/app/containers/UIKit/VideoConferenceBlock/components/styles.ts b/app/containers/UIKit/VideoConferenceBlock/components/styles.ts index d8258e24d..df607ff8d 100644 --- a/app/containers/UIKit/VideoConferenceBlock/components/styles.ts +++ b/app/containers/UIKit/VideoConferenceBlock/components/styles.ts @@ -100,7 +100,8 @@ export default function useStyle() { actionSheetUsername: { fontSize: 16, ...sharedStyles.textBold, - color: colors.passcodePrimary + color: colors.passcodePrimary, + flexShrink: 1 }, enabledBackground: { backgroundColor: colors.conferenceCallEnabledIconBackground diff --git a/app/containers/UIKit/utils.ts b/app/containers/UIKit/utils.ts index f127f8068..b56eaaec1 100644 --- a/app/containers/UIKit/utils.ts +++ b/app/containers/UIKit/utils.ts @@ -2,7 +2,7 @@ import { BlockContext } from '@rocket.chat/ui-kit'; import React, { useContext, useState } from 'react'; -import { useVideoConf } from '../../lib/hooks/useVideoConf'; +import { videoConfJoin } from '../../lib/methods/videoConf'; import { IText } from './interfaces'; export const textParser = ([{ text }]: IText[]) => text; @@ -40,7 +40,6 @@ export const useBlockContext = ({ blockId, actionId, appId, initialValue }: IUse const { action, appId: appIdFromContext, viewId, state, language, errors, values = {} } = useContext(KitContext); const { value = initialValue } = values[actionId] || {}; const [loading, setLoading] = useState(false); - const { joinCall } = useVideoConf(); const error = errors && actionId && errors[actionId]; @@ -58,7 +57,7 @@ export const useBlockContext = ({ blockId, actionId, appId, initialValue }: IUse try { if (appId === 'videoconf-core' && blockId) { setLoading(false); - return joinCall(blockId); + return videoConfJoin(blockId); } await action({ blockId, diff --git a/app/containers/message/CallButton.tsx b/app/containers/message/CallButton.tsx index 9a81ca7ce..265ebb268 100644 --- a/app/containers/message/CallButton.tsx +++ b/app/containers/message/CallButton.tsx @@ -10,12 +10,12 @@ import { themes } from '../../lib/constants'; import { IMessageCallButton } from './interfaces'; import { useTheme } from '../../theme'; -const CallButton = React.memo(({ callJitsi }: IMessageCallButton) => { +const CallButton = React.memo(({ handleEnterCall }: IMessageCallButton) => { const { theme } = useTheme(); return ( void; onReactionLongPress?: (item: TAnyMessageModel) => void; navToRoomInfo: (navParam: IRoomInfoParam) => void; - callJitsi?: () => void; + handleEnterCall?: () => void; blockAction?: (params: { actionId: string; appId: string; value: string; blockId: string; rid: string; mid: string }) => void; onAnswerButtonPress?: (message: string, tmid?: string, tshow?: boolean) => void; threadBadgeColor?: string; @@ -69,7 +69,6 @@ class MessageContainer extends React.Component null, onLongPress: () => {}, - callJitsi: () => {}, blockAction: () => {}, archived: false, broadcast: false, @@ -338,7 +337,7 @@ class MessageContainer extends React.Component void; + handleEnterCall?: () => void; } export interface IMessageContent { diff --git a/app/definitions/IVideoConference.ts b/app/definitions/IVideoConference.ts index af689be47..1ba428cb3 100644 --- a/app/definitions/IVideoConference.ts +++ b/app/definitions/IVideoConference.ts @@ -4,37 +4,34 @@ import type { IRoom } from './IRoom'; import type { IUser } from './IUser'; import type { IMessage } from './IMessage'; -export enum VideoConferenceStatus { +export declare enum VideoConferenceStatus { CALLING = 0, STARTED = 1, EXPIRED = 2, ENDED = 3, DECLINED = 4 } - -export type DirectCallInstructions = { +export declare type DirectCallInstructions = { type: 'direct'; - callee: IUser['_id']; + calleeId: IUser['_id']; callId: string; }; - -export type ConferenceInstructions = { +export declare type ConferenceInstructions = { type: 'videoconference'; callId: string; rid: IRoom['_id']; }; - -export type LivechatInstructions = { +export declare type LivechatInstructions = { type: 'livechat'; callId: string; }; - -export type VideoConferenceType = DirectCallInstructions['type'] | ConferenceInstructions['type'] | LivechatInstructions['type']; - +export declare type VideoConferenceType = + | DirectCallInstructions['type'] + | ConferenceInstructions['type'] + | LivechatInstructions['type']; export interface IVideoConferenceUser extends Pick, '_id' | 'username' | 'name' | 'avatarETag'> { ts: Date; } - export interface IVideoConference extends IRocketChatRecord { type: VideoConferenceType; rid: string; @@ -45,51 +42,68 @@ export interface IVideoConference extends IRocketChatRecord { ended?: IMessage['_id']; }; url?: string; - - createdBy: Pick; + createdBy: Pick, '_id' | 'username' | 'name'>; createdAt: Date; - - endedBy?: Pick; + endedBy?: Pick, '_id' | 'username' | 'name'>; endedAt?: Date; - providerName: string; providerData?: Record; - ringing?: boolean; } - export interface IDirectVideoConference extends IVideoConference { type: 'direct'; } - export interface IGroupVideoConference extends IVideoConference { type: 'videoconference'; anonymousUsers: number; title: string; } - export interface ILivechatVideoConference extends IVideoConference { type: 'livechat'; } - -export type VideoConference = IDirectVideoConference | IGroupVideoConference | ILivechatVideoConference; - -export type VideoConferenceInstructions = DirectCallInstructions | ConferenceInstructions | LivechatInstructions; - -export const isDirectVideoConference = (call: VideoConference | undefined | null): call is IDirectVideoConference => - call?.type === 'direct'; - -export const isGroupVideoConference = (call: VideoConference | undefined | null): call is IGroupVideoConference => - call?.type === 'videoconference'; - -export const isLivechatVideoConference = (call: VideoConference | undefined | null): call is ILivechatVideoConference => - call?.type === 'livechat'; - -type GroupVideoConferenceCreateData = Omit & { createdBy: IUser['_id'] }; -type DirectVideoConferenceCreateData = Omit & { createdBy: IUser['_id'] }; -type LivechatVideoConferenceCreateData = Omit & { createdBy: IUser['_id'] }; - -export type VideoConferenceCreateData = AtLeast< +export declare type VideoConference = IDirectVideoConference | IGroupVideoConference | ILivechatVideoConference; +export declare type VideoConferenceInstructions = DirectCallInstructions | ConferenceInstructions | LivechatInstructions; +export declare const isDirectVideoConference: (call: VideoConference | undefined | null) => call is IDirectVideoConference; +export declare const isGroupVideoConference: (call: VideoConference | undefined | null) => call is IGroupVideoConference; +export declare const isLivechatVideoConference: (call: VideoConference | undefined | null) => call is ILivechatVideoConference; +declare type GroupVideoConferenceCreateData = Omit & { + createdBy: IUser['_id']; +}; +declare type DirectVideoConferenceCreateData = Omit & { + createdBy: IUser['_id']; +}; +declare type LivechatVideoConferenceCreateData = Omit & { + createdBy: IUser['_id']; +}; +export declare type VideoConferenceCreateData = AtLeast< DirectVideoConferenceCreateData | GroupVideoConferenceCreateData | LivechatVideoConferenceCreateData, 'createdBy' | 'type' | 'rid' | 'providerName' | 'providerData' >; + +export type VideoConferenceCapabilities = { + mic?: boolean; + cam?: boolean; + title?: boolean; +}; + +export type VideoConfStartProps = { roomId: string; title?: string; allowRinging?: boolean }; + +export type VideoConfJoinProps = { + callId: string; + state?: { + mic?: boolean; + cam?: boolean; + }; +}; + +export type VideoConfCancelProps = { + callId: string; +}; + +export type VideoConfListProps = { + roomId: string; + count?: number; + offset?: number; +}; + +export type VideoConfInfoProps = { callId: string }; diff --git a/app/definitions/rest/v1/videoConference.ts b/app/definitions/rest/v1/videoConference.ts index ba681df28..2d93cd7b6 100644 --- a/app/definitions/rest/v1/videoConference.ts +++ b/app/definitions/rest/v1/videoConference.ts @@ -1,27 +1,45 @@ -import { VideoConference } from '../../IVideoConference'; +import { + VideoConfCancelProps, + VideoConference, + VideoConferenceCapabilities, + VideoConferenceInstructions, + VideoConfInfoProps, + VideoConfJoinProps, + VideoConfListProps, + VideoConfStartProps +} from '../../IVideoConference'; +import { PaginatedResult } from '../helpers/PaginatedResult'; export type VideoConferenceEndpoints = { - 'video-conference/jitsi.update-timeout': { - 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 }; + POST: (params: VideoConfStartProps) => { data: VideoConferenceInstructions & { providerName: string } }; + }; + + 'video-conference.join': { + POST: (params: VideoConfJoinProps) => { url: string; providerName: string }; }; 'video-conference.cancel': { - POST: (params: { callId: string }) => void; + POST: (params: VideoConfCancelProps) => void; }; 'video-conference.info': { - GET: (params: { callId: string }) => VideoConference & { - capabilities: { - mic?: boolean; - cam?: boolean; - title?: boolean; - }; - }; + GET: (params: VideoConfInfoProps) => VideoConference & { capabilities: VideoConferenceCapabilities }; + }; + + 'video-conference.list': { + GET: (params: VideoConfListProps) => PaginatedResult<{ data: VideoConference[] }>; + }; + + 'video-conference.capabilities': { + GET: () => { providerName: string; capabilities: VideoConferenceCapabilities }; + }; + + 'video-conference.providers': { + GET: () => { data: { key: string; label: string }[] }; + }; + + 'video-conference/jitsi.update-timeout': { + POST: (params: { roomId: string }) => void; }; }; diff --git a/app/i18n/locales/en.json b/app/i18n/locales/en.json index 20f4360dd..c695d3c63 100644 --- a/app/i18n/locales/en.json +++ b/app/i18n/locales/en.json @@ -877,6 +877,18 @@ "Reply_in_direct_message": "Reply in Direct Message", "room_archived": "archived room", "room_unarchived": "unarchived room", + "no-videoconf-provider-app-header": "Conference call not available", + "no-videoconf-provider-app-body": "Conference call apps can be installed in the Rocket.Chat marketplace by a workspace admin.", + "admin-no-videoconf-provider-app-header": "Conference call not enabled", + "admin-no-videoconf-provider-app-body": "Conference call apps are available in the Rocket.Chat marketplace.", + "no-active-video-conf-provider-header": "Conference call not enabled", + "no-active-video-conf-provider-body": "A workspace admin needs to enable the conference call feature first.", + "admin-no-active-video-conf-provider-header": "Conference call not enabled", + "admin-no-active-video-conf-provider-body": "Configure conference calls in order to make it available on this workspace.", + "video-conf-provider-not-configured-header": "Conference call not enabled", + "video-conf-provider-not-configured-body": "A workspace admin needs to enable the conference calls feature first.", + "admin-video-conf-provider-not-configured-header": "Conference call not enabled", + "admin-video-conf-provider-not-configured-body": "Configure conference calls in order to make it available on this workspace.", "Presence_Cap_Warning_Title": "User status temporarily disabled", "Presence_Cap_Warning_Description": "Active connections have reached the limit for the workspace, thus the service that handles user status is disabled. It can be re-enabled manually in workspace settings.", "Learn_more": "Learn more" diff --git a/app/lib/hooks/useVideoConf.ts b/app/lib/hooks/useVideoConf.ts deleted file mode 100644 index 02aba7c7c..000000000 --- a/app/lib/hooks/useVideoConf.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { useCallback } from 'react'; - -import { TActionSheetOptionsItem, useActionSheet } from '../../containers/ActionSheet'; -import i18n from '../../i18n'; -import { videoConfJoin } from '../methods/videoConf'; - -export const useVideoConf = (): { joinCall: (blockId: string) => void } => { - const { showActionSheet } = useActionSheet(); - - const joinCall = useCallback(blockId => { - 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 { joinCall }; -}; diff --git a/app/lib/hooks/useVideoConf.tsx b/app/lib/hooks/useVideoConf.tsx new file mode 100644 index 000000000..6b79f59eb --- /dev/null +++ b/app/lib/hooks/useVideoConf.tsx @@ -0,0 +1,113 @@ +import React, { useEffect, useState } from 'react'; +import { Q } from '@nozbe/watermelondb'; + +import { useActionSheet } from '../../containers/ActionSheet'; +import StartACallActionSheet from '../../containers/UIKit/VideoConferenceBlock/components/StartACallActionSheet'; +import { ISubscription, SubscriptionType, TSubscriptionModel } from '../../definitions'; +import i18n from '../../i18n'; +import { getUserSelector } from '../../selectors/login'; +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 { Services } from '../services'; +import { useAppSelector } from './useAppSelector'; +import { useSnaps } from './useSnaps'; + +const availabilityErrors = { + NOT_CONFIGURED: 'video-conf-provider-not-configured', + NOT_ACTIVE: 'no-active-video-conf-provider', + NO_APP: 'no-videoconf-provider-app' +} as const; + +const handleErrors = (isAdmin: boolean, error: typeof availabilityErrors[keyof typeof availabilityErrors]) => { + if (isAdmin) return showErrorAlert(i18n.t(`admin-${error}-body`), i18n.t(`admin-${error}-header`)); + return showErrorAlert(i18n.t(`${error}-body`), i18n.t(`${error}-header`)); +}; + +export const useVideoConf = (rid: string): { showInitCallActionSheet: () => Promise; showCallOption: boolean } => { + const [showCallOption, setShowCallOption] = useState(false); + + const serverVersion = useAppSelector(state => state.server.version); + const jitsiEnabled = useAppSelector(state => state.settings.Jitsi_Enabled); + const jitsiEnableTeams = useAppSelector(state => state.settings.Jitsi_Enable_Teams); + const jitsiEnableChannels = useAppSelector(state => state.settings.Jitsi_Enable_Channels); + const user = useAppSelector(state => getUserSelector(state)); + + const isServer5OrNewer = compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '5.0.0'); + + const { showActionSheet } = useActionSheet(); + const snaps = useSnaps([1250]); + + const handleShowCallOption = (room: TSubscriptionModel) => { + if (isServer5OrNewer) return setShowCallOption(true); + const isJitsiDisabledForTeams = room.teamMain && !jitsiEnableTeams; + const isJitsiDisabledForChannels = !room.teamMain && (room.t === 'p' || room.t === 'c') && !jitsiEnableChannels; + + if (room.t === SubscriptionType.DIRECT) return setShowCallOption(!!jitsiEnabled); + if (room.t === SubscriptionType.CHANNEL) return setShowCallOption(!isJitsiDisabledForChannels); + if (room.t === SubscriptionType.GROUP) return setShowCallOption(!isJitsiDisabledForTeams); + + return setShowCallOption(false); + }; + + const canInitAnCall = async () => { + if (isServer5OrNewer) { + try { + await Services.videoConferenceGetCapabilities(); + return true; + } catch (error: any) { + const isAdmin = !!['admin'].find(role => user.roles?.includes(role)); + switch (error?.error) { + case availabilityErrors.NOT_CONFIGURED: + return handleErrors(isAdmin, availabilityErrors.NOT_CONFIGURED); + case availabilityErrors.NOT_ACTIVE: + return handleErrors(isAdmin, availabilityErrors.NOT_ACTIVE); + case availabilityErrors.NO_APP: + return handleErrors(isAdmin, availabilityErrors.NO_APP); + default: + return handleErrors(isAdmin, availabilityErrors.NOT_CONFIGURED); + } + } + } + return true; + }; + + const initCall = async ({ cam, mic }: { cam: boolean; mic: boolean }) => { + if (isServer5OrNewer) return videoConfStartAndJoin({ rid, cam, mic }); + const room = (await getSubscriptionByRoomId(rid)) as ISubscription; + callJitsi({ room, cam }); + }; + + const showInitCallActionSheet = async () => { + const canInit = await canInitAnCall(); + if (canInit) { + showActionSheet({ + children: , + snaps + }); + } + }; + + const initSubscription = () => { + try { + const db = database.active; + const observeSubCollection = db.get('subscriptions').query(Q.where('rid', rid)).observe(); + const subObserveQuery = observeSubCollection.subscribe(data => { + if (data[0]) { + handleShowCallOption(data[0]); + subObserveQuery.unsubscribe(); + } + }); + } catch (e) { + console.log("observeSubscriptions: Can't find subscription to observe"); + } + }; + + useEffect(() => { + initSubscription(); + }, []); + + return { showInitCallActionSheet, showCallOption }; +}; diff --git a/app/lib/methods/callJitsi.ts b/app/lib/methods/callJitsi.ts index b6e836408..dfcdcc071 100644 --- a/app/lib/methods/callJitsi.ts +++ b/app/lib/methods/callJitsi.ts @@ -46,8 +46,8 @@ export function callJitsiWithoutServer(path: string): void { Navigation.navigate('JitsiMeetView', { url, onlyAudio: false }); } -export async function callJitsi(room: ISubscription, onlyAudio = false): Promise { - logEvent(onlyAudio ? events.RA_JITSI_AUDIO : events.RA_JITSI_VIDEO); +export async function callJitsi({ room, cam = false }: { room: ISubscription; cam?: boolean }): Promise { + logEvent(cam ? events.RA_JITSI_AUDIO : events.RA_JITSI_VIDEO); const url = await jitsiURL({ room }); - Navigation.navigate('JitsiMeetView', { url, onlyAudio, rid: room?.rid }); + Navigation.navigate('JitsiMeetView', { url, onlyAudio: cam, rid: room?.rid }); } diff --git a/app/lib/methods/videoConf.ts b/app/lib/methods/videoConf.ts index 255d18688..3385ea7ef 100644 --- a/app/lib/methods/videoConf.ts +++ b/app/lib/methods/videoConf.ts @@ -19,9 +19,9 @@ const handleBltPermission = async (): Promise => { return [PermissionsAndroid.PERMISSIONS.ACCESS_COARSE_LOCATION]; }; -export const videoConfJoin = async (callId: string, cam: boolean) => { +export const videoConfJoin = async (callId: string, cam?: boolean, mic?: boolean): Promise => { try { - const result = await Services.videoConferenceJoin(callId, cam); + const result = await Services.videoConferenceJoin(callId, cam, mic); if (result.success) { if (isAndroid) { const bltPermission = await handleBltPermission(); @@ -44,11 +44,11 @@ export const videoConfJoin = async (callId: string, cam: boolean) => { } }; -export const videoConfStartAndJoin = async (rid: string, cam: boolean) => { +export const videoConfStartAndJoin = async ({ rid, cam, mic }: { rid: string; cam?: boolean; mic?: boolean }): Promise => { try { - const videoConfResponse: any = await Services.videoConferenceStart(rid); + const videoConfResponse = await Services.videoConferenceStart(rid); if (videoConfResponse.success) { - videoConfJoin(videoConfResponse.data.callId, cam); + videoConfJoin(videoConfResponse.data.callId, cam, mic); } } catch (e) { showErrorAlert(i18n.t('error-init-video-conf')); diff --git a/app/lib/methods/videoConfTimer.ts b/app/lib/methods/videoConfTimer.ts new file mode 100644 index 000000000..46bcd3ed1 --- /dev/null +++ b/app/lib/methods/videoConfTimer.ts @@ -0,0 +1,27 @@ +import BackgroundTimer from 'react-native-background-timer'; + +import { Services } from '../services'; + +let interval: number | null = null; + +export const initVideoConfTimer = (rid: string): void => { + if (rid) { + Services.updateJitsiTimeout(rid).catch((e: unknown) => console.log(e)); + if (interval) { + BackgroundTimer.clearInterval(interval); + BackgroundTimer.stopBackgroundTimer(); + interval = null; + } + interval = BackgroundTimer.setInterval(() => { + Services.updateJitsiTimeout(rid).catch((e: unknown) => console.log(e)); + }, 10000); + } +}; + +export const endVideoConfTimer = (): void => { + if (interval) { + BackgroundTimer.clearInterval(interval); + interval = null; + BackgroundTimer.stopBackgroundTimer(); + } +}; diff --git a/app/lib/services/restApi.ts b/app/lib/services/restApi.ts index eefb89831..d73c1d9df 100644 --- a/app/lib/services/restApi.ts +++ b/app/lib/services/restApi.ts @@ -936,8 +936,10 @@ export function getUserInfo(userId: string) { 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 videoConferenceJoin = (callId: string, cam?: boolean, mic?: boolean) => + sdk.post('video-conference.join', { callId, state: { cam: !!cam, mic: mic === undefined ? true : mic } }); + +export const videoConferenceGetCapabilities = () => sdk.get('video-conference.capabilities'); export const videoConferenceStart = (roomId: string) => sdk.post('video-conference.start', { roomId }); diff --git a/app/views/JitsiMeetView.android.tsx b/app/views/JitsiMeetView.android.tsx index ce54691c1..45265494d 100644 --- a/app/views/JitsiMeetView.android.tsx +++ b/app/views/JitsiMeetView.android.tsx @@ -1,14 +1,13 @@ +import { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake'; 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 { activateKeepAwake, deactivateKeepAwake } from 'expo-keep-awake'; import { IBaseScreen } from '../definitions'; import { events, logEvent } from '../lib/methods/helpers/log'; -import { Services } from '../lib/services'; +import { endVideoConfTimer, initVideoConfTimer } from '../lib/methods/videoConfTimer'; import { ChatsStackParamList } from '../stacks/types'; import { withTheme } from '../theme'; @@ -20,7 +19,6 @@ class JitsiMeetView extends React.Component { private rid: string; private url: string; private videoConf: boolean; - private jitsiTimeout: number | null; private backHandler!: NativeEventSubscription; constructor(props: TJitsiMeetViewProps) { @@ -28,7 +26,6 @@ class JitsiMeetView extends React.Component { this.rid = props.route.params?.rid; this.url = props.route.params?.url; this.videoConf = !!props.route.params?.videoConf; - this.jitsiTimeout = null; } componentDidMount() { @@ -50,10 +47,8 @@ class JitsiMeetView extends React.Component { 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(); + if (!this.videoConf) { + endVideoConfTimer(); } this.backHandler.remove(); deactivateKeepAwake(); @@ -64,15 +59,7 @@ class JitsiMeetView extends React.Component { 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); + initVideoConfTimer(this.rid); } }; @@ -90,7 +77,7 @@ class JitsiMeetView extends React.Component { render() { return ( this.onNavigationStateChange(nativeEvent)} onNavigationStateChange={this.onNavigationStateChange} style={{ flex: 1 }} diff --git a/app/views/JitsiMeetView.ios.tsx b/app/views/JitsiMeetView.ios.tsx index effbe2331..d4f0c4c72 100644 --- a/app/views/JitsiMeetView.ios.tsx +++ b/app/views/JitsiMeetView.ios.tsx @@ -9,6 +9,7 @@ import { useAppSelector } from '../lib/hooks'; import { events, logEvent } from '../lib/methods/helpers/log'; import { getUserSelector } from '../selectors/login'; import { ChatsStackParamList } from '../stacks/types'; +import { endVideoConfTimer, initVideoConfTimer } from '../lib/methods/videoConfTimer'; const formatUrl = (url: string, baseUrl: string, uriSize: number, avatarAuthURLFragment: string) => `${baseUrl}/avatar/${url}?format=png&width=${uriSize}&height=${uriSize}${avatarAuthURLFragment}`; @@ -16,7 +17,7 @@ const formatUrl = (url: string, baseUrl: string, uriSize: number, avatarAuthURLF const JitsiMeetView = (): React.ReactElement => { const { goBack } = useNavigation(); const { - params: { url, onlyAudio, videoConf } + params: { url, onlyAudio, videoConf, rid } } = useRoute>(); const user = useAppSelector(state => getUserSelector(state)); const baseUrl = useAppSelector(state => state.server.server); @@ -60,8 +61,10 @@ const JitsiMeetView = (): React.ReactElement => { } }; logEvent(videoConf ? events.LIVECHAT_VIDEOCONF_JOIN : events.JM_CONFERENCE_JOIN); + if (!videoConf) initVideoConfTimer(rid); await JitsiMeet.launchJitsiMeetView(conferenceOptions); logEvent(videoConf ? events.LIVECHAT_VIDEOCONF_TERMINATE : events.JM_CONFERENCE_TERMINATE); + if (!videoConf) endVideoConfTimer(); goBack(); }; diff --git a/app/views/RoomActionsView/components/CallSection.tsx b/app/views/RoomActionsView/components/CallSection.tsx new file mode 100644 index 000000000..180bbbb52 --- /dev/null +++ b/app/views/RoomActionsView/components/CallSection.tsx @@ -0,0 +1,25 @@ +import React from 'react'; + +import * as List from '../../../containers/List'; +import i18n from '../../../i18n'; +import { useVideoConf } from '../../../lib/hooks/useVideoConf'; + +export default function CallSection({ rid }: { rid: string }): React.ReactElement | null { + const { showCallOption, showInitCallActionSheet } = useVideoConf(rid); + + if (showCallOption) + return ( + + + } + showActionIndicator + /> + + + ); + return null; +} diff --git a/app/views/RoomActionsView/index.tsx b/app/views/RoomActionsView/index.tsx index 9c3a4eaaa..8e0a409b6 100644 --- a/app/views/RoomActionsView/index.tsx +++ b/app/views/RoomActionsView/index.tsx @@ -33,7 +33,7 @@ import sharedStyles from '../Styles'; import styles from './styles'; import { ERoomType } from '../../definitions/ERoomType'; import { E2E_ROOM_TYPES, SWITCH_TRACK_COLOR, themes } from '../../lib/constants'; -import { callJitsi, getPermalinkChannel } from '../../lib/methods'; +import { getPermalinkChannel } from '../../lib/methods'; import { canAutoTranslate as canAutoTranslateMethod, getRoomAvatar, @@ -48,9 +48,9 @@ import { getSubscriptionByRoomId } from '../../lib/database/services/Subscriptio import { IActionSheetProvider, withActionSheet } from '../../containers/ActionSheet'; import { MasterDetailInsideStackParamList } from '../../stacks/MasterDetailStack/types'; import { closeLivechat } from '../../lib/methods/helpers/closeLivechat'; -import { videoConfStartAndJoin } from '../../lib/methods/videoConf'; import { ILivechatDepartment } from '../../definitions/ILivechatDepartment'; import { ILivechatTag } from '../../definitions/ILivechatTag'; +import CallSection from './components/CallSection'; interface IOnPressTouch { (item: { route?: T; params?: ChatsStackParamList[T]; event?: Function }): void; @@ -730,16 +730,6 @@ class RoomActionsView extends React.Component { - const { room } = this.state; - const { serverVersion } = this.props; - if (compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '5.0.0')) { - videoConfStartAndJoin(room.rid, video); - } else { - callJitsi(room, !video); - } - }; - renderRoomInfo = () => { const { room, member } = this.state; const { rid, name, t, topic, source } = room; @@ -815,63 +805,6 @@ class RoomActionsView extends React.Component { - const { room } = this.state; - 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 isJitsiDisabledForChannels = !room.teamMain && (room.t === 'p' || room.t === 'c') && !jitsiEnableChannels; - - 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 ( - - - this.startVideoConf({ video: false })} - testID='room-actions-voice' - left={() => } - showActionIndicator - /> - - this.startVideoConf({ video: true })} - testID='room-actions-video' - left={() => } - showActionIndicator - /> - - - ); - }; - renderE2EEncryption = () => { const { room } = this.state; const { encryptionEnabled } = this.props; @@ -1108,7 +1041,7 @@ class RoomActionsView extends React.Component {this.renderRoomInfo()} - {this.renderJitsi()} + {this.renderE2EEncryption()} @@ -1299,13 +1232,6 @@ class RoomActionsView extends React.Component ({ userId: getUserSelector(state).id, - jitsiEnabled: (state.settings.Jitsi_Enabled || false) as boolean, - jitsiEnableTeams: (state.settings.Jitsi_Enable_Teams || 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, serverVersion: state.server.version, isMasterDetail: state.app.isMasterDetail, diff --git a/app/views/RoomInfoView/components/UserInfoButton.tsx b/app/views/RoomInfoView/components/UserInfoButton.tsx new file mode 100644 index 000000000..7185c8112 --- /dev/null +++ b/app/views/RoomInfoView/components/UserInfoButton.tsx @@ -0,0 +1,48 @@ +import React from 'react'; +import { Text } from 'react-native'; +import { BorderlessButton } from 'react-native-gesture-handler'; + +import { CustomIcon, TIconsName } from '../../../containers/CustomIcon'; +import styles from '../styles'; +import { useTheme } from '../../../theme'; +import { useVideoConf } from '../../../lib/hooks/useVideoConf'; +import i18n from '../../../i18n'; +import { useAppSelector } from '../../../lib/hooks'; +import { compareServerVersion } from '../../../lib/methods/helpers'; + +// TODO: change other icons on future +function UserInfoButton({ + danger, + iconName, + onPress, + label, + showIcon +}: { + danger?: boolean; + iconName: TIconsName; + onPress?: (prop: any) => void; + label: string; + showIcon?: boolean; +}): React.ReactElement | null { + const { colors } = useTheme(); + const color = danger ? colors.dangerColor : colors.actionTintColor; + + if (showIcon) + return ( + + + {label} + + ); + return null; +} + +export function CallButton({ rid, isDirect }: { rid: string; isDirect: boolean }): React.ReactElement | null { + const { showCallOption, showInitCallActionSheet } = useVideoConf(rid); + const serverVersion = useAppSelector(state => state.server.version); + const greaterThanFive = compareServerVersion(serverVersion, 'greaterThanOrEqualTo', '5.0.0'); + + const showIcon = greaterThanFive ? showCallOption : showCallOption && isDirect; + + return ; +} diff --git a/app/views/RoomInfoView/index.tsx b/app/views/RoomInfoView/index.tsx index 2744c0cc6..d57a23bae 100644 --- a/app/views/RoomInfoView/index.tsx +++ b/app/views/RoomInfoView/index.tsx @@ -1,43 +1,43 @@ +import { CompositeNavigationProp, RouteProp } from '@react-navigation/native'; +import { StackNavigationProp } from '@react-navigation/stack'; +import isEmpty from 'lodash/isEmpty'; import React from 'react'; import { ScrollView, Text, View } from 'react-native'; import { BorderlessButton } from 'react-native-gesture-handler'; import { connect } from 'react-redux'; -import UAParser from 'ua-parser-js'; -import isEmpty from 'lodash/isEmpty'; -import { StackNavigationProp } from '@react-navigation/stack'; -import { CompositeNavigationProp, RouteProp } from '@react-navigation/native'; import { Observable, Subscription } from 'rxjs'; +import UAParser from 'ua-parser-js'; -import { CustomIcon, TIconsName } from '../../containers/CustomIcon'; -import Status from '../../containers/Status'; import Avatar from '../../containers/Avatar'; -import sharedStyles from '../Styles'; -import RoomTypeIcon from '../../containers/RoomTypeIcon'; -import I18n from '../../i18n'; +import { CustomIcon, TIconsName } from '../../containers/CustomIcon'; import * as HeaderButton from '../../containers/HeaderButton'; -import StatusBar from '../../containers/StatusBar'; -import log, { events, logEvent } from '../../lib/methods/helpers/log'; -import { themes } from '../../lib/constants'; -import { TSupportedThemes, withTheme } from '../../theme'; import { MarkdownPreview } from '../../containers/markdown'; -import { LISTENER } from '../../containers/Toast'; -import EventEmitter from '../../lib/methods/helpers/events'; +import RoomTypeIcon from '../../containers/RoomTypeIcon'; import SafeAreaView from '../../containers/SafeAreaView'; -import { goRoom } from '../../lib/methods/helpers/goRoom'; -import Navigation from '../../lib/navigation/appNavigation'; -import Livechat from './Livechat'; -import Channel from './Channel'; -import Direct from './Direct'; -import styles from './styles'; -import { ChatsStackParamList } from '../../stacks/types'; -import { MasterDetailInsideStackParamList } from '../../stacks/MasterDetailStack/types'; -import { SubscriptionType, TSubscriptionModel, ISubscription, IUser, IApplicationState } from '../../definitions'; +import Status from '../../containers/Status'; +import StatusBar from '../../containers/StatusBar'; +import { LISTENER } from '../../containers/Toast'; +import { IApplicationState, ISubscription, IUser, SubscriptionType, TSubscriptionModel } from '../../definitions'; import { ILivechatVisitor } from '../../definitions/ILivechatVisitor'; -import { callJitsi } from '../../lib/methods'; -import { getRoomTitle, getUidDirectMessage, hasPermission } from '../../lib/methods/helpers'; -import { Services } from '../../lib/services'; +import I18n from '../../i18n'; +import { themes } from '../../lib/constants'; import { getSubscriptionByRoomId } from '../../lib/database/services/Subscription'; +import { getRoomTitle, getUidDirectMessage, hasPermission } from '../../lib/methods/helpers'; +import EventEmitter from '../../lib/methods/helpers/events'; +import { goRoom } from '../../lib/methods/helpers/goRoom'; import { handleIgnore } from '../../lib/methods/helpers/handleIgnore'; +import log, { events, logEvent } from '../../lib/methods/helpers/log'; +import Navigation from '../../lib/navigation/appNavigation'; +import { Services } from '../../lib/services'; +import { MasterDetailInsideStackParamList } from '../../stacks/MasterDetailStack/types'; +import { ChatsStackParamList } from '../../stacks/types'; +import { TSupportedThemes, withTheme } from '../../theme'; +import sharedStyles from '../Styles'; +import Channel from './Channel'; +import { CallButton } from './components/UserInfoButton'; +import Direct from './Direct'; +import Livechat from './Livechat'; +import styles from './styles'; interface IGetRoomTitle { room: ISubscription; @@ -386,11 +386,6 @@ class RoomInfoView extends React.Component { - const { room } = this.state; - callJitsi(room); - }; - handleBlockUser = async (rid: string, blocked: string, block: boolean) => { logEvent(events.RI_TOGGLE_BLOCK_USER); try { @@ -425,8 +420,7 @@ class RoomInfoView extends React.Component { - const { roomFromRid, roomUser } = this.state; - const { jitsiEnabled } = this.props; + const { roomFromRid, roomUser, room } = this.state; const isFromDm = roomFromRid?.rid ? new RegExp(roomUser._id).test(roomFromRid.rid) : false; const isDirectFromSaved = this.isDirect && this.fromRid && roomFromRid; @@ -442,9 +436,7 @@ class RoomInfoView extends React.Component {this.renderButton(() => this.handleCreateDirectMessage(this.goRoom), 'message', I18n.t('Message'))} - {jitsiEnabled && this.isDirect - ? this.renderButton(() => this.handleCreateDirectMessage(this.videoCall), 'camera', I18n.t('Video_call')) - : null} + {isDirectFromSaved && !isFromDm && !isDmWithMyself ? this.renderButton( () => handleIgnore(roomUser._id, !isIgnored, roomFromRid.rid), diff --git a/app/views/RoomView/RightButtons.tsx b/app/views/RoomView/RightButtons.tsx index 22001d5ee..a37b5d11f 100644 --- a/app/views/RoomView/RightButtons.tsx +++ b/app/views/RoomView/RightButtons.tsx @@ -10,8 +10,7 @@ import * as HeaderButton from '../../containers/HeaderButton'; import database from '../../lib/database'; import { getUserSelector } from '../../selectors/login'; import { events, logEvent } from '../../lib/methods/helpers/log'; -import { isTeamRoom } from '../../lib/methods/helpers/room'; -import { IApplicationState, SubscriptionType, TMessageModel, TSubscriptionModel } from '../../definitions'; +import { IApplicationState, ISubscription, SubscriptionType, TMessageModel, TSubscriptionModel } from '../../definitions'; import { ChatsStackParamList } from '../../stacks/types'; import { TActionSheetOptionsItem } from '../../containers/ActionSheet'; import i18n from '../../i18n'; @@ -20,12 +19,11 @@ import { onHoldLivechat, returnLivechat } from '../../lib/services/restApi'; import { closeLivechat as closeLivechatService } from '../../lib/methods/helpers/closeLivechat'; import { Services } from '../../lib/services'; import { ILivechatDepartment } from '../../definitions/ILivechatDepartment'; +import HeaderCallButton from './components/HeaderCallButton'; -interface IRightButtonsProps { +interface IRightButtonsProps extends Pick { userId?: string; threadsEnabled: boolean; - rid?: string; - t: string; tmid?: string; teamId?: string; isMasterDetail: boolean; @@ -43,6 +41,7 @@ interface IRightButtonsProps { livechatRequestComment: boolean; showActionSheet: Function; departmentId?: string; + rid?: string; } interface IRigthButtonsState { @@ -338,7 +337,7 @@ class RightButtonsContainer extends Component - {isTeamRoom({ teamId, joined }) ? ( - - ) : null} + {rid ? : null} {threadsEnabled ? ( ; + return null; +} diff --git a/app/views/RoomView/index.tsx b/app/views/RoomView/index.tsx index 586c9ddef..c51d009d8 100644 --- a/app/views/RoomView/index.tsx +++ b/app/views/RoomView/index.tsx @@ -627,7 +627,7 @@ class RoomView extends React.Component { joined={joined} status={room.status} omnichannelPermissions={omnichannelPermissions} - t={this.t || t} + t={(this.t || t) as SubscriptionType} encrypted={encrypted} navigation={navigation} toggleFollowThread={this.toggleFollowThread} @@ -1229,14 +1229,15 @@ class RoomView extends React.Component { }); }; - handleCallJitsi = () => { + // OLD METHOD - support versions before 5.0.0 + handleEnterCall = () => { const { room } = this.state; if ('id' in room) { const { jitsiTimeout } = room; if (jitsiTimeout && jitsiTimeout < new Date()) { showErrorAlert(I18n.t('Call_already_ended')); } else { - callJitsi(room); + callJitsi({ room }); } } }; @@ -1382,7 +1383,7 @@ class RoomView extends React.Component { autoTranslateLanguage={'id' in room ? room.autoTranslateLanguage : undefined} navToRoomInfo={this.navToRoomInfo} getCustomEmoji={this.getCustomEmoji} - callJitsi={this.handleCallJitsi} + handleEnterCall={this.handleEnterCall} blockAction={this.blockAction} threadBadgeColor={this.getBadgeColor(item?.id)} toggleFollowThread={this.toggleFollowThread}