MM-43737 - Calls: Follow call thread on user joining a call (#7293)
* follow threads on call join * treat custom_calls posts as the start of threads * make thread follow logic robust to race conditions; tests * only follow thread when CRT is enabled * Revert "only follow thread when CRT is enabled" This reverts commit 4e52a2619b35db3d2104f0b6ba5ad8be23f7b224.
This commit is contained in:
parent
17dfb96c93
commit
e6254885ee
5 changed files with 91 additions and 7 deletions
|
|
@ -32,6 +32,7 @@ import {
|
|||
DefaultCallsConfig,
|
||||
DefaultCallsState,
|
||||
} from '@calls/types/calls';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import NetworkManager from '@managers/network_manager';
|
||||
|
||||
const mockClient = {
|
||||
|
|
@ -77,6 +78,16 @@ jest.mock('@calls/connection/connection', () => ({
|
|||
})),
|
||||
}));
|
||||
|
||||
jest.mock('@actions/remote/thread', () => ({
|
||||
updateThreadFollowing: jest.fn(() => Promise.resolve({})),
|
||||
}));
|
||||
|
||||
jest.mock('@queries/servers/thread', () => ({
|
||||
getThreadById: jest.fn(() => Promise.resolve({
|
||||
isFollowing: false,
|
||||
})),
|
||||
}));
|
||||
|
||||
jest.mock('@calls/alerts');
|
||||
|
||||
const addFakeCall = (serverUrl: string, channelId: string) => {
|
||||
|
|
@ -104,6 +115,8 @@ const addFakeCall = (serverUrl: string, channelId: string) => {
|
|||
|
||||
describe('Actions.Calls', () => {
|
||||
const {newConnection} = require('@calls/connection/connection');
|
||||
const {updateThreadFollowing} = require('@actions/remote/thread');
|
||||
|
||||
InCallManager.setSpeakerphoneOn = jest.fn();
|
||||
InCallManager.setForceSpeakerphoneOn = jest.fn();
|
||||
// eslint-disable-next-line
|
||||
|
|
@ -111,7 +124,9 @@ describe('Actions.Calls', () => {
|
|||
NetworkManager.getClient = () => mockClient;
|
||||
jest.spyOn(Permissions, 'hasMicrophonePermission').mockReturnValue(Promise.resolve(true));
|
||||
|
||||
beforeAll(() => {
|
||||
beforeAll(async () => {
|
||||
await DatabaseManager.init(['server1']);
|
||||
|
||||
// create subjects
|
||||
const {result} = renderHook(() => {
|
||||
return [useCallsState('server1'), useChannelsWithCalls('server1'), useCurrentCall(), useCallsConfig('server1')];
|
||||
|
|
@ -125,6 +140,7 @@ describe('Actions.Calls', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
newConnection.mockClear();
|
||||
updateThreadFollowing.mockClear();
|
||||
mockClient.getCalls.mockClear();
|
||||
mockClient.getCallsConfig.mockClear();
|
||||
mockClient.getPluginsManifests.mockClear();
|
||||
|
|
@ -152,13 +168,13 @@ describe('Actions.Calls', () => {
|
|||
|
||||
// manually call newCurrentConnection because newConnection is mocked
|
||||
newCurrentCall('server1', 'channel-id', 'myUserId');
|
||||
userJoinedCall('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();
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import InCallManager from 'react-native-incall-manager';
|
|||
import {Navigation} from 'react-native-navigation';
|
||||
|
||||
import {forceLogoutIfNecessary} from '@actions/remote/session';
|
||||
import {updateThreadFollowing} from '@actions/remote/thread';
|
||||
import {fetchUsersByIds} from '@actions/remote/user';
|
||||
import {leaveAndJoinWithAlert, needsRecordingWillBePostedAlert, needsRecordingErrorAlert} from '@calls/alerts';
|
||||
import {
|
||||
|
|
@ -31,6 +32,7 @@ import NetworkManager from '@managers/network_manager';
|
|||
import {getChannelById} from '@queries/servers/channel';
|
||||
import {queryDisplayNamePreferences} from '@queries/servers/preference';
|
||||
import {getConfig, getLicense} from '@queries/servers/system';
|
||||
import {getThreadById} from '@queries/servers/thread';
|
||||
import {getCurrentUser, getUserById} from '@queries/servers/user';
|
||||
import {dismissAllModalsAndPopToScreen} from '@screens/navigation';
|
||||
import NavigationStore from '@store/navigation_store';
|
||||
|
|
@ -265,6 +267,25 @@ export const joinCall = async (
|
|||
|
||||
try {
|
||||
await connection.waitForPeerConnection();
|
||||
|
||||
// Follow the thread.
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return {data: channelId};
|
||||
}
|
||||
|
||||
// If this was a call started by ourselves, then we should have subscribed in the start_call ws handler
|
||||
// (unless we received the start_call ws before the post/thread ws).
|
||||
// If this was us joining an existing call, follow the thread here.
|
||||
const call = getCallsState(serverUrl).calls[channelId];
|
||||
if (call && call.threadId) {
|
||||
const thread = await getThreadById(database, call.threadId);
|
||||
if (thread && !thread.isFollowing) {
|
||||
const channel = await getChannelById(database, channelId);
|
||||
updateThreadFollowing(serverUrl, channel?.teamId || '', call.threadId, true, false);
|
||||
}
|
||||
}
|
||||
|
||||
return {data: channelId};
|
||||
} catch (e) {
|
||||
connection.disconnect();
|
||||
|
|
|
|||
|
|
@ -51,11 +51,22 @@ import {
|
|||
type GlobalCallsState,
|
||||
} from '@calls/types/calls';
|
||||
import {License} from '@constants';
|
||||
import DatabaseManager from '@database/manager';
|
||||
|
||||
import type {CallRecordingState} from '@mattermost/calls/lib/types';
|
||||
|
||||
jest.mock('@calls/alerts');
|
||||
|
||||
jest.mock('@actions/remote/thread', () => ({
|
||||
updateThreadFollowing: jest.fn(() => Promise.resolve({})),
|
||||
}));
|
||||
|
||||
jest.mock('@queries/servers/thread', () => ({
|
||||
getThreadById: jest.fn(() => Promise.resolve({
|
||||
isFollowing: false,
|
||||
})),
|
||||
}));
|
||||
|
||||
const call1: Call = {
|
||||
participants: {
|
||||
'user-1': {id: 'user-1', muted: false, raisedHand: 0},
|
||||
|
|
@ -94,6 +105,8 @@ const call3: Call = {
|
|||
};
|
||||
|
||||
describe('useCallsState', () => {
|
||||
const {updateThreadFollowing} = require('@actions/remote/thread');
|
||||
|
||||
beforeAll(() => {
|
||||
// create subjects
|
||||
const {result} = renderHook(() => {
|
||||
|
|
@ -107,6 +120,8 @@ describe('useCallsState', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
// reset to default state for each test
|
||||
updateThreadFollowing.mockClear();
|
||||
|
||||
act(() => {
|
||||
setCallsState('server1', DefaultCallsState);
|
||||
setChannelsWithCalls('server1', {});
|
||||
|
|
@ -372,8 +387,18 @@ describe('useCallsState', () => {
|
|||
assert.deepEqual(result.current[2], expectedCurrentCallState);
|
||||
});
|
||||
|
||||
it('callStarted', () => {
|
||||
it('callStarted', async () => {
|
||||
const initialCurrentCallState: CurrentCall = {
|
||||
...DefaultCurrentCall,
|
||||
connected: false,
|
||||
serverUrl: 'server1',
|
||||
myUserId: 'myUserId',
|
||||
...call1,
|
||||
};
|
||||
|
||||
// setup
|
||||
await DatabaseManager.init(['server1']);
|
||||
|
||||
const {result} = renderHook(() => {
|
||||
return [useCallsState('server1'), useChannelsWithCalls('server1'), useCurrentCall()];
|
||||
});
|
||||
|
|
@ -382,10 +407,14 @@ describe('useCallsState', () => {
|
|||
assert.deepEqual(result.current[2], null);
|
||||
|
||||
// test
|
||||
act(() => callStarted('server1', call1));
|
||||
await act(async () => {
|
||||
setCurrentCall(initialCurrentCallState);
|
||||
await callStarted('server1', call1);
|
||||
});
|
||||
assert.deepEqual((result.current[0] as CallsState).calls, {'channel-1': call1});
|
||||
assert.deepEqual(result.current[1], {'channel-1': true});
|
||||
assert.deepEqual(result.current[2], null);
|
||||
assert.deepEqual(result.current[2], initialCurrentCallState);
|
||||
expect(updateThreadFollowing).toBeCalled();
|
||||
});
|
||||
|
||||
it('callEnded', () => {
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {updateThreadFollowing} from '@actions/remote/thread';
|
||||
import {needsRecordingAlert} from '@calls/alerts';
|
||||
import {
|
||||
getCallsConfig,
|
||||
|
|
@ -24,6 +25,9 @@ import {
|
|||
type ReactionStreamEmoji,
|
||||
} from '@calls/types/calls';
|
||||
import {REACTION_LIMIT, REACTION_TIMEOUT} from '@constants/calls';
|
||||
import DatabaseManager from '@database/manager';
|
||||
import {getChannelById} from '@queries/servers/channel';
|
||||
import {getThreadById} from '@queries/servers/thread';
|
||||
|
||||
import type {CallRecordingState, UserReactionData} from '@mattermost/calls/lib/types';
|
||||
|
||||
|
|
@ -212,7 +216,7 @@ export const myselfLeftCall = () => {
|
|||
setCurrentCall(null);
|
||||
};
|
||||
|
||||
export const callStarted = (serverUrl: string, call: Call) => {
|
||||
export const callStarted = async (serverUrl: string, call: Call) => {
|
||||
const callsState = getCallsState(serverUrl);
|
||||
const nextCalls = {...callsState.calls};
|
||||
nextCalls[call.channelId] = call;
|
||||
|
|
@ -233,6 +237,19 @@ export const callStarted = (serverUrl: string, call: Call) => {
|
|||
...call,
|
||||
};
|
||||
setCurrentCall(nextCurrentCall);
|
||||
|
||||
// We started the call, and it succeeded, so follow the call thread.
|
||||
const database = DatabaseManager.serverDatabases[serverUrl]?.database;
|
||||
if (!database) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Make sure the post/thread has arrived from the server.
|
||||
const thread = await getThreadById(database, call.threadId);
|
||||
if (thread && !thread.isFollowing) {
|
||||
const channel = await getChannelById(database, call.channelId);
|
||||
updateThreadFollowing(serverUrl, channel?.teamId || '', call.threadId, true, false);
|
||||
}
|
||||
};
|
||||
|
||||
export const callEnded = (serverUrl: string, channelId: string) => {
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import {map, switchMap, distinctUntilChanged} from 'rxjs/operators';
|
|||
|
||||
import {Config} from '@constants';
|
||||
import {MM_TABLES} from '@constants/database';
|
||||
import {PostTypes} from '@constants/post';
|
||||
import {processIsCRTAllowed, processIsCRTEnabled} from '@utils/thread';
|
||||
|
||||
import {observeChannel} from './channel';
|
||||
|
|
@ -116,7 +117,7 @@ export const prepareThreadsFromReceivedPosts = async (operator: ServerDataOperat
|
|||
let processedThreads: Set<string> | undefined;
|
||||
|
||||
posts.forEach((post: Post) => {
|
||||
if (!post.root_id && post.type === '') {
|
||||
if (!post.root_id && ['', PostTypes.CUSTOM_CALLS].includes(post.type)) {
|
||||
threads.push({
|
||||
id: post.id,
|
||||
participants: post.participants,
|
||||
|
|
|
|||
Loading…
Reference in a new issue