* Handle user leaving channel with active call
* Remove debug log
(cherry picked from commit 84858e4450)
Co-authored-by: Claudio Costa <cstcld91@gmail.com>
This commit is contained in:
parent
810f4b3d24
commit
35c064ff9a
8 changed files with 226 additions and 17 deletions
|
|
@ -11,7 +11,9 @@ import {fetchMissingDirectChannelsInfo, fetchMyChannel, fetchChannelStats, fetch
|
|||
import {fetchPostsForChannel} from '@actions/remote/post';
|
||||
import {fetchRolesIfNeeded} from '@actions/remote/role';
|
||||
import {fetchUsersByIds, updateUsersNoLongerVisible} from '@actions/remote/user';
|
||||
import {loadCallForChannel} from '@calls/actions/calls';
|
||||
import {loadCallForChannel, leaveCall} from '@calls/actions/calls';
|
||||
import {userLeftChannelErr, userRemovedFromChannelErr} from '@calls/errors';
|
||||
import {getCurrentCall} from '@calls/state';
|
||||
import {Events, General} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {deleteChannelMembership, getChannelById, prepareMyChannelsForTeam, getCurrentChannel} from '@queries/servers/channel';
|
||||
|
|
@ -360,6 +362,9 @@ export async function handleUserRemovedFromChannelEvent(serverUrl: string, msg:
|
|||
const channelId = msg.data.channel_id || msg.broadcast.channel_id;
|
||||
|
||||
if (EphemeralStore.isLeavingChannel(channelId)) {
|
||||
if (getCurrentCall()?.channelId === channelId) {
|
||||
leaveCall(userLeftChannelErr);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -382,7 +387,12 @@ export async function handleUserRemovedFromChannelEvent(serverUrl: string, msg:
|
|||
if (currentChannelId && currentChannelId === channelId) {
|
||||
await handleKickFromChannel(serverUrl, currentChannelId);
|
||||
}
|
||||
|
||||
await removeCurrentUserFromChannel(serverUrl, channelId);
|
||||
|
||||
if (getCurrentCall()?.channelId === channelId) {
|
||||
leaveCall(userRemovedFromChannelErr);
|
||||
}
|
||||
} else {
|
||||
const {models: deleteMemberModels} = await deleteChannelMembership(operator, userId, channelId, true);
|
||||
if (deleteMemberModels) {
|
||||
|
|
|
|||
|
|
@ -4,12 +4,14 @@
|
|||
import assert from 'assert';
|
||||
|
||||
import {act, renderHook} from '@testing-library/react-hooks';
|
||||
import {createIntl} from 'react-intl';
|
||||
import InCallManager from 'react-native-incall-manager';
|
||||
|
||||
import * as CallsActions from '@calls/actions';
|
||||
import {getConnectionForTesting} from '@calls/actions/calls';
|
||||
import * as Permissions from '@calls/actions/permissions';
|
||||
import {needsRecordingWillBePostedAlert, needsRecordingErrorAlert} from '@calls/alerts';
|
||||
import {userLeftChannelErr, userRemovedFromChannelErr} from '@calls/errors';
|
||||
import * as State from '@calls/state';
|
||||
import {
|
||||
myselfLeftCall,
|
||||
|
|
@ -32,6 +34,7 @@ import {
|
|||
DefaultCallsConfig,
|
||||
DefaultCallsState,
|
||||
} from '@calls/types/calls';
|
||||
import {errorAlert} from '@calls/utils';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
|
||||
|
|
@ -71,8 +74,8 @@ const mockClient = {
|
|||
};
|
||||
|
||||
jest.mock('@calls/connection/connection', () => ({
|
||||
newConnection: jest.fn(() => Promise.resolve({
|
||||
disconnect: jest.fn(),
|
||||
newConnection: jest.fn((serverURL, channelId, onClose) => Promise.resolve({
|
||||
disconnect: jest.fn((err?: Error) => onClose(err)),
|
||||
mute: jest.fn(),
|
||||
unmute: jest.fn(),
|
||||
waitForPeerConnection: jest.fn(() => Promise.resolve()),
|
||||
|
|
@ -89,7 +92,17 @@ jest.mock('@queries/servers/thread', () => ({
|
|||
})),
|
||||
}));
|
||||
|
||||
jest.mock('@calls/alerts');
|
||||
jest.mock('@calls/alerts', () => {
|
||||
const alerts = jest.requireActual('../alerts');
|
||||
return {
|
||||
needsRecordingErrorAlert: jest.fn(),
|
||||
needsRecordingWillBePostedAlert: jest.fn(),
|
||||
showErrorAlertOnClose: alerts.showErrorAlertOnClose,
|
||||
};
|
||||
});
|
||||
|
||||
jest.mock('@calls/utils');
|
||||
|
||||
jest.mock('react-native-navigation', () => ({
|
||||
Navigation: {
|
||||
pop: jest.fn(() => Promise.resolve({
|
||||
|
|
@ -174,7 +187,10 @@ describe('Actions.Calls', () => {
|
|||
|
||||
let response: { data?: string };
|
||||
await act(async () => {
|
||||
response = await CallsActions.joinCall('server1', 'channel-id', 'myUserId', true);
|
||||
response = await CallsActions.joinCall('server1', 'channel-id', 'myUserId', true, createIntl({
|
||||
locale: 'en',
|
||||
messages: {},
|
||||
}));
|
||||
|
||||
// manually call newCurrentConnection because newConnection is mocked
|
||||
newCurrentCall('server1', 'channel-id', 'myUserId');
|
||||
|
|
@ -201,7 +217,10 @@ describe('Actions.Calls', () => {
|
|||
|
||||
let response: { data?: string };
|
||||
await act(async () => {
|
||||
response = await CallsActions.joinCall('server1', 'channel-id', 'myUserId', true);
|
||||
response = await CallsActions.joinCall('server1', 'channel-id', 'myUserId', true, createIntl({
|
||||
locale: 'en',
|
||||
messages: {},
|
||||
}));
|
||||
|
||||
// manually call newCurrentConnection because newConnection is mocked
|
||||
newCurrentCall('server1', 'channel-id', 'myUserId');
|
||||
|
|
@ -235,7 +254,10 @@ describe('Actions.Calls', () => {
|
|||
|
||||
let response: { data?: string };
|
||||
await act(async () => {
|
||||
response = await CallsActions.joinCall('server1', 'channel-id', 'myUserId', true);
|
||||
response = await CallsActions.joinCall('server1', 'channel-id', 'myUserId', true, createIntl({
|
||||
locale: 'en',
|
||||
messages: {},
|
||||
}));
|
||||
|
||||
// manually call newCurrentConnection because newConnection is mocked
|
||||
newCurrentCall('server1', 'channel-id', 'myUserId');
|
||||
|
|
@ -265,7 +287,10 @@ describe('Actions.Calls', () => {
|
|||
|
||||
let response: { data?: string };
|
||||
await act(async () => {
|
||||
response = await CallsActions.joinCall('server1', 'channel-id', 'mysUserId', true);
|
||||
response = await CallsActions.joinCall('server1', 'channel-id', 'mysUserId', true, createIntl({
|
||||
locale: 'en',
|
||||
messages: {},
|
||||
}));
|
||||
|
||||
// manually call newCurrentConnection because newConnection is mocked
|
||||
newCurrentCall('server1', 'channel-id', 'myUserId');
|
||||
|
|
@ -396,4 +421,125 @@ describe('Actions.Calls', () => {
|
|||
|
||||
expect(mockClient.dismissCall).toBeCalledWith('channel-id');
|
||||
});
|
||||
|
||||
it('userLeftChannelErr', async () => {
|
||||
// setup
|
||||
const {result} = renderHook(() => {
|
||||
return [useCallsState('server1'), useCurrentCall()];
|
||||
});
|
||||
addFakeCall('server1', 'channel-id');
|
||||
|
||||
let response: { data?: string };
|
||||
|
||||
const intl = createIntl({
|
||||
locale: 'en',
|
||||
messages: {},
|
||||
});
|
||||
intl.formatMessage = jest.fn();
|
||||
|
||||
await act(async () => {
|
||||
response = await CallsActions.joinCall('server1', 'channel-id', 'myUserId', true, intl);
|
||||
|
||||
// manually call newCurrentConnection because newConnection is mocked
|
||||
newCurrentCall('server1', 'channel-id', 'myUserId');
|
||||
});
|
||||
|
||||
assert.equal(response!.data, 'channel-id');
|
||||
assert.equal((result.current[1] as CurrentCall).channelId, 'channel-id');
|
||||
expect(newConnection).toBeCalled();
|
||||
expect(newConnection.mock.calls[0][1]).toBe('channel-id');
|
||||
expect(updateThreadFollowing).toBeCalled();
|
||||
|
||||
await act(async () => {
|
||||
CallsActions.leaveCall(userLeftChannelErr);
|
||||
});
|
||||
|
||||
expect(intl.formatMessage).toBeCalledWith({
|
||||
id: 'mobile.calls_user_left_channel_error_title',
|
||||
defaultMessage: 'You left the channel',
|
||||
});
|
||||
|
||||
expect(intl.formatMessage).toBeCalledWith({
|
||||
id: 'mobile.calls_user_left_channel_error_message',
|
||||
defaultMessage: 'You have left the channel, and have been disconnected from the call.',
|
||||
});
|
||||
});
|
||||
|
||||
it('userRemovedFromChannelErr', async () => {
|
||||
// setup
|
||||
const {result} = renderHook(() => {
|
||||
return [useCallsState('server1'), useCurrentCall()];
|
||||
});
|
||||
addFakeCall('server1', 'channel-id');
|
||||
|
||||
let response: { data?: string };
|
||||
|
||||
const intl = createIntl({
|
||||
locale: 'en',
|
||||
messages: {},
|
||||
});
|
||||
intl.formatMessage = jest.fn();
|
||||
|
||||
await act(async () => {
|
||||
response = await CallsActions.joinCall('server1', 'channel-id', 'myUserId', true, intl);
|
||||
|
||||
// manually call newCurrentConnection because newConnection is mocked
|
||||
newCurrentCall('server1', 'channel-id', 'myUserId');
|
||||
});
|
||||
|
||||
assert.equal(response!.data, 'channel-id');
|
||||
assert.equal((result.current[1] as CurrentCall).channelId, 'channel-id');
|
||||
expect(newConnection).toBeCalled();
|
||||
expect(newConnection.mock.calls[0][1]).toBe('channel-id');
|
||||
expect(updateThreadFollowing).toBeCalled();
|
||||
|
||||
await act(async () => {
|
||||
CallsActions.leaveCall(userRemovedFromChannelErr);
|
||||
});
|
||||
|
||||
expect(intl.formatMessage).toBeCalledWith({
|
||||
id: 'mobile.calls_user_removed_from_channel_error_title',
|
||||
defaultMessage: 'You were removed from channel',
|
||||
});
|
||||
|
||||
expect(intl.formatMessage).toBeCalledWith({
|
||||
id: 'mobile.calls_user_removed_from_channel_error_message',
|
||||
defaultMessage: 'You have been removed from the channel, and have been disconnected from the call.',
|
||||
});
|
||||
});
|
||||
|
||||
it('generic error on close', async () => {
|
||||
// setup
|
||||
const {result} = renderHook(() => {
|
||||
return [useCallsState('server1'), useCurrentCall()];
|
||||
});
|
||||
addFakeCall('server1', 'channel-id');
|
||||
|
||||
let response: { data?: string };
|
||||
|
||||
const intl = createIntl({
|
||||
locale: 'en',
|
||||
messages: {},
|
||||
});
|
||||
intl.formatMessage = jest.fn();
|
||||
|
||||
await act(async () => {
|
||||
response = await CallsActions.joinCall('server1', 'channel-id', 'myUserId', true, intl);
|
||||
|
||||
// manually call newCurrentConnection because newConnection is mocked
|
||||
newCurrentCall('server1', 'channel-id', 'myUserId');
|
||||
});
|
||||
|
||||
assert.equal(response!.data, 'channel-id');
|
||||
assert.equal((result.current[1] as CurrentCall).channelId, 'channel-id');
|
||||
expect(newConnection).toBeCalled();
|
||||
expect(newConnection.mock.calls[0][1]).toBe('channel-id');
|
||||
expect(updateThreadFollowing).toBeCalled();
|
||||
|
||||
await act(async () => {
|
||||
CallsActions.leaveCall(new Error('generic error'));
|
||||
});
|
||||
|
||||
expect(errorAlert).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -7,7 +7,12 @@ import InCallManager from 'react-native-incall-manager';
|
|||
import {forceLogoutIfNecessary} from '@actions/remote/session';
|
||||
import {updateThreadFollowing} from '@actions/remote/thread';
|
||||
import {fetchUsersByIds} from '@actions/remote/user';
|
||||
import {leaveAndJoinWithAlert, needsRecordingErrorAlert, needsRecordingWillBePostedAlert} from '@calls/alerts';
|
||||
import {
|
||||
leaveAndJoinWithAlert,
|
||||
needsRecordingErrorAlert,
|
||||
needsRecordingWillBePostedAlert,
|
||||
showErrorAlertOnClose,
|
||||
} from '@calls/alerts';
|
||||
import {
|
||||
getCallsConfig,
|
||||
getCallsState,
|
||||
|
|
@ -230,6 +235,7 @@ export const joinCall = async (
|
|||
channelId: string,
|
||||
userId: string,
|
||||
hasMicPermission: boolean,
|
||||
intl: IntlShape,
|
||||
title?: string,
|
||||
rootId?: string,
|
||||
): Promise<{ error?: unknown; data?: string }> => {
|
||||
|
|
@ -248,8 +254,12 @@ export const joinCall = async (
|
|||
newCurrentCall(serverUrl, channelId, userId);
|
||||
|
||||
try {
|
||||
connection = await newConnection(serverUrl, channelId, () => {
|
||||
connection = await newConnection(serverUrl, channelId, (err?: Error) => {
|
||||
myselfLeftCall();
|
||||
if (err) {
|
||||
logDebug('calls: error on close', getFullErrorMessage(err));
|
||||
showErrorAlertOnClose(err, intl);
|
||||
}
|
||||
}, setScreenShareURL, hasMicPermission, title, rootId);
|
||||
} catch (error) {
|
||||
await forceLogoutIfNecessary(serverUrl, error);
|
||||
|
|
@ -285,9 +295,9 @@ export const joinCall = async (
|
|||
}
|
||||
};
|
||||
|
||||
export const leaveCall = () => {
|
||||
export const leaveCall = (err?: Error) => {
|
||||
if (connection) {
|
||||
connection.disconnect();
|
||||
connection.disconnect(err);
|
||||
connection = null;
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import {Alert} from 'react-native';
|
|||
import {hasMicrophonePermission, joinCall, leaveCall, unmuteMyself} from '@calls/actions';
|
||||
import {dismissIncomingCall} from '@calls/actions/calls';
|
||||
import {hasBluetoothPermission} from '@calls/actions/permissions';
|
||||
import {userLeftChannelErr, userRemovedFromChannelErr} from '@calls/errors';
|
||||
import {
|
||||
getCallsConfig,
|
||||
getCallsState,
|
||||
|
|
@ -19,6 +20,7 @@ import DatabaseManager from '@database/manager';
|
|||
import {getChannelById} from '@queries/servers/channel';
|
||||
import {getCurrentUser} from '@queries/servers/user';
|
||||
import {isDMorGM} from '@utils/channel';
|
||||
import {getFullErrorMessage} from '@utils/errors';
|
||||
import {logError} from '@utils/log';
|
||||
import {isSystemAdmin} from '@utils/user';
|
||||
|
||||
|
|
@ -208,7 +210,7 @@ const doJoinCall = async (
|
|||
removeIncomingCall(serverUrl, callId, channelId);
|
||||
}
|
||||
|
||||
const res = await joinCall(serverUrl, channelId, user.id, hasPermission, title, rootId);
|
||||
const res = await joinCall(serverUrl, channelId, user.id, hasPermission, intl, title, rootId);
|
||||
if (res.error) {
|
||||
const seeLogs = formatMessage({id: 'mobile.calls_see_logs', defaultMessage: 'See server logs'});
|
||||
errorAlert(res.error?.toString() || seeLogs, intl);
|
||||
|
|
@ -384,3 +386,35 @@ export const recordingErrorAlert = (intl: IntlShape) => {
|
|||
}],
|
||||
);
|
||||
};
|
||||
|
||||
export const showErrorAlertOnClose = (err: Error, intl: IntlShape) => {
|
||||
switch (err) {
|
||||
case userLeftChannelErr:
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'mobile.calls_user_left_channel_error_title',
|
||||
defaultMessage: 'You left the channel',
|
||||
}),
|
||||
intl.formatMessage({
|
||||
id: 'mobile.calls_user_left_channel_error_message',
|
||||
defaultMessage: 'You have left the channel, and have been disconnected from the call.',
|
||||
}),
|
||||
);
|
||||
break;
|
||||
case userRemovedFromChannelErr:
|
||||
Alert.alert(
|
||||
intl.formatMessage({
|
||||
id: 'mobile.calls_user_removed_from_channel_error_title',
|
||||
defaultMessage: 'You were removed from channel',
|
||||
}),
|
||||
intl.formatMessage({
|
||||
id: 'mobile.calls_user_removed_from_channel_error_message',
|
||||
defaultMessage: 'You have been removed from the channel, and have been disconnected from the call.',
|
||||
}),
|
||||
);
|
||||
break;
|
||||
default:
|
||||
// Fallback with generic error
|
||||
errorAlert(getFullErrorMessage(err, intl), intl);
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -39,7 +39,7 @@ if (Platform.OS === 'android') {
|
|||
export async function newConnection(
|
||||
serverUrl: string,
|
||||
channelID: string,
|
||||
closeCb: () => void,
|
||||
closeCb: (err?: Error) => void,
|
||||
setScreenShareURL: (url: string) => void,
|
||||
hasMicPermission: boolean,
|
||||
title?: string,
|
||||
|
|
@ -93,7 +93,7 @@ export async function newConnection(
|
|||
initializeVoiceTrack();
|
||||
}
|
||||
|
||||
const disconnect = () => {
|
||||
const disconnect = (err?: Error) => {
|
||||
if (isClosed) {
|
||||
return;
|
||||
}
|
||||
|
|
@ -126,7 +126,7 @@ export async function newConnection(
|
|||
}
|
||||
|
||||
if (closeCb) {
|
||||
closeCb();
|
||||
closeCb(err);
|
||||
}
|
||||
};
|
||||
|
||||
|
|
|
|||
5
app/products/calls/errors.ts
Normal file
5
app/products/calls/errors.ts
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export const userRemovedFromChannelErr = new Error('user was removed from channel');
|
||||
export const userLeftChannelErr = new Error('user has left channel');
|
||||
|
|
@ -124,7 +124,7 @@ export type CallSession = {
|
|||
export type ChannelsWithCalls = Dictionary<boolean>;
|
||||
|
||||
export type CallsConnection = {
|
||||
disconnect: () => void;
|
||||
disconnect: (err?: Error) => void;
|
||||
mute: () => void;
|
||||
unmute: () => void;
|
||||
waitForPeerConnection: () => Promise<void>;
|
||||
|
|
|
|||
|
|
@ -517,6 +517,10 @@
|
|||
"mobile.calls_tablet": "Tablet",
|
||||
"mobile.calls_thread": "Thread",
|
||||
"mobile.calls_unmute": "Unmute",
|
||||
"mobile.calls_user_left_channel_error_message": "You have left the channel, and have been disconnected from the call.",
|
||||
"mobile.calls_user_left_channel_error_title": "You left the channel",
|
||||
"mobile.calls_user_removed_from_channel_error_message": "You have been removed from the channel, and have been disconnected from the call.",
|
||||
"mobile.calls_user_removed_from_channel_error_title": "You were removed from channel",
|
||||
"mobile.calls_viewing_screen": "You are viewing {name}'s screen",
|
||||
"mobile.calls_you": "(you)",
|
||||
"mobile.calls_you_2": "You",
|
||||
|
|
|
|||
Loading…
Reference in a new issue