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:
Christopher Poile 2023-04-26 15:20:07 -04:00 committed by GitHub
parent 17dfb96c93
commit e6254885ee
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 91 additions and 7 deletions

View file

@ -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();

View file

@ -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();

View file

@ -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', () => {

View file

@ -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) => {

View file

@ -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,