diff --git a/app/products/calls/alerts.ts b/app/products/calls/alerts.ts
index 58b0a0938..308118a29 100644
--- a/app/products/calls/alerts.ts
+++ b/app/products/calls/alerts.ts
@@ -102,7 +102,7 @@ export const leaveAndJoinWithAlert = async (
}
} catch (error) {
logError('failed to getServerDatabase in leaveAndJoinWithAlert', error);
- return;
+ return false;
}
if (leaveServerUrl && leaveChannelId) {
@@ -119,33 +119,38 @@ export const leaveAndJoinWithAlert = async (
}, {leaveChannelName, joinChannelName});
}
- Alert.alert(
- formatMessage({
- id: 'mobile.leave_and_join_title',
- defaultMessage: 'Are you sure you want to switch to a different call?',
- }),
- joinMessage,
- [
- {
- text: formatMessage({
- id: 'mobile.post.cancel',
- defaultMessage: 'Cancel',
- }),
- style: 'destructive',
- },
- {
- text: formatMessage({
- id: 'mobile.leave_and_join_confirmation',
- defaultMessage: 'Leave & Join',
- }),
- onPress: () => doJoinCall(joinServerUrl, joinChannelId, joinChannelIsDMorGM, newCall, intl, title, rootId),
- style: 'cancel',
- },
- ],
- );
- } else {
- doJoinCall(joinServerUrl, joinChannelId, joinChannelIsDMorGM, newCall, intl, title, rootId);
+ const asyncAlert = async () => new Promise((resolve) => {
+ Alert.alert(
+ formatMessage({
+ id: 'mobile.leave_and_join_title',
+ defaultMessage: 'Are you sure you want to switch to a different call?',
+ }),
+ joinMessage,
+ [
+ {
+ text: formatMessage({
+ id: 'mobile.post.cancel',
+ defaultMessage: 'Cancel',
+ }),
+ onPress: async () => resolve(false),
+ style: 'destructive',
+ },
+ {
+ text: formatMessage({
+ id: 'mobile.leave_and_join_confirmation',
+ defaultMessage: 'Leave & Join',
+ }),
+ onPress: async () => resolve(await doJoinCall(joinServerUrl, joinChannelId, joinChannelIsDMorGM, newCall, intl, title, rootId)),
+ isPreferred: true,
+ },
+ ],
+ );
+ });
+
+ return asyncAlert();
}
+
+ return doJoinCall(joinServerUrl, joinChannelId, joinChannelIsDMorGM, newCall, intl, title, rootId);
};
const doJoinCall = async (
@@ -166,7 +171,7 @@ const doJoinCall = async (
user = await getCurrentUser(database);
if (!user) {
// This shouldn't happen, so don't bother localizing and displaying an alert.
- return;
+ return false;
}
if (newCall) {
@@ -189,12 +194,12 @@ const doJoinCall = async (
// continue through and start the call
} else {
contactAdminAlert(intl);
- return;
+ return false;
}
}
} catch (error) {
logError('failed to getServerDatabaseAndOperator in doJoinCall', error);
- return;
+ return false;
}
recordingAlertLock = false;
@@ -215,12 +220,14 @@ const doJoinCall = async (
if (res.error) {
const seeLogs = formatMessage({id: 'mobile.calls_see_logs', defaultMessage: 'See server logs'});
errorAlert(res.error?.toString() || seeLogs, intl);
- return;
+ return false;
}
if (joinChannelIsDMorGM) {
unmuteMyself();
}
+
+ return true;
};
const contactAdminAlert = ({formatMessage}: IntlShape) => {
diff --git a/app/products/calls/components/calls_custom_message/calls_custom_message.tsx b/app/products/calls/components/calls_custom_message/calls_custom_message.tsx
index 63df36327..e233a18cd 100644
--- a/app/products/calls/components/calls_custom_message/calls_custom_message.tsx
+++ b/app/products/calls/components/calls_custom_message/calls_custom_message.tsx
@@ -8,10 +8,12 @@ import {Text, TouchableOpacity, View} from 'react-native';
import {leaveCall} from '@calls/actions';
import {leaveAndJoinWithAlert, showLimitRestrictedAlert} from '@calls/alerts';
+import {setJoiningChannelId} from '@calls/state';
import CompassIcon from '@components/compass_icon';
import FormattedRelativeTime from '@components/formatted_relative_time';
import FormattedText from '@components/formatted_text';
import FormattedTime from '@components/formatted_time';
+import Loading from '@components/loading';
import {useServerUrl} from '@context/server';
import {useTheme} from '@context/theme';
import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
@@ -28,6 +30,7 @@ type Props = {
isMilitaryTime: boolean;
limitRestrictedInfo?: LimitRestrictedInfo;
ccChannelId?: string;
+ joiningChannelId: string | null;
}
const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
@@ -121,23 +124,34 @@ const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => {
};
});
-export const CallsCustomMessage = ({post, currentUser, isMilitaryTime, ccChannelId, limitRestrictedInfo}: Props) => {
+export const CallsCustomMessage = ({
+ post,
+ currentUser,
+ isMilitaryTime,
+ ccChannelId,
+ limitRestrictedInfo,
+ joiningChannelId,
+}: Props) => {
const intl = useIntl();
const theme = useTheme();
const style = getStyleSheet(theme);
const serverUrl = useServerUrl();
const timezone = getUserTimezone(currentUser);
+ const joiningThisCall = Boolean(joiningChannelId === post.channelId);
const alreadyInTheCall = Boolean(ccChannelId && ccChannelId === post.channelId);
const isLimitRestricted = Boolean(limitRestrictedInfo?.limitRestricted);
+ const joiningMsg = intl.formatMessage({id: 'mobile.calls_joining', defaultMessage: 'Joining...'});
- const joinHandler = useCallback(() => {
+ const joinHandler = useCallback(async () => {
if (isLimitRestricted) {
showLimitRestrictedAlert(limitRestrictedInfo!, intl);
return;
}
- leaveAndJoinWithAlert(intl, serverUrl, post.channelId);
+ setJoiningChannelId(post.channelId);
+ await leaveAndJoinWithAlert(intl, serverUrl, post.channelId);
+ setJoiningChannelId(null);
}, [limitRestrictedInfo, intl, serverUrl, post.channelId]);
const leaveHandler = useCallback(() => {
@@ -227,6 +241,16 @@ export const CallsCustomMessage = ({post, currentUser, isMilitaryTime, ccChannel
);
+ const joiningButton = (
+
+ );
+
return (
<>
{title}
@@ -248,7 +272,7 @@ export const CallsCustomMessage = ({post, currentUser, isMilitaryTime, ccChannel
style={style.timeText}
/>
- {button}
+ {joiningThisCall ? joiningButton : button}
>
);
diff --git a/app/products/calls/components/calls_custom_message/index.ts b/app/products/calls/components/calls_custom_message/index.ts
index 6368f17b6..c50763bf7 100644
--- a/app/products/calls/components/calls_custom_message/index.ts
+++ b/app/products/calls/components/calls_custom_message/index.ts
@@ -7,7 +7,7 @@ import {distinctUntilChanged, switchMap} from 'rxjs/operators';
import {CallsCustomMessage} from '@calls/components/calls_custom_message/calls_custom_message';
import {observeIsCallLimitRestricted} from '@calls/observers';
-import {observeCurrentCall} from '@calls/state';
+import {observeCurrentCall, observeGlobalCallsState} from '@calls/state';
import {Preferences} from '@constants';
import {getDisplayNamePreferenceAsBool} from '@helpers/api/preference';
import {queryDisplayNamePreferences} from '@queries/servers/preference';
@@ -41,12 +41,17 @@ const enhanced = withObservables(['post'], ({serverUrl, post, database}: OwnProp
switchMap((call) => of$(call?.channelId)),
distinctUntilChanged(),
);
+ const joiningChannelId = observeGlobalCallsState().pipe(
+ switchMap((state) => of$(state?.joiningChannelId)),
+ distinctUntilChanged(),
+ );
return {
currentUser,
isMilitaryTime,
limitRestrictedInfo: observeIsCallLimitRestricted(database, serverUrl, post.channelId),
ccChannelId,
+ joiningChannelId,
};
});
diff --git a/app/products/calls/components/channel_info_start/channel_info_start_button.tsx b/app/products/calls/components/channel_info_start/channel_info_start_button.tsx
index 26cd872c9..a79d4146a 100644
--- a/app/products/calls/components/channel_info_start/channel_info_start_button.tsx
+++ b/app/products/calls/components/channel_info_start/channel_info_start_button.tsx
@@ -1,14 +1,18 @@
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
-import React, {useCallback} from 'react';
+import React, {useCallback, useState} from 'react';
import {useIntl} from 'react-intl';
import {leaveCall} from '@calls/actions';
import {leaveAndJoinWithAlert, showLimitRestrictedAlert} from '@calls/alerts';
import {useTryCallsFunction} from '@calls/hooks';
-import OptionBox from '@components/option_box';
+import Loading from '@components/loading';
+import OptionBox, {OPTIONS_HEIGHT} from '@components/option_box';
+import {useTheme} from '@context/theme';
import {preventDoubleTap} from '@utils/tap';
+import {changeOpacity, makeStyleSheetFromTheme} from '@utils/theme';
+import {typography} from '@utils/typography';
import type {LimitRestrictedInfo} from '@calls/observers';
@@ -21,6 +25,27 @@ export interface Props {
limitRestrictedInfo: LimitRestrictedInfo;
}
+const getStyleSheet = makeStyleSheetFromTheme((theme: Theme) => ({
+ container: {
+ alignItems: 'center',
+ backgroundColor: changeOpacity(theme.buttonBg, 0.08),
+ borderRadius: 4,
+ flex: 1,
+ maxHeight: OPTIONS_HEIGHT,
+ justifyContent: 'center',
+ minWidth: 60,
+ paddingTop: 12,
+ paddingBottom: 10,
+ },
+ text: {
+ color: theme.buttonBg,
+ paddingTop: 3,
+ width: '100%',
+ textAlign: 'center',
+ ...typography('Body', 50, 'SemiBold'),
+ },
+}));
+
const ChannelInfoStartButton = ({
serverUrl,
channelId,
@@ -30,18 +55,30 @@ const ChannelInfoStartButton = ({
limitRestrictedInfo,
}: Props) => {
const intl = useIntl();
+ const theme = useTheme();
+ const styles = getStyleSheet(theme);
const isLimitRestricted = limitRestrictedInfo.limitRestricted;
+ const [connecting, setConnecting] = useState(false);
+ const [joiningMsg, setJoiningMsg] = useState('');
- const toggleJoinLeave = useCallback(() => {
+ const starting = intl.formatMessage({id: 'mobile.calls_starting', defaultMessage: 'Starting...'});
+ const joining = intl.formatMessage({id: 'mobile.calls_joining', defaultMessage: 'Joining...'});
+
+ const toggleJoinLeave = useCallback(async () => {
if (alreadyInCall) {
leaveCall();
+ dismissChannelInfo();
} else if (isLimitRestricted) {
showLimitRestrictedAlert(limitRestrictedInfo, intl);
+ dismissChannelInfo();
} else {
- leaveAndJoinWithAlert(intl, serverUrl, channelId);
- }
+ setJoiningMsg(isACallInCurrentChannel ? joining : starting);
+ setConnecting(true);
- dismissChannelInfo();
+ await leaveAndJoinWithAlert(intl, serverUrl, channelId);
+ setConnecting(false);
+ dismissChannelInfo();
+ }
}, [isLimitRestricted, alreadyInCall, dismissChannelInfo, intl, serverUrl, channelId, isACallInCurrentChannel]);
const [tryJoin, msgPostfix] = useTryCallsFunction(toggleJoinLeave);
@@ -49,14 +86,28 @@ const ChannelInfoStartButton = ({
const joinText = intl.formatMessage({id: 'mobile.calls_join_call', defaultMessage: 'Join call'});
const startText = intl.formatMessage({id: 'mobile.calls_start_call', defaultMessage: 'Start call'});
const leaveText = intl.formatMessage({id: 'mobile.calls_leave_call', defaultMessage: 'Leave call'});
+ const text = isACallInCurrentChannel ? joinText + msgPostfix : startText + msgPostfix;
+ const icon = isACallInCurrentChannel ? 'phone-in-talk' : 'phone';
+
+ if (connecting) {
+ return (
+
+ );
+ }
return (
{
diff --git a/app/products/calls/state/actions.test.ts b/app/products/calls/state/actions.test.ts
index e0c96822a..bd89327c3 100644
--- a/app/products/calls/state/actions.test.ts
+++ b/app/products/calls/state/actions.test.ts
@@ -19,6 +19,7 @@ import {
setChannelsWithCalls,
setCurrentCall,
setHost,
+ setJoiningChannelId,
setMicPermissionsErrorDismissed,
setMicPermissionsGranted,
setRecordingState,
@@ -919,6 +920,7 @@ describe('useCallsState', () => {
};
const expectedGlobalState: GlobalCallsState = {
micPermissionsGranted: true,
+ joiningChannelId: null,
};
// setup
@@ -955,11 +957,35 @@ describe('useCallsState', () => {
act(() => {
myselfLeftCall();
userLeftCall('server1', 'channel-1', 'mySessionId');
+
+ // reset state to default
+ setMicPermissionsGranted(false);
});
assert.deepEqual(result.current[0], initialCallsState);
assert.deepEqual(result.current[1], null);
});
+ it('joining call', () => {
+ const initialGlobalState = DefaultGlobalCallsState;
+ const expectedGlobalState: GlobalCallsState = {
+ ...DefaultGlobalCallsState,
+ joiningChannelId: 'channel-1',
+ };
+
+ // setup
+ const {result} = renderHook(() => {
+ return [useGlobalCallsState()];
+ });
+
+ // start joining call
+ act(() => setJoiningChannelId('channel-1'));
+ assert.deepEqual(result.current[0], expectedGlobalState);
+
+ // end joining call
+ act(() => setJoiningChannelId(null));
+ assert.deepEqual(result.current[0], initialGlobalState);
+ });
+
it('CallQuality', async () => {
const initialCallsState: CallsState = {
...DefaultCallsState,
diff --git a/app/products/calls/state/actions.ts b/app/products/calls/state/actions.ts
index 5ce391ec4..43064de98 100644
--- a/app/products/calls/state/actions.ts
+++ b/app/products/calls/state/actions.ts
@@ -564,6 +564,14 @@ export const setSpeakerPhone = (speakerphoneOn: boolean) => {
}
};
+export const setJoiningChannelId = (joiningChannelId: string | null) => {
+ const globalCallsState = getGlobalCallsState();
+ setGlobalCallsState({
+ ...globalCallsState,
+ joiningChannelId,
+ });
+};
+
export const setAudioDeviceInfo = (info: AudioDeviceInfo) => {
const call = getCurrentCall();
if (call) {
diff --git a/app/products/calls/types/calls.ts b/app/products/calls/types/calls.ts
index 2846ae625..5f455b9d4 100644
--- a/app/products/calls/types/calls.ts
+++ b/app/products/calls/types/calls.ts
@@ -11,10 +11,12 @@ import type UserModel from '@typings/database/models/servers/user';
export type GlobalCallsState = {
micPermissionsGranted: boolean;
+ joiningChannelId: string | null;
}
export const DefaultGlobalCallsState: GlobalCallsState = {
micPermissionsGranted: false,
+ joiningChannelId: null,
};
export type CallsState = {
diff --git a/assets/base/i18n/en.json b/assets/base/i18n/en.json
index b9a38edad..34a4b89b8 100644
--- a/assets/base/i18n/en.json
+++ b/assets/base/i18n/en.json
@@ -475,6 +475,7 @@
"mobile.calls_incoming_gm": "{name} is inviting you to a call with {num, plural, one {# other} other {# others}}",
"mobile.calls_join": "Join",
"mobile.calls_join_call": "Join call",
+ "mobile.calls_joining": "Joining...",
"mobile.calls_lasted": "Lasted {duration}",
"mobile.calls_leave": "Leave",
"mobile.calls_leave_call": "Leave call",
@@ -529,6 +530,7 @@
"mobile.calls_start_call": "Start Call",
"mobile.calls_start_call_exists": "A call is already ongoing in the channel.",
"mobile.calls_started_call": "Call started",
+ "mobile.calls_starting": "Starting...",
"mobile.calls_stop_recording": "Stop Recording",
"mobile.calls_stop_screenshare": "Stop screen share",
"mobile.calls_tablet": "Tablet",